Merge "Make differ main thread executor use ArchTaskExecutor again" into oc-mr1-jetpack-dev
diff --git a/paging/common/src/main/java/android/arch/paging/ContiguousPagedList.java b/paging/common/src/main/java/android/arch/paging/ContiguousPagedList.java
index 2401e6e..b540ce1 100644
--- a/paging/common/src/main/java/android/arch/paging/ContiguousPagedList.java
+++ b/paging/common/src/main/java/android/arch/paging/ContiguousPagedList.java
@@ -237,6 +237,12 @@
         return true;
     }
 
+    @NonNull
+    @Override
+    public DataSource<?, V> getDataSource() {
+        return mDataSource;
+    }
+
     @Nullable
     @Override
     public Object getLastKey() {
diff --git a/paging/common/src/main/java/android/arch/paging/DataSource.java b/paging/common/src/main/java/android/arch/paging/DataSource.java
index cf21180..78d7dcb 100644
--- a/paging/common/src/main/java/android/arch/paging/DataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/DataSource.java
@@ -16,11 +16,13 @@
 
 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;
@@ -130,12 +132,66 @@
         DataSource<Key, Value> create();
     }
 
+    @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 loads items produced by the passed 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 loads items produced by the passed 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.
      */
diff --git a/paging/common/src/main/java/android/arch/paging/ItemKeyedDataSource.java b/paging/common/src/main/java/android/arch/paging/ItemKeyedDataSource.java
index 5d0c972..f68fae1 100644
--- a/paging/common/src/main/java/android/arch/paging/ItemKeyedDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/ItemKeyedDataSource.java
@@ -16,6 +16,7 @@
 
 package android.arch.paging;
 
+import android.arch.core.util.Function;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 
@@ -356,4 +357,18 @@
      */
     @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/PageKeyedDataSource.java b/paging/common/src/main/java/android/arch/paging/PageKeyedDataSource.java
index 477a35b..7fc5166 100644
--- a/paging/common/src/main/java/android/arch/paging/PageKeyedDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/PageKeyedDataSource.java
@@ -16,6 +16,7 @@
 
 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;
@@ -407,4 +408,18 @@
      */
     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/PagedList.java b/paging/common/src/main/java/android/arch/paging/PagedList.java
index e04da36..a2ea3ab 100644
--- a/paging/common/src/main/java/android/arch/paging/PagedList.java
+++ b/paging/common/src/main/java/android/arch/paging/PagedList.java
@@ -31,23 +31,25 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
- * Lazy loading list that pages in content from a {@link DataSource}.
+ * 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)}. See {@link PagedListAdapter}, which enables the binding of a PagedList
- * to a {@link android.support.v7.widget.RecyclerView}.
+ * {@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 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.
+ * 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>
- * 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.
+ * 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.
@@ -91,6 +93,21 @@
  * 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.
  */
@@ -504,8 +521,12 @@
     }
 
     /**
-     * Returns whether the list is immutable. Immutable lists may not become mutable again, and may
-     * safely be accessed from any thread.
+     * 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.
      */
@@ -515,8 +536,10 @@
     }
 
     /**
-     * Returns an immutable snapshot of the PagedList. If this PagedList is already
-     * immutable, it will be returned.
+     * 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.
      */
@@ -542,6 +565,14 @@
     }
 
     /**
+     * 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
@@ -557,6 +588,8 @@
     /**
      * 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.
      */
diff --git a/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java b/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
index a70e8d9..1fe6725 100644
--- a/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
@@ -16,6 +16,7 @@
 
 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;
@@ -467,6 +468,22 @@
             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,
@@ -511,5 +528,19 @@
         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
index 6a8a748..4627d2a 100644
--- a/paging/common/src/main/java/android/arch/paging/SnapshotPagedList.java
+++ b/paging/common/src/main/java/android/arch/paging/SnapshotPagedList.java
@@ -22,6 +22,7 @@
 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(),
@@ -29,6 +30,7 @@
                 pagedList.mBackgroundThreadExecutor,
                 null,
                 pagedList.mConfig);
+        mDataSource = pagedList.getDataSource();
         mContiguous = pagedList.isContiguous();
         mLastKey = pagedList.getLastKey();
     }
@@ -54,6 +56,12 @@
         return mLastKey;
     }
 
+    @NonNull
+    @Override
+    public DataSource<?, T> getDataSource() {
+        return mDataSource;
+    }
+
     @Override
     void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> storageSnapshot,
             @NonNull Callback callback) {
diff --git a/paging/common/src/main/java/android/arch/paging/TiledPagedList.java b/paging/common/src/main/java/android/arch/paging/TiledPagedList.java
index 9958b8d..374fe2a 100644
--- a/paging/common/src/main/java/android/arch/paging/TiledPagedList.java
+++ b/paging/common/src/main/java/android/arch/paging/TiledPagedList.java
@@ -106,6 +106,12 @@
         return false;
     }
 
+    @NonNull
+    @Override
+    public DataSource<?, T> getDataSource() {
+        return mDataSource;
+    }
+
     @Nullable
     @Override
     public Object getLastKey() {
diff --git a/paging/common/src/main/java/android/arch/paging/WrapperItemKeyedDataSource.java b/paging/common/src/main/java/android/arch/paging/WrapperItemKeyedDataSource.java
new file mode 100644
index 0000000..93520ca
--- /dev/null
+++ b/paging/common/src/main/java/android/arch/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 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
new file mode 100644
index 0000000..e6fa274
--- /dev/null
+++ b/paging/common/src/main/java/android/arch/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 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
new file mode 100644
index 0000000..0626f87
--- /dev/null
+++ b/paging/common/src/main/java/android/arch/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 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/test/java/android/arch/paging/ContiguousPagedListTest.kt b/paging/common/src/test/java/android/arch/paging/ContiguousPagedListTest.kt
index de4e3d8..a0dbfb7 100644
--- a/paging/common/src/test/java/android/arch/paging/ContiguousPagedListTest.kt
+++ b/paging/common/src/test/java/android/arch/paging/ContiguousPagedListTest.kt
@@ -16,9 +16,11 @@
 
 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
@@ -103,6 +105,16 @@
         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>) {
@@ -158,6 +170,16 @@
         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) {
diff --git a/paging/common/src/test/java/android/arch/paging/ItemKeyedDataSourceTest.kt b/paging/common/src/test/java/android/arch/paging/ItemKeyedDataSourceTest.kt
index dd4dcaa..060094a 100644
--- a/paging/common/src/test/java/android/arch/paging/ItemKeyedDataSourceTest.kt
+++ b/paging/common/src/test/java/android/arch/paging/ItemKeyedDataSourceTest.kt
@@ -394,8 +394,8 @@
         }
     }
 
-    @Test
-    fun simpleWrappedDataSource() {
+    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)
@@ -427,6 +427,21 @@
         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 })
diff --git a/paging/common/src/test/java/android/arch/paging/PageKeyedDataSourceTest.kt b/paging/common/src/test/java/android/arch/paging/PageKeyedDataSourceTest.kt
index 3f6c7c8..f7ea4b6 100644
--- a/paging/common/src/test/java/android/arch/paging/PageKeyedDataSourceTest.kt
+++ b/paging/common/src/test/java/android/arch/paging/PageKeyedDataSourceTest.kt
@@ -214,11 +214,11 @@
         }
     }
 
-    @Test
-    fun simpleWrappedDataSource() {
+    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 = StringWrapperDataSource<String, Item>(orig)
+        val wrapper = createWrapper(orig)
 
         // load initial
         @Suppress("UNCHECKED_CAST")
@@ -232,10 +232,10 @@
                 expectedInitial.prev, expectedInitial.next)
         verifyNoMoreInteractions(loadInitialCallback)
 
-        // load after
         @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() },
@@ -249,6 +249,20 @@
         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"
diff --git a/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt b/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt
index 672fdaa..eab778c 100644
--- a/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt
+++ b/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt
@@ -255,18 +255,17 @@
         protected abstract fun convert(source: List<A>): List<B>
     }
 
-    private class StringWrapper<in A>(source: PositionalDataSource<A>)
+    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() }
         }
     }
 
-    @Test
-    fun simpleWrappedDataSource() {
-        // verify that it's possible to wrap a PositionalDataSource, and transform its data
+    private fun verifyWrappedDataSource(
+            createWrapper: (PositionalDataSource<Int>) -> PositionalDataSource<String>) {
         val orig = ListDataSource(listOf(0, 5, 4, 8, 12))
-        val wrapper = StringWrapper(orig)
+        val wrapper = createWrapper(orig)
 
         // load initial
         @Suppress("UNCHECKED_CAST")
@@ -294,4 +293,19 @@
         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/TiledPagedListTest.kt b/paging/common/src/test/java/android/arch/paging/TiledPagedListTest.kt
index 9069a1a..fbd8f54 100644
--- a/paging/common/src/test/java/android/arch/paging/TiledPagedListTest.kt
+++ b/paging/common/src/test/java/android/arch/paging/TiledPagedListTest.kt
@@ -69,6 +69,16 @@
     }
 
     @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)
diff --git a/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.kt b/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.kt
index 48d51bc..00e8b0e 100644
--- a/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.kt
+++ b/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.kt
@@ -52,4 +52,8 @@
     override fun onPagePlaceholderInserted(pageIndex: Int) {}
 
     override fun onPageInserted(start: Int, count: Int) {}
+
+    override fun getDataSource(): DataSource<*, String> {
+        throw UnsupportedOperationException()
+    }
 }