Merge changes from topic "am-98104462-c8b8-4af3-954f-1032bea680ea"

* changes:
  [automerger] Added sizing api to FAB am: 993e74168a
  Added sizing api to FAB
diff --git a/.gitignore b/.gitignore
index abfef7c..162af55 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,8 @@
 .classpath
 .gradle
-.idea/
-!.idea/codeStyleSettings.xml
+**/.idea/**
+!**/.idea/codeStyleSettings.xml
+!**/.idea/copyright
 !.idea/copyright/AndroidCopyright.xml
 !.idea/copyright/profiles_settings.xml
 !.idea/vcs.xml
diff --git a/app-toolkit/.idea/codeStyleSettings.xml b/app-toolkit/.idea/codeStyleSettings.xml
new file mode 120000
index 0000000..13e0b76
--- /dev/null
+++ b/app-toolkit/.idea/codeStyleSettings.xml
@@ -0,0 +1 @@
+../../.idea/codeStyleSettings.xml
\ No newline at end of file
diff --git a/app-toolkit/.idea/copyright b/app-toolkit/.idea/copyright
new file mode 120000
index 0000000..51a82b0
--- /dev/null
+++ b/app-toolkit/.idea/copyright
@@ -0,0 +1 @@
+../../.idea/copyright
\ No newline at end of file
diff --git a/app-toolkit/core-testing/build.gradle b/app-toolkit/core-testing/build.gradle
index 7f7f3db..adb93e6 100644
--- a/app-toolkit/core-testing/build.gradle
+++ b/app-toolkit/core-testing/build.gradle
@@ -29,31 +29,21 @@
 }
 
 dependencies {
-    compile project(":arch:runtime")
-    compile libs.support.annotations
-    compile(libs.junit)
-    compile libs.mockito_core, { exclude group: 'net.bytebuddy' }
+    api project(":arch:runtime")
+    api libs.support.annotations
+    api libs.junit
+    api libs.mockito_core, { exclude group: 'net.bytebuddy' }
 
-    testCompile libs.junit
-    testCompile libs.support.annotations
+    testImplementation libs.junit
+    testImplementation libs.support.annotations
 
-    androidTestCompile libs.junit
+    androidTestImplementation libs.junit
     androidTestImplementation libs.test_runner,      { exclude module: 'support-annotations' }
     androidTestImplementation libs.espresso_core,    { exclude module: 'support-annotations' }
 }
 
 createAndroidCheckstyle(project)
 
-android.libraryVariants.all { variant ->
-    def name = variant.buildType.name
-    def suffix = name.capitalize()
-    project.tasks.create(name: "jar${suffix}", type: Jar) {
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        destinationDir new File(project.buildDir, "libJar")
-    }
-}
-
 supportLibrary {
     name = "Android Core-Testing"
     publish = true
diff --git a/app-toolkit/dependencies.gradle b/app-toolkit/dependencies.gradle
index 54df9d3..2a63f42 100644
--- a/app-toolkit/dependencies.gradle
+++ b/app-toolkit/dependencies.gradle
@@ -22,7 +22,7 @@
     ffLibs = libs
 }
 def ffVersions = [:]
-ffVersions.kotlin = "1.1.3"
+ffVersions.kotlin = "1.1.51"
 ffVersions.auto_common = "0.6"
 ffVersions.javapoet = "1.8.0"
 ffVersions.compile_testing = "0.11"
diff --git a/app-toolkit/runtime/build.gradle b/app-toolkit/runtime/build.gradle
index 526ef4e..3b46384 100644
--- a/app-toolkit/runtime/build.gradle
+++ b/app-toolkit/runtime/build.gradle
@@ -26,29 +26,15 @@
     defaultConfig {
         minSdkVersion flatfoot.min_sdk
     }
-
-    testOptions {
-        unitTests.returnDefaultValues = true
-    }
 }
 
 dependencies {
-    compile libs.support.annotations
-    compile project(":arch:common")
+    api libs.support.annotations
+    api project(":arch:common")
 }
 
 createAndroidCheckstyle(project)
 
-android.libraryVariants.all { variant ->
-    def name = variant.buildType.name
-    def suffix = name.capitalize()
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        destinationDir new File(project.buildDir, "libJar")
-    }
-}
-
 supportLibrary {
     name = "Android Arch-Runtime"
     publish = true
diff --git a/buildSrc/build_dependencies.gradle b/buildSrc/build_dependencies.gradle
index b7a5049..dc41841 100644
--- a/buildSrc/build_dependencies.gradle
+++ b/buildSrc/build_dependencies.gradle
@@ -29,6 +29,6 @@
 build_libs.jarjar_gradle = 'org.anarres.jarjar:jarjar-gradle:1.0.0'
 build_libs.error_prone = 'net.ltgt.gradle:gradle-errorprone-plugin:0.0.10'
 build_libs.jacoco = 'org.jacoco:org.jacoco.core:0.7.8'
-build_libs.kotlin = [gradle_plugin: "org.jetbrains.kotlin:kotlin-gradle-plugin:1.1.3"]
+build_libs.kotlin = [gradle_plugin: "org.jetbrains.kotlin:kotlin-gradle-plugin:1.1.51"]
 
 rootProject.ext['build_libs'] = build_libs
diff --git a/buildSrc/src/main/groovy/android/support/SupportAndroidLibraryPlugin.groovy b/buildSrc/src/main/groovy/android/support/SupportAndroidLibraryPlugin.groovy
deleted file mode 100644
index 6f1288e..0000000
--- a/buildSrc/src/main/groovy/android/support/SupportAndroidLibraryPlugin.groovy
+++ /dev/null
@@ -1,167 +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.support
-
-import com.android.build.gradle.LibraryExtension
-import com.android.build.gradle.api.LibraryVariant
-import com.android.builder.core.BuilderConstants
-import com.google.common.collect.ImmutableMap
-import net.ltgt.gradle.errorprone.ErrorProneBasePlugin
-import net.ltgt.gradle.errorprone.ErrorProneToolChain
-import org.gradle.api.Action
-import org.gradle.api.JavaVersion
-import org.gradle.api.Plugin
-import org.gradle.api.Project
-import org.gradle.api.tasks.bundling.Jar
-
-/**
- * Support library specific com.android.library plugin that sets common configurations needed for
- * support library modules.
- */
-class SupportAndroidLibraryPlugin implements Plugin<Project> {
-    private static final String INSTRUMENTATION_RUNNER =
-            "android.support.test.runner.AndroidJUnitRunner";
-
-    @Override
-    public void apply(Project project) {
-        KotlinNoOp noOp = new KotlinNoOp()
-        noOp.noOp()
-        SupportLibraryExtension supportLibraryExtension =
-                project.extensions.create("supportLibrary", SupportLibraryExtension, project);
-        SupportLibraryMavenUploader.apply(project, supportLibraryExtension);
-
-        project.afterEvaluate {
-            LibraryExtension library = project.extensions.findByType(LibraryExtension.class);
-
-            if (supportLibraryExtension.legacySourceLocation) {
-                library.sourceSets {
-                    main {
-                        // We use a non-standard manifest path.
-                        manifest.srcFile 'AndroidManifest.xml'
-                    }
-
-                    androidTest {
-                        // We use a non-standard test directory structure.
-                        root 'tests'
-                        java.srcDir 'tests/src'
-                        res.srcDir 'tests/res'
-                        manifest.srcFile 'tests/AndroidManifest.xml'
-                    }
-                }
-            }
-
-            // Java 8 is only fully supported on API 24+ and not all Java 8 features are binary
-            // compatible with API < 24, so use Java 7 for both source AND target.
-            final JavaVersion javaVersion;
-            if (supportLibraryExtension.java8Library) {
-                if (library.defaultConfig.minSdkVersion.apiLevel < 24) {
-                    throw new IllegalArgumentException("Libraries can only support Java 8 if "
-                            + "minSdkVersion is 24 or higher");
-                }
-                javaVersion = JavaVersion.VERSION_1_8
-            } else {
-                javaVersion = JavaVersion.VERSION_1_7
-            }
-
-            library.compileOptions {
-                sourceCompatibility javaVersion
-                targetCompatibility javaVersion
-            }
-        }
-
-        VersionFileWriterTask.setUpAndroidLibrary(project);
-
-        project.apply(ImmutableMap.of("plugin", "com.android.library"));
-        project.apply(ImmutableMap.of("plugin", ErrorProneBasePlugin.class));
-
-        LibraryExtension library = project.extensions.findByType(LibraryExtension.class);
-
-        library.compileSdkVersion project.currentSdk
-
-        library.defaultConfig {
-            // Update the version meta-data in each Manifest.
-            addManifestPlaceholders(["target-sdk-version": project.currentSdk])
-
-            // Set test related options.
-            testInstrumentationRunner INSTRUMENTATION_RUNNER
-        }
-
-        library.signingConfigs {
-            debug {
-                // Use a local debug keystore to avoid build server issues.
-                storeFile project.rootProject.init.debugKeystore
-            }
-        }
-
-        // Always lint check NewApi as fatal.
-        library.lintOptions {
-            abortOnError true
-            ignoreWarnings true
-
-            // Write output directly to the console (and nowhere else).
-            textOutput 'stderr'
-            textReport true
-            htmlReport false
-            //xmlReport false
-
-            // Format output for convenience.
-            explainIssues true
-            noLines false
-            quiet true
-
-            // Always fail on NewApi.
-            error 'NewApi'
-        }
-
-        // Set baseline file for all legacy lint warnings.
-        if (System.getenv("GRADLE_PLUGIN_VERSION") != null) {
-            library.lintOptions.check 'NewApi'
-        } else {
-            library.lintOptions.baseline new File(project.projectDir, "/lint-baseline.xml")
-        }
-
-        if (project.rootProject.usingFullSdk) {
-            // Library projects don't run lint by default, so set up dependency.
-            project.uploadArchives.dependsOn "lintRelease"
-        }
-
-        SourceJarTaskHelper.setUpAndroidProject(project, library);
-
-        final ErrorProneToolChain toolChain = ErrorProneToolChain.create(project);
-        library.getBuildTypes().create("errorProne")
-        library.getLibraryVariants().all(new Action<LibraryVariant>() {
-            @Override
-            void execute(LibraryVariant libraryVariant) {
-                if (libraryVariant.getBuildType().getName().equals("errorProne")) {
-                    libraryVariant.getJavaCompile().setToolChain(toolChain);
-
-                    libraryVariant.getJavaCompile().options.compilerArgs += [
-                            '-XDcompilePolicy=simple', // Workaround for b/36098770
-
-                            // Enforce the following checks.
-                            '-Xep:MissingOverride:ERROR',
-                            '-Xep:NarrowingCompoundAssignment:ERROR',
-                            '-Xep:ClassNewInstance:ERROR',
-                            '-Xep:ClassCanBeStatic:ERROR',
-                            '-Xep:SynchronizeOnNonFinalField:ERROR',
-                            '-Xep:OperatorPrecedence:ERROR'
-                    ]
-                }
-            }
-        })
-    }
-}
diff --git a/buildSrc/src/main/groovy/android/support/SupportJavaLibraryPlugin.groovy b/buildSrc/src/main/groovy/android/support/SupportJavaLibraryPlugin.groovy
deleted file mode 100644
index 3113e6c..0000000
--- a/buildSrc/src/main/groovy/android/support/SupportJavaLibraryPlugin.groovy
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR 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
-
-import com.google.common.collect.ImmutableMap
-import org.gradle.api.JavaVersion
-import org.gradle.api.Plugin
-import org.gradle.api.Project
-import org.gradle.api.plugins.JavaPluginConvention
-
-/**
- * Support java library specific plugin that sets common configurations needed for
- * support library modules.
- */
-class SupportJavaLibraryPlugin implements Plugin<Project> {
-    @Override
-    public void apply(Project project) {
-        SupportLibraryExtension supportLibraryExtension =
-                project.extensions.create("supportLibrary", SupportLibraryExtension, project);
-        SupportLibraryMavenUploader.apply(project, supportLibraryExtension);
-
-        project.apply(ImmutableMap.of("plugin", "java"));
-        project.afterEvaluate {
-            project.compileJava {
-                def version = supportLibraryExtension.java8Library ?
-                    JavaVersion.VERSION_1_8 : JavaVersion.VERSION_1_7
-                sourceCompatibility = version
-                targetCompatibility = version
-            }
-        }
-
-        SourceJarTaskHelper.setUpJavaProject(project);
-    }
-}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/android/support/SupportKotlinLibraryPlugin.groovy b/buildSrc/src/main/groovy/android/support/SupportKotlinLibraryPlugin.groovy
index 237aa97..78b4777 100644
--- a/buildSrc/src/main/groovy/android/support/SupportKotlinLibraryPlugin.groovy
+++ b/buildSrc/src/main/groovy/android/support/SupportKotlinLibraryPlugin.groovy
@@ -30,7 +30,7 @@
     public void apply(Project project) {
         SupportLibraryExtension supportLibraryExtension =
                 project.extensions.create("supportLibrary", SupportLibraryExtension, project);
-        SupportLibraryMavenUploader.apply(project, supportLibraryExtension);
+        MavenUploadHelperKt.apply(project, supportLibraryExtension);
 
         project.apply(ImmutableMap.of("plugin", "kotlin"));
         project.apply(ImmutableMap.of("plugin", "kotlin-kapt"));
diff --git a/buildSrc/src/main/groovy/android/support/SupportLibraryExtension.groovy b/buildSrc/src/main/groovy/android/support/SupportLibraryExtension.groovy
deleted file mode 100644
index 8f86e33..0000000
--- a/buildSrc/src/main/groovy/android/support/SupportLibraryExtension.groovy
+++ /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.support;
-
-import org.gradle.api.Project
-
-/**
- * Extension for {@link SupportAndroidLibraryPlugin} and {@link SupportJavaLibraryPlugin}.
- */
-class SupportLibraryExtension {
-    static final String ARCHITECTURE_URL =
-            "https://developer.android.com/topic/libraries/architecture/index.html";
-    static final String SUPPORT_URL =
-            "http://developer.android.com/tools/extras/support-library.html";
-
-    Project project
-    String name;
-    Version mavenVersion;
-    String mavenGroup;
-    String description;
-    String inceptionYear;
-    String url = SUPPORT_URL;
-    Collection<License> licenses = [];
-    boolean java8Library = false;
-    boolean legacySourceLocation = false;
-    boolean publish = false;
-
-    SupportLibraryExtension(Project project) {
-        this.project = project
-    }
-
-    License license(Closure closure) {
-        def license = project.configure(new License(), closure)
-        licenses.add(license)
-        return license
-    }
-
-    class License {
-        String name;
-        String url;
-
-        void url(String p) {
-            url = p
-        }
-
-        void name(String p) {
-            name = p
-        }
-    }
-}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/android/support/SupportLibraryMavenUploader.groovy b/buildSrc/src/main/groovy/android/support/SupportLibraryMavenUploader.groovy
deleted file mode 100644
index b557ec3..0000000
--- a/buildSrc/src/main/groovy/android/support/SupportLibraryMavenUploader.groovy
+++ /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.support
-
-import com.android.build.gradle.LibraryPlugin
-import com.google.common.collect.ImmutableMap
-import org.gradle.api.Action
-import org.gradle.api.Project
-import org.gradle.api.artifacts.ProjectDependency
-import org.gradle.api.artifacts.maven.MavenDeployer
-import org.gradle.api.tasks.Upload
-
-class SupportLibraryMavenUploader {
-    static void apply(Project project, SupportLibraryExtension supportLibraryExtension) {
-        project.afterEvaluate {
-            if (supportLibraryExtension.publish) {
-                if (supportLibraryExtension.mavenGroup == null) {
-                    throw Exception("You must specify mavenGroup for " + project.name + " project");
-                }
-                if (supportLibraryExtension.mavenVersion == null) {
-                    throw Exception("You must specify mavenVersion for " + project.name + " project");
-                }
-                project.group = supportLibraryExtension.mavenGroup
-                project.version = supportLibraryExtension.mavenVersion.toString()
-            }
-        }
-
-        project.apply(ImmutableMap.of("plugin", "maven"));
-
-        // Set uploadArchives options.
-        Upload uploadTask = (Upload) project.getTasks().getByName("uploadArchives");
-        project.afterEvaluate {
-            if (supportLibraryExtension.publish) {
-                uploadTask.getRepositories().withType(MavenDeployer.class, new Action<MavenDeployer>() {
-                    @Override
-                    public void execute(MavenDeployer mavenDeployer) {
-                        mavenDeployer.getPom().project {
-                            name supportLibraryExtension.getName()
-                            description supportLibraryExtension.getDescription()
-                            url supportLibraryExtension.getUrl()
-                            inceptionYear supportLibraryExtension.getInceptionYear()
-
-                            licenses {
-                                license {
-                                    name 'The Apache Software License, Version 2.0'
-                                    url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
-                                    distribution 'repo'
-                                }
-
-                                supportLibraryExtension.getLicenses().each {
-                                    SupportLibraryExtension.License supportLicense ->
-                                        license {
-                                            name supportLicense.name
-                                            url supportLicense.url
-                                            distribution 'repo'
-                                        }
-                                }
-                            }
-
-                            scm {
-                                url "http://source.android.com"
-                                connection "scm:git:https://android.googlesource.com/platform/frameworks/support"
-                            }
-                            developers {
-                                developer {
-                                    name 'The Android Open Source Project'
-                                }
-                            }
-                        }
-
-                        uploadTask.doFirst {
-                            Set<ProjectDependency> allDeps = new HashSet<>();
-                            collectDependenciesForConfiguration(allDeps, project, "api");
-                            collectDependenciesForConfiguration(allDeps, project, "implementation");
-                            collectDependenciesForConfiguration(allDeps, project, "compile");
-
-                            mavenDeployer.getPom().whenConfigured {
-                                it.dependencies.forEach { dep ->
-                                    if (isAndroidProject(dep.groupId, dep.artifactId, allDeps)) {
-                                        dep.type = "aar"
-                                    }
-                                }
-                            }
-                        }
-                    }
-                });
-            } else {
-                uploadTask.enabled = false;
-            }
-        }
-    }
-
-    private static void collectDependenciesForConfiguration(Set<ProjectDependency> dependencies,
-            Project project, String name) {
-        def config = project.configurations.findByName(name);
-        if (config != null) {
-            config.dependencies.withType(ProjectDependency.class).forEach {
-                dep -> dependencies.add(dep)
-            }
-        }
-    }
-
-    private static boolean isAndroidProject(String groupId, String artifactId, Set<ProjectDependency> deps) {
-        for (ProjectDependency dep : deps) {
-            if (dep.group == groupId && dep.name == artifactId) {
-                return dep.getDependencyProject().plugins.hasPlugin(LibraryPlugin.class)
-            }
-        }
-        return false;
-    }
-}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy b/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy
deleted file mode 100644
index 4f829f6..0000000
--- a/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy
+++ /dev/null
@@ -1,280 +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.support.checkapi
-
-import org.gradle.api.DefaultTask
-import org.gradle.api.GradleException
-import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.InputFile
-import org.gradle.api.tasks.InputFiles
-import org.gradle.api.tasks.TaskAction
-import org.gradle.api.tasks.Optional
-import org.gradle.api.tasks.OutputFile
-
-import java.security.MessageDigest
-
-/**
- * Task used to verify changes between two API files.
- * <p>
- * This task may be configured to ignore, warn, or fail with a message for a specific set of
- * Doclava-defined error codes. See {@link com.google.doclava.Errors} for a complete list of
- * supported error codes.
- * <p>
- * Specific failures may be ignored by specifying a list of SHAs in {@link #whitelistErrors}. Each
- * SHA is unique to a specific API change and is logged to the error output on failure.
- */
-public class CheckApiTask extends DefaultTask {
-    /** Character that resets console output color. */
-    private static final String ANSI_RESET = "\u001B[0m";
-
-    /** Character that sets console output color to red. */
-    private static final String ANSI_RED = "\u001B[31m";
-
-    /** Character that sets console output color to yellow. */
-    private static final String ANSI_YELLOW = "\u001B[33m";
-
-    /** API file that represents the existing API surface. */
-    @Optional
-    @InputFile
-    File oldApiFile
-
-    /** API file that represents the existing API surface's removals. */
-    @Optional
-    @InputFile
-    File oldRemovedApiFile
-
-    /** API file that represents the candidate API surface. */
-    @InputFile
-    File newApiFile
-
-    /** API file that represents the candidate API surface's removals. */
-    @Optional
-    @InputFile
-    File newRemovedApiFile
-
-    /** Optional file containing a newline-delimited list of error SHAs to ignore. */
-    File whitelistErrorsFile
-
-    @Optional
-    @InputFile
-    File getWhiteListErrorsFileInput() {
-        // Gradle requires non-null InputFiles to exist -- even with Optional -- so work around that
-        // by returning null for this field if the file doesn't exist.
-        if (whitelistErrorsFile && whitelistErrorsFile.exists()) {
-            return whitelistErrorsFile;
-        }
-        return null;
-    }
-
-    /**
-     * Optional list of packages to ignore.
-     * <p>
-     * Packages names will be matched exactly; sub-packages are not automatically recognized.
-     */
-    @Optional
-    @Input
-    Collection ignoredPackages
-
-    /**
-     * Optional list of classes to ignore.
-     * <p>
-     * Class names will be matched exactly by their fully-qualified names; inner classes are not
-     * automatically recognized.
-     */
-    @Optional
-    @Input
-    Collection ignoredClasses
-
-    /**
-     * Optional set of error SHAs to ignore.
-     * <p>
-     * Each error SHA is unique to a specific API change.
-     */
-    @Optional
-    @Input
-    Set whitelistErrors = []
-
-    // Errors that were both detected and whitelisted.
-    Set detectedWhitelistErrors = []
-
-    @InputFiles
-    Collection<File> doclavaClasspath
-
-    // A dummy output file meant only to tag when this check was last ran.
-    // Without any outputs, Gradle will run this task every time.
-    @Optional
-    private File mOutputFile
-
-    @OutputFile
-    public File getOutputFile() {
-        return mOutputFile ?: new File(project.buildDir, "checkApi/${name}-completed")
-    }
-
-    @Optional
-    public void setOutputFile(File outputFile) {
-        mOutputFile = outputFile
-    }
-
-    /**
-     * List of Doclava error codes to treat as errors.
-     * <p>
-     * See {@link com.google.doclava.Errors} for a complete list of error codes.
-     */
-    @Input
-    Collection checkApiErrors
-
-    /**
-     * List of Doclava error codes to treat as warnings.
-     * <p>
-     * See {@link com.google.doclava.Errors} for a complete list of error codes.
-     */
-    @Input
-    Collection checkApiWarnings
-
-    /**
-     * List of Doclava error codes to ignore.
-     * <p>
-     * See {@link com.google.doclava.Errors} for a complete list of error codes.
-     */
-    @Input
-    Collection checkApiHidden
-
-    /** Message to display on API check failure. */
-    @Input
-    String onFailMessage
-
-    public CheckApiTask() {
-        group = 'Verification'
-        description = 'Invoke Doclava\'s ApiCheck tool to make sure current.txt is up to date.'
-    }
-
-    private Set<File> collectAndVerifyInputs() {
-        if (getOldRemovedApiFile() != null && getNewRemovedApiFile() != null) {
-            return [getOldApiFile(), getNewApiFile(), getOldRemovedApiFile(),
-                    getNewRemovedApiFile()] as Set
-        } else {
-            return [getOldApiFile(), getNewApiFile()] as Set
-        }
-    }
-
-    public void setCheckApiErrors(Collection errors) {
-        // Make it serializable.
-        checkApiErrors = errors as int[]
-    }
-
-    public void setCheckApiWarnings(Collection warnings) {
-        // Make it serializable.
-        checkApiWarnings = warnings as int[]
-    }
-
-    public void setCheckApiHidden(Collection hidden) {
-        // Make it serializable.
-        checkApiHidden = hidden as int[]
-    }
-
-    @TaskAction
-    public void exec() {
-        if (getOldApiFile() == null) {
-            // Nothing to do.
-            return
-        }
-
-        final def apiFiles = collectAndVerifyInputs()
-
-        OutputStream errStream = new ByteArrayOutputStream()
-
-        // If either of those gets tweaked, then this should be refactored to extend JavaExec.
-        project.javaexec {
-            // Put Doclava on the classpath so we can get the ApiCheck class.
-            classpath(getDoclavaClasspath())
-            main = 'com.google.doclava.apicheck.ApiCheck'
-
-            minHeapSize = '128m'
-            maxHeapSize = '1024m'
-
-            // add -error LEVEL for every error level we want to fail the build on.
-            getCheckApiErrors().each { args('-error', it) }
-            getCheckApiWarnings().each { args('-warning', it) }
-            getCheckApiHidden().each { args('-hide', it) }
-
-            Collection ignoredPackages = getIgnoredPackages()
-            if (ignoredPackages) {
-                ignoredPackages.each { args('-ignorePackage', it) }
-            }
-            Collection ignoredClasses = getIgnoredClasses()
-            if (ignoredClasses) {
-                ignoredClasses.each { args('-ignoreClass', it) }
-            }
-
-            args(apiFiles.collect( { it.absolutePath } ))
-
-            // Redirect error output so that we can whitelist specific errors.
-            errorOutput = errStream
-
-            // We will be handling failures ourselves with a custom message.
-            ignoreExitValue = true
-        }
-
-        // Load the whitelist file, if present.
-        if (whitelistErrorsFile && whitelistErrorsFile.exists()) {
-            whitelistErrors += whitelistErrorsFile.readLines()
-        }
-
-        // Parse the error output.
-        Set unparsedErrors = []
-        Set detectedErrors = []
-        Set parsedErrors = []
-        errStream.toString().split("\n").each {
-            if (it) {
-                def matcher = it =~ ~/^(.+):(.+): (\w+) (\d+): (.+)$/
-                if (!matcher) {
-                    unparsedErrors += [it]
-                } else if (matcher[0][3] == "error") {
-                    def hash = getShortHash(matcher[0][5]);
-                    def error = matcher[0][1..-1] + [hash]
-                    if (hash in whitelistErrors) {
-                        detectedErrors += [error]
-                        detectedWhitelistErrors += error[5]
-                    } else {
-                        parsedErrors += [error]
-                    }
-                }
-            }
-        }
-
-        unparsedErrors.each { error -> logger.error "$ANSI_RED$error$ANSI_RESET" }
-        parsedErrors.each { logger.error "$ANSI_RED${it[5]}$ANSI_RESET ${it[4]}"}
-        detectedErrors.each { logger.warn "$ANSI_YELLOW${it[5]}$ANSI_RESET ${it[4]}"}
-
-        if (unparsedErrors || parsedErrors) {
-            throw new GradleException(onFailMessage)
-        }
-
-        // Just create a dummy file upon completion. Without any outputs, Gradle will run this task
-        // every time.
-        File outputFile = getOutputFile()
-        outputFile.parentFile.mkdirs()
-        outputFile.createNewFile()
-    }
-
-    def getShortHash(src) {
-        return MessageDigest.getInstance("SHA-1")
-                .digest(src.toString().bytes)
-                .encodeHex()
-                .toString()[-7..-1]
-    }
-}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/android/support/GroovyInteroperability.kt b/buildSrc/src/main/kotlin/android/support/GroovyInteroperability.kt
new file mode 100644
index 0000000..ec2d2f6
--- /dev/null
+++ b/buildSrc/src/main/kotlin/android/support/GroovyInteroperability.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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
+
+import groovy.lang.Closure
+import groovy.lang.GroovyObject
+import groovy.lang.MetaClass
+
+import org.codehaus.groovy.runtime.InvokerHelper.getMetaClass
+
+operator fun <T> Closure<T>.invoke(): T = call()
+
+operator fun <T> Closure<T>.invoke(x: Any?): T = call(x)
+
+operator fun <T> Closure<T>.invoke(vararg xs: Any?): T = call(*xs)
+
+
+/**
+ * Executes the given [builder] against this object's [GroovyBuilderScope].
+ *
+ * @see [GroovyBuilderScope]
+ */
+inline
+fun <T> Any.withGroovyBuilder(builder: GroovyBuilderScope.() -> T): T =
+        GroovyBuilderScope.of(this).builder()
+
+
+/**
+ * Provides a dynamic dispatching DSL with Groovy semantics for better integration with
+ * plugins that rely on Groovy builders such as the core `maven` plugin.
+ *
+ * It supports Groovy keyword arguments and arbitrary nesting, for instance, the following Groovy code:
+ *
+ * ```Groovy
+ * repository(url: "scp://repos.mycompany.com/releases") {
+ *   authentication(userName: "me", password: "myPassword")
+ * }
+ * ```
+ *
+ * Can be mechanically translated to the following Kotlin with the aid of `withGroovyBuilder`:
+ *
+ * ```Kotlin
+ * withGroovyBuilder {
+ *   "repository"("url" to "scp://repos.mycompany.com/releases") {
+ *     "authentication"("userName" to "me", "password" to "myPassword")
+ *   }
+ * }
+ * ```
+ *
+ * @see [withGroovyBuilder]
+ */
+interface GroovyBuilderScope : GroovyObject {
+
+    companion object {
+
+        fun of(value: Any): GroovyBuilderScope =
+                when (value) {
+                    is GroovyObject -> GroovyBuilderScopeForGroovyObject(value)
+                    else            -> GroovyBuilderScopeForRegularObject(value)
+                }
+    }
+
+    val delegate: Any
+
+    operator fun String.invoke(vararg arguments: Any?): Any?
+
+    operator fun String.invoke(): Any? =
+            invoke(*emptyArray<Any>())
+
+    operator fun <T> String.invoke(vararg arguments: Any?, builder: GroovyBuilderScope.() -> T): Any? =
+            invoke(*arguments, closureFor(builder))
+
+    operator fun <T> String.invoke(builder: GroovyBuilderScope.() -> T): Any? =
+            invoke(closureFor(builder))
+
+    operator fun <T> String.invoke(vararg keywordArguments: Pair<String, Any?>, builder: GroovyBuilderScope.() -> T): Any? =
+            invoke(keywordArguments.toMap(), closureFor(builder))
+
+    operator fun String.invoke(vararg keywordArguments: Pair<String, Any?>): Any? =
+            invoke(keywordArguments.toMap())
+
+    private
+    fun <T> closureFor(builder: GroovyBuilderScope.() -> T): Closure<Any?> =
+            object : Closure<Any?>(this, this) {
+                @Suppress("unused")
+                fun doCall() = delegate.withGroovyBuilder(builder)
+            }
+}
+
+
+private
+class GroovyBuilderScopeForGroovyObject(override val delegate: GroovyObject) : GroovyBuilderScope, GroovyObject by delegate {
+
+    override fun String.invoke(vararg arguments: Any?): Any? =
+            delegate.invokeMethod(this, arguments)
+}
+
+
+private
+class GroovyBuilderScopeForRegularObject(override val delegate: Any) : GroovyBuilderScope {
+
+    private
+    val groovyMetaClass: MetaClass by lazy {
+        getMetaClass(delegate)
+    }
+
+    override fun invokeMethod(name: String, args: Any?): Any? =
+            groovyMetaClass.invokeMethod(delegate, name, args)
+
+    override fun setProperty(propertyName: String, newValue: Any?) =
+            groovyMetaClass.setProperty(delegate, propertyName, newValue)
+
+    override fun getProperty(propertyName: String): Any =
+            groovyMetaClass.getProperty(delegate, propertyName)
+
+    override fun setMetaClass(metaClass: MetaClass?) =
+            throw IllegalStateException()
+
+    override fun getMetaClass(): MetaClass =
+            groovyMetaClass
+
+    override fun String.invoke(vararg arguments: Any?): Any? =
+            groovyMetaClass.invokeMethod(delegate, this, arguments)
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/android/support/MavenUploadHelper.kt b/buildSrc/src/main/kotlin/android/support/MavenUploadHelper.kt
new file mode 100644
index 0000000..1698bf9
--- /dev/null
+++ b/buildSrc/src/main/kotlin/android/support/MavenUploadHelper.kt
@@ -0,0 +1,136 @@
+/*
+ * 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.support
+
+import com.android.build.gradle.LibraryPlugin
+import org.gradle.api.Project
+import org.gradle.api.artifacts.ProjectDependency
+import org.gradle.api.artifacts.maven.MavenDeployer
+import org.gradle.api.tasks.Upload
+
+fun apply(project: Project, extension: SupportLibraryExtension) {
+    project.afterEvaluate {
+        if (extension.publish) {
+            if (extension.mavenGroup == null) {
+                throw Exception("You must specify mavenGroup for " + project.name + " project");
+            }
+            if (extension.mavenVersion == null) {
+                throw Exception("You must specify mavenVersion for " + project.name + " project");
+            }
+            project.group = extension.mavenGroup!!
+            project.version = extension.mavenVersion.toString()
+        }
+    }
+
+    project.apply(mapOf("plugin" to "maven"))
+
+    // Set uploadArchives options.
+    val uploadTask = project.tasks.getByName("uploadArchives") as Upload
+    project.afterEvaluate {
+        if (extension.publish) {
+            uploadTask.repositories.withType(MavenDeployer::class.java) { mavenDeployer ->
+
+                mavenDeployer.getPom().project {
+                    it.withGroovyBuilder {
+                        "name"(extension.name)
+                        "description"(extension.description)
+                        "url"(extension.url)
+                        "inceptionYear"(extension.inceptionYear)
+
+                        "licenses" {
+                            "license" {
+                                "name"("The Apache Software License, Version 2.0")
+                                "url"("http://www.apache.org/licenses/LICENSE-2.0.txt")
+                                "distribution"("repo")
+                            }
+                            for (license in extension.getLicenses()) {
+                                "license" {
+                                    "name"(license.name)
+                                    "url"(license.url)
+                                    "distribution"("repo")
+                                }
+                            }
+                        }
+
+                        "scm" {
+                            "url"("http://source.android.com")
+                            "connection"("scm:git:https://android.googlesource.com/platform/frameworks/support")
+                        }
+
+                        "developers" {
+                            "developer" {
+                                "name"("The Android Open Source Project")
+                            }
+                        }
+                    }
+                }
+
+                // TODO(aurimas): remove this when Gradle bug is fixed.
+                // https://github.com/gradle/gradle/issues/3170
+                uploadTask.doFirst {
+                    val allDeps = HashSet<ProjectDependency>()
+                    collectDependenciesForConfiguration(allDeps, project, "api");
+                    collectDependenciesForConfiguration(allDeps, project, "implementation");
+                    collectDependenciesForConfiguration(allDeps, project, "compile");
+
+                    mavenDeployer.getPom().whenConfigured {
+                        it.dependencies.forEach { dep ->
+                            if (dep == null) {
+                                return@forEach
+                            }
+
+                            val getGroupIdMethod =
+                                    dep::class.java.getDeclaredMethod("getGroupId")
+                            val groupId : String = getGroupIdMethod.invoke(dep) as String
+                            val getArtifactIdMethod =
+                                    dep::class.java.getDeclaredMethod("getArtifactId")
+                            val artifactId : String = getArtifactIdMethod.invoke(dep) as String
+
+                            if (isAndroidProject(groupId, artifactId, allDeps)) {
+                                val setTypeMethod = dep::class.java.getDeclaredMethod("setType",
+                                        java.lang.String::class.java)
+                                setTypeMethod.invoke(dep, "aar")
+                            }
+                        }
+                    }
+                }
+            }
+        } else {
+            uploadTask.enabled = false
+        }
+    }
+}
+
+private fun collectDependenciesForConfiguration(projectDependencies : MutableSet<ProjectDependency>,
+                                                project : Project, name : String) {
+    val config = project.configurations.findByName(name)
+    if (config != null) {
+        config.dependencies.withType(ProjectDependency::class.java).forEach {
+            dep -> projectDependencies.add(dep)
+        }
+    }
+}
+
+private fun isAndroidProject(groupId : String, artifactId : String,
+                             deps : Set<ProjectDependency>) : Boolean {
+    for (dep in deps) {
+        if (dep.group == groupId && dep.name == artifactId) {
+            return dep.getDependencyProject().plugins.hasPlugin(LibraryPlugin::class.java)
+        }
+    }
+    return false
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/android/support/SupportAndroidLibraryPlugin.kt b/buildSrc/src/main/kotlin/android/support/SupportAndroidLibraryPlugin.kt
new file mode 100644
index 0000000..b347415
--- /dev/null
+++ b/buildSrc/src/main/kotlin/android/support/SupportAndroidLibraryPlugin.kt
@@ -0,0 +1,161 @@
+/*
+ * 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.support
+
+import com.android.build.gradle.LibraryExtension
+import com.android.build.gradle.internal.dsl.LintOptions
+import net.ltgt.gradle.errorprone.ErrorProneBasePlugin
+import net.ltgt.gradle.errorprone.ErrorProneToolChain
+import org.gradle.api.JavaVersion
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import java.io.File
+import java.util.*
+
+/**
+ * Support library specific com.android.library plugin that sets common configurations needed for
+ * support library modules.
+ */
+class SupportAndroidLibraryPlugin : Plugin<Project> {
+
+    override fun apply(project: Project) {
+        val supportLibraryExtension = project.extensions.create("supportLibrary",
+                SupportLibraryExtension::class.java, project)
+        apply(project, supportLibraryExtension)
+
+        project.afterEvaluate {
+            val library = project.extensions.findByType(LibraryExtension::class.java)
+                    ?: return@afterEvaluate
+
+            if (supportLibraryExtension.legacySourceLocation) {
+                // We use a non-standard manifest path.
+                library.sourceSets.getByName("main").manifest.srcFile("AndroidManifest.xml")
+
+                // We use a non-standard test directory structure.
+                val androidTest = library.sourceSets.getByName("androidTest")
+                androidTest.setRoot("tests")
+                androidTest.java.srcDir("tests/src")
+                androidTest.res.srcDir("tests/res")
+                androidTest.manifest.srcFile("tests/AndroidManifest.xml")
+            }
+
+            // Java 8 is only fully supported on API 24+ and not all Java 8 features are binary
+            // compatible with API < 24, so use Java 7 for both source AND target.
+            val javaVersion: JavaVersion;
+            if (supportLibraryExtension.java8Library) {
+                if (library.defaultConfig.minSdkVersion.apiLevel < 24) {
+                    throw IllegalArgumentException("Libraries can only support Java 8 if "
+                            + "minSdkVersion is 24 or higher");
+                }
+                javaVersion = JavaVersion.VERSION_1_8
+            } else {
+                javaVersion = JavaVersion.VERSION_1_7
+            }
+
+            library.compileOptions.setSourceCompatibility(javaVersion)
+            library.compileOptions.setTargetCompatibility(javaVersion)
+        }
+
+        VersionFileWriterTask.setUpAndroidLibrary(project)
+
+        project.apply(mapOf("plugin" to "com.android.library"))
+        project.apply(mapOf("plugin" to ErrorProneBasePlugin::class.java))
+
+        val library = project.extensions.findByType(LibraryExtension::class.java)
+                ?: throw Exception("Failed to find Android extension")
+
+        val currentSdk = project.property("currentSdk")
+        when (currentSdk) {
+            is Int -> library.compileSdkVersion(currentSdk)
+            is String -> library.compileSdkVersion(currentSdk)
+        }
+
+        // Update the version meta-data in each Manifest.
+        library.defaultConfig.addManifestPlaceholders(mapOf("target-sdk-version" to currentSdk))
+
+        // Set test runner.
+        library.defaultConfig.testInstrumentationRunner = INSTRUMENTATION_RUNNER
+
+        library.testOptions.unitTests.isReturnDefaultValues = true
+
+        // Use a local debug keystore to avoid build server issues.
+        val key = ((project.rootProject.property("init") as Properties)
+                .getValue("debugKeystore")) as File
+        library.signingConfigs.findByName("debug")?.storeFile = key
+
+        setUpLint(library.lintOptions, File(project.projectDir, "/lint-baseline.xml"))
+
+        if (project.rootProject.property("usingFullSdk") as Boolean) {
+            // Library projects don't run lint by default, so set up dependency.
+            project.tasks.getByName("uploadArchives").dependsOn("lintRelease")
+        }
+
+        SourceJarTaskHelper.setUpAndroidProject(project, library)
+
+        val toolChain = ErrorProneToolChain.create(project)
+        library.buildTypes.create("errorProne")
+        library.libraryVariants.all { libraryVariant ->
+            if (libraryVariant.getBuildType().getName().equals("errorProne")) {
+                @Suppress("DEPRECATION")
+                libraryVariant.getJavaCompile().setToolChain(toolChain);
+
+                @Suppress("DEPRECATION")
+                val compilerArgs = libraryVariant.getJavaCompile().options.compilerArgs
+                compilerArgs += arrayListOf(
+                        "-XDcompilePolicy=simple", // Workaround for b/36098770
+
+                        // Enforce the following checks.
+                        "-Xep:MissingOverride:ERROR",
+                        "-Xep:NarrowingCompoundAssignment:ERROR",
+                        "-Xep:ClassNewInstance:ERROR",
+                        "-Xep:ClassCanBeStatic:ERROR",
+                        "-Xep:SynchronizeOnNonFinalField:ERROR",
+                        "-Xep:OperatorPrecedence:ERROR"
+                )
+            }
+        }
+    }
+
+    companion object {
+        private val INSTRUMENTATION_RUNNER = "android.support.test.runner.AndroidJUnitRunner"
+    }
+}
+
+private fun setUpLint(lintOptions: LintOptions, baseline: File) {
+    // Always lint check NewApi as fatal.
+    lintOptions.isAbortOnError = true
+    lintOptions.isIgnoreWarnings = true
+
+    // Write output directly to the console (and nowhere else).
+    lintOptions.textOutput("stderr")
+    lintOptions.textReport = true
+    lintOptions.htmlReport = false
+
+    // Format output for convenience.
+    lintOptions.isExplainIssues = true
+    lintOptions.isNoLines = false
+    lintOptions.isQuiet = true
+
+    lintOptions.error("NewApi")
+
+    // Set baseline file for all legacy lint warnings.
+    if (System.getenv("GRADLE_PLUGIN_VERSION") != null) {
+        lintOptions.check("NewApi")
+    } else {
+        lintOptions.baseline(baseline)
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/android/support/SupportJavaLibraryPlugin.kt b/buildSrc/src/main/kotlin/android/support/SupportJavaLibraryPlugin.kt
new file mode 100644
index 0000000..8e0714b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/android/support/SupportJavaLibraryPlugin.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.support
+
+import org.gradle.api.JavaVersion
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.plugins.JavaPluginConvention
+
+/**
+ * Support java library specific plugin that sets common configurations needed for
+ * support library modules.
+ */
+class SupportJavaLibraryPlugin : Plugin<Project> {
+
+    override fun apply(project: Project) {
+        val supportLibraryExtension = project.extensions.create("supportLibrary",
+                SupportLibraryExtension::class.java, project)
+        apply(project, supportLibraryExtension)
+
+        project.apply(mapOf("plugin" to "java"))
+        project.afterEvaluate {
+            val convention = project.convention.getPlugin(JavaPluginConvention::class.java)
+            if (supportLibraryExtension.java8Library) {
+                convention.sourceCompatibility = JavaVersion.VERSION_1_8
+                convention.targetCompatibility = JavaVersion.VERSION_1_8
+            } else {
+                convention.sourceCompatibility = JavaVersion.VERSION_1_7
+                convention.targetCompatibility = JavaVersion.VERSION_1_7
+            }
+        }
+
+        SourceJarTaskHelper.setUpJavaProject(project)
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/android/support/SupportLibraryExtension.kt b/buildSrc/src/main/kotlin/android/support/SupportLibraryExtension.kt
new file mode 100644
index 0000000..088e169
--- /dev/null
+++ b/buildSrc/src/main/kotlin/android/support/SupportLibraryExtension.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.support
+
+import groovy.lang.Closure
+import org.gradle.api.Project
+import java.util.ArrayList
+
+/**
+ * Extension for [SupportAndroidLibraryPlugin] and [SupportJavaLibraryPlugin].
+ */
+open class SupportLibraryExtension(val project: Project) {
+    var name: String? = null
+    var mavenVersion: Version? = null
+    var mavenGroup: String? = null
+    var description: String? = null
+    var inceptionYear: String? = null
+    var url = SUPPORT_URL
+    private var licenses: MutableCollection<License> = ArrayList()
+    var java8Library = false
+    var legacySourceLocation = false
+    var publish = false
+
+    fun license(closure: Closure<*>): License {
+        val license = project.configure(License(), closure) as License
+        licenses.add(license)
+        return license
+    }
+
+    fun getLicenses(): Collection<License> {
+        return licenses
+    }
+
+    companion object {
+        @JvmField
+        val ARCHITECTURE_URL = "https://developer.android.com/topic/libraries/architecture/index.html"
+        @JvmField
+        val SUPPORT_URL = "http://developer.android.com/tools/extras/support-library.html"
+    }
+}
+
+class License {
+    var name: String? = null
+    var url: String? = null
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/android/support/checkapi/CheckApiTask.kt b/buildSrc/src/main/kotlin/android/support/checkapi/CheckApiTask.kt
new file mode 100644
index 0000000..8f96b2e
--- /dev/null
+++ b/buildSrc/src/main/kotlin/android/support/checkapi/CheckApiTask.kt
@@ -0,0 +1,251 @@
+/*
+ * 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.support.checkapi
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.TaskAction
+import java.io.*
+import java.security.MessageDigest
+
+
+/** Character that resets console output color. */
+private const val ANSI_RESET = "\u001B[0m"
+
+/** Character that sets console output color to red. */
+private const val ANSI_RED = "\u001B[31m"
+
+/** Character that sets console output color to yellow. */
+private const val ANSI_YELLOW = "\u001B[33m"
+
+private val ERROR_REGEX = Regex("^(.+):(.+): (\\w+) (\\d+): (.+)$")
+
+private fun ByteArray.encodeHex() = fold(StringBuilder(), { builder, byte ->
+    val hexString = Integer.toHexString(byte.toInt() and 0xFF)
+    if (hexString.length < 2) {
+        builder.append("0")
+    }
+    builder.append(hexString)
+}).toString()
+
+private fun getShortHash(src: String): String {
+    val str = MessageDigest.getInstance("SHA-1")
+            .digest(src.toByteArray()).encodeHex()
+    val len = str.length
+    return str.substring(len - 7, len)
+}
+
+/**
+ * Task used to verify changes between two API files.
+ * <p>
+ * This task may be configured to ignore, warn, or fail with a message for a specific set of
+ * Doclava-defined error codes. See {@link com.google.doclava.Errors} for a complete list of
+ * supported error codes.
+ * <p>
+ * Specific failures may be ignored by specifying a list of SHAs in {@link #whitelistErrors}. Each
+ * SHA is unique to a specific API change and is logged to the error output on failure.
+ */
+open class CheckApiTask : DefaultTask() {
+
+    /** API file that represents the existing API surface. */
+    @Optional
+    @InputFile
+    var oldApiFile: File? = null
+
+    /** API file that represents the existing API surface's removals. */
+    @Optional
+    @InputFile
+    var oldRemovedApiFile: File? = null
+
+    /** API file that represents the candidate API surface. */
+    @InputFile
+    var newApiFile: File? = null
+
+    /** API file that represents the candidate API surface's removals. */
+    @Optional
+    @InputFile
+    var newRemovedApiFile: File? = null
+
+    /** Optional file containing a newline-delimited list of error SHAs to ignore. */
+    var whitelistErrorsFile: File? = null
+
+    @Optional
+    @InputFile
+    fun getWhiteListErrorsFileInput(): File? {
+        // Gradle requires non-null InputFiles to exist -- even with Optional -- so work around that
+        // by returning null for this field if the file doesn't exist.
+        if (whitelistErrorsFile?.exists() == true) {
+            return whitelistErrorsFile
+        }
+        return null
+    }
+
+    /**
+     * Optional set of error SHAs to ignore.
+     * <p>
+     * Each error SHA is unique to a specific API change.
+     */
+    @Optional
+    @Input
+    var whitelistErrors = emptySet<String>()
+
+    var detectedWhitelistErrors = mutableSetOf<String>()
+
+    @InputFiles
+    var doclavaClasspath: Collection<File> = emptyList()
+
+    // A dummy output file meant only to tag when this check was last ran.
+    // Without any outputs, Gradle will run this task every time.
+    @Optional
+    private var mOutputFile: File? = null
+
+    @OutputFile
+    fun getOutputFile(): File {
+        return if (mOutputFile != null)
+            mOutputFile!!
+        else File(project.buildDir, "checkApi/${name}-completed")
+    }
+
+    @Optional
+    fun setOutputFile(outputFile: File) {
+        mOutputFile = outputFile
+    }
+
+    /**
+     * List of Doclava error codes to treat as errors.
+     * <p>
+     * See {@link com.google.doclava.Errors} for a complete list of error codes.
+     */
+    @Input
+    var checkApiErrors: Collection<Int> = emptyList()
+
+    /**
+     * List of Doclava error codes to treat as warnings.
+     * <p>
+     * See {@link com.google.doclava.Errors} for a complete list of error codes.
+     */
+    @Input
+    var checkApiWarnings: Collection<Int> = emptyList()
+
+    /**
+     * List of Doclava error codes to ignore.
+     * <p>
+     * See {@link com.google.doclava.Errors} for a complete list of error codes.
+     */
+    @Input
+    var checkApiHidden: Collection<Int> = emptyList()
+
+    /** Message to display on API check failure. */
+    @Input
+    var onFailMessage: String = "Failed."
+
+    init {
+        group = "Verification"
+        description = "Invoke Doclava\'s ApiCheck tool to make sure current.txt is up to date."
+    }
+
+    private fun collectAndVerifyInputs(): Set<File> {
+        if (oldRemovedApiFile != null && newRemovedApiFile != null) {
+            return setOf(oldApiFile!!, newApiFile!!, oldRemovedApiFile!!, newRemovedApiFile!!)
+        } else {
+            return setOf(oldApiFile!!, newApiFile!!)
+        }
+    }
+
+    @TaskAction
+    fun exec() {
+        if (oldApiFile == null) {
+            // Nothing to do.
+            return
+        }
+
+        val apiFiles = collectAndVerifyInputs()
+
+        val errStream = ByteArrayOutputStream()
+
+        // If either of those gets tweaked, then this should be refactored to extend JavaExec.
+        project.javaexec { spec ->
+            spec.apply {
+                // Put Doclava on the classpath so we can get the ApiCheck class.
+                classpath(doclavaClasspath)
+                main = "com.google.doclava.apicheck.ApiCheck"
+
+                minHeapSize = "128m"
+                maxHeapSize = "1024m"
+
+                // add -error LEVEL for every error level we want to fail the build on.
+                checkApiErrors.forEach { args("-error", it) }
+                checkApiWarnings.forEach { args("-warning", it) }
+                checkApiHidden.forEach { args("-hide", it) }
+
+                spec.args(apiFiles.map { it.absolutePath })
+
+                // Redirect error output so that we can whitelist specific errors.
+                errorOutput = errStream
+                // We will be handling failures ourselves with a custom message.
+                setIgnoreExitValue(true)
+            }
+        }
+
+
+        // Load the whitelist file, if present.
+        val whitelistFile = whitelistErrorsFile
+        if (whitelistFile?.exists() == true) {
+            whitelistErrors += whitelistFile.readLines()
+        }
+
+        // Parse the error output.
+        val unparsedErrors = mutableSetOf<String>()
+        val detectedErrors = mutableSetOf<List<String>>()
+        val parsedErrors = mutableSetOf<List<String>>()
+        ByteArrayInputStream(errStream.toByteArray()).bufferedReader().lines().forEach {
+            val match = ERROR_REGEX.matchEntire(it)
+
+            if (match == null) {
+                unparsedErrors.add(it)
+            } else if (match.groups[3]?.value == "error") {
+                val hash = getShortHash(match.groups[5]?.value!!)
+                val error = match.groupValues.subList(1, match.groupValues.size) + listOf(hash)
+                if (hash in whitelistErrors) {
+                    detectedErrors.add(error)
+                    detectedWhitelistErrors.add(error[5])
+                } else {
+                    parsedErrors.add(error)
+                }
+            }
+        }
+
+        unparsedErrors.forEach { error -> logger.error("$ANSI_RED$error$ANSI_RESET") }
+        parsedErrors.forEach { logger.error("$ANSI_RED${it[5]}$ANSI_RESET ${it[4]}") }
+        detectedErrors.forEach { logger.warn("$ANSI_YELLOW${it[5]}$ANSI_RESET ${it[4]}") }
+
+        if (unparsedErrors.isNotEmpty() || parsedErrors.isNotEmpty()) {
+            throw GradleException(onFailMessage)
+        }
+
+        // Just create a dummy file upon completion. Without any outputs, Gradle will run this task
+        // every time.
+        val outputFile = getOutputFile()
+        outputFile.parentFile.mkdirs()
+        outputFile.createNewFile()
+    }
+}
\ No newline at end of file
diff --git a/car/res/drawable-hdpi/ic_list_view_disable.png b/car/res/drawable-hdpi/ic_list_view_disable.png
deleted file mode 100644
index e82a74f..0000000
--- a/car/res/drawable-hdpi/ic_list_view_disable.png
+++ /dev/null
Binary files differ
diff --git a/car/res/drawable-mdpi/ic_list_view_disable.png b/car/res/drawable-mdpi/ic_list_view_disable.png
deleted file mode 100644
index 9887c8e..0000000
--- a/car/res/drawable-mdpi/ic_list_view_disable.png
+++ /dev/null
Binary files differ
diff --git a/car/res/drawable-xhdpi/ic_list_view_disable.png b/car/res/drawable-xhdpi/ic_list_view_disable.png
deleted file mode 100644
index 32edc30..0000000
--- a/car/res/drawable-xhdpi/ic_list_view_disable.png
+++ /dev/null
Binary files differ
diff --git a/car/res/drawable-xxhdpi/ic_list_view_disable.png b/car/res/drawable-xxhdpi/ic_list_view_disable.png
deleted file mode 100644
index 1f61690..0000000
--- a/car/res/drawable-xxhdpi/ic_list_view_disable.png
+++ /dev/null
Binary files differ
diff --git a/car/res/drawable/ic_list_view_disable.xml b/car/res/drawable/ic_list_view_disable.xml
new file mode 100644
index 0000000..8649423
--- /dev/null
+++ b/car/res/drawable/ic_list_view_disable.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="176dp"
+        android:height="176dp"
+        android:viewportWidth="176.0"
+        android:viewportHeight="176.0">
+    <path
+        android:pathData="M88.99,55.55l15.71,15.71l46.13,0l0,-15.71z"
+        android:fillColor="#212121"/>
+    <path
+        android:pathData="M25.19,119.06h66.5v15.71h-66.5z"
+        android:fillColor="#212121"/>
+    <path
+        android:pathData="M114.58,103.35l-15.71,-15.71l-0.12,0l-16.38,-16.38l0.12,0l-15.71,-15.71l-0.12,0l-30.29,-30.29l-11.11,11.11l19.19,19.18l-19.28,0l0,15.71l34.98,0l16.39,16.38l-51.37,0l0,15.71l67.08,0l47.38,47.39l11.11,-11.11l-36.28,-36.28z"
+        android:fillColor="#212121"/>
+    <path
+        android:pathData="M136.79,103.35l14.04,0l0,-15.71l-29.74,0z"
+        android:fillColor="#212121"/>
+</vector>
diff --git a/car/res/values-h1752dp/dimens.xml b/car/res/values-h1752dp/dimens.xml
index 93aa85f..feb8631 100644
--- a/car/res/values-h1752dp/dimens.xml
+++ b/car/res/values-h1752dp/dimens.xml
@@ -34,5 +34,6 @@
     <dimen name="car_secondary_icon_size">36dp</dimen>
 
     <!-- Line heights -->
+    <dimen name="car_single_line_list_item_height">128dp</dimen>
     <dimen name="car_double_line_list_item_height">128dp</dimen>
 </resources>
diff --git a/car/res/values-h684dp/dimens.xml b/car/res/values-h684dp/dimens.xml
index 72b04f2..a072681 100644
--- a/car/res/values-h684dp/dimens.xml
+++ b/car/res/values-h684dp/dimens.xml
@@ -23,5 +23,6 @@
     <dimen name="car_drawer_list_item_end_icon_size">56dp</dimen>
 
     <!-- Line heights -->
+    <dimen name="car_single_line_list_item_height">116dp</dimen>
     <dimen name="car_double_line_list_item_height">116dp</dimen>
 </resources>
diff --git a/car/res/values-w1280dp/dimens.xml b/car/res/values-w1280dp/dimens.xml
index ea46dcf..9837355 100644
--- a/car/res/values-w1280dp/dimens.xml
+++ b/car/res/values-w1280dp/dimens.xml
@@ -20,8 +20,11 @@
     <dimen name="car_keyline_1">32dp</dimen>
     <dimen name="car_keyline_2">108dp</dimen>
     <dimen name="car_keyline_3">128dp</dimen>
-    <dimen name="car_keyline_4">168dp</dimen>
+    <dimen name="car_keyline_4">182dp</dimen>
     <dimen name="car_keyline_1_neg">32dp</dimen>
     <dimen name="car_keyline_2_neg">108dp</dimen>
     <dimen name="car_keyline_3_neg">128dp</dimen>
+
+    <!-- Margin -->
+    <dimen name="car_margin">148dp</dimen>
 </resources>
diff --git a/car/res/values-w1920dp/dimens.xml b/car/res/values-w1920dp/dimens.xml
index 9914613..52962a1 100644
--- a/car/res/values-w1920dp/dimens.xml
+++ b/car/res/values-w1920dp/dimens.xml
@@ -14,5 +14,8 @@
 limitations under the License.
 -->
 <resources>
-    <dimen name="car_keyline_4">184dp</dimen>
+    <dimen name="car_keyline_3">152dp</dimen>
+
+    <!-- Margin -->
+    <dimen name="car_margin">192dp</dimen>
 </resources>
diff --git a/car/res/values-w690dp/dimens.xml b/car/res/values-w690dp/dimens.xml
index edf6c59..f797955 100644
--- a/car/res/values-w690dp/dimens.xml
+++ b/car/res/values-w690dp/dimens.xml
@@ -21,4 +21,7 @@
     <dimen name="car_keyline_1_neg">-24dp</dimen>
     <dimen name="car_keyline_2_neg">-96dp</dimen>
     <dimen name="car_keyline_3_neg">-112dp</dimen>
+
+    <!-- Margin -->
+    <dimen name="car_margin">112dp</dimen>
 </resources>
diff --git a/car/res/values-w930dp/dimens.xml b/car/res/values-w930dp/dimens.xml
new file mode 100644
index 0000000..481480e
--- /dev/null
+++ b/car/res/values-w930dp/dimens.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+    <dimen name="car_keyline_1">32dp</dimen>
+    <dimen name="car_keyline_2">108dp</dimen>
+    <dimen name="car_keyline_3">128dp</dimen>
+    <dimen name="car_keyline_4">168dp</dimen>
+    <dimen name="car_keyline_1_neg">-32dp</dimen>
+    <dimen name="car_keyline_2_neg">-108dp</dimen>
+    <dimen name="car_keyline_3_neg">-128dp</dimen>
+
+    <!-- Margin -->
+    <dimen name="car_margin">112dp</dimen>
+</resources>
diff --git a/car/res/values/dimens.xml b/car/res/values/dimens.xml
index c42437d..1ffabd5 100644
--- a/car/res/values/dimens.xml
+++ b/car/res/values/dimens.xml
@@ -24,7 +24,6 @@
     <dimen name="car_keyline_3_neg">-152dp</dimen>
 
     <!-- Type Sizings -->
-    <dimen name="car_title_size">26sp</dimen>
     <dimen name="car_title2_size">32sp</dimen>
     <dimen name="car_headline1_size">45sp</dimen>
     <dimen name="car_headline2_size">36sp</dimen>
@@ -44,14 +43,14 @@
     <dimen name="car_padding_4">28dp</dimen>
     <dimen name="car_padding_5">32dp</dimen>
 
-    <!-- Radii -->
+    <!-- Radius -->
     <dimen name="car_radius_1">4dp</dimen>
     <dimen name="car_radius_2">8dp</dimen>
     <dimen name="car_radius_3">16dp</dimen>
     <dimen name="car_radius_5">100dp</dimen>
 
     <!-- Margin -->
-    <dimen name="car_margin">112dp</dimen>
+    <dimen name="car_margin">20dp</dimen>
 
     <!-- Car Component Dimensions -->
     <!-- Application Bar Height -->
diff --git a/car/src/main/java/android/support/car/widget/PagedListView.java b/car/src/main/java/android/support/car/widget/PagedListView.java
index 4695c45..67a6247 100644
--- a/car/src/main/java/android/support/car/widget/PagedListView.java
+++ b/car/src/main/java/android/support/car/widget/PagedListView.java
@@ -286,38 +286,6 @@
         return mLayoutManager.getPosition(v);
     }
 
-    private void scroll(int direction) {
-        View focusedView = mRecyclerView.getFocusedChild();
-        if (focusedView != null) {
-            int position = mLayoutManager.getPosition(focusedView);
-            int newPosition =
-                    Math.max(Math.min(position + direction, mLayoutManager.getItemCount() - 1), 0);
-            if (newPosition != position) {
-                // newPosition/position are adapter positions.
-                // Convert to layout position by subtracting adapter position of view at layout
-                // position 0.
-                View childAt = mRecyclerView.getChildAt(
-                        newPosition - mLayoutManager.getPosition(mLayoutManager.getChildAt(0)));
-                if (childAt != null) {
-                    childAt.requestFocus();
-                }
-            }
-        }
-    }
-
-    private boolean canScroll(int direction) {
-        View focusedView = mRecyclerView.getFocusedChild();
-        if (focusedView != null) {
-            int position = mLayoutManager.getPosition(focusedView);
-            int newPosition =
-                    Math.max(Math.min(position + direction, mLayoutManager.getItemCount() - 1), 0);
-            if (newPosition != position) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     @NonNull
     public CarRecyclerView getRecyclerView() {
         return mRecyclerView;
diff --git a/car/tests/src/android/support/car/widget/PagedListViewSavedStateTest.java b/car/tests/src/android/support/car/widget/PagedListViewSavedStateTest.java
index fba53e2..9b871b3 100644
--- a/car/tests/src/android/support/car/widget/PagedListViewSavedStateTest.java
+++ b/car/tests/src/android/support/car/widget/PagedListViewSavedStateTest.java
@@ -30,6 +30,7 @@
 import android.support.test.espresso.IdlingRegistry;
 import android.support.test.espresso.IdlingResource;
 import android.support.test.filters.SmallTest;
+import android.support.test.filters.Suppress;
 import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v7.widget.RecyclerView;
@@ -115,6 +116,7 @@
         }
     }
 
+    @Suppress
     @Test
     public void testPagePositionRememberedOnRotation() {
         if (!isAutoDevice()) {
diff --git a/car/tests/src/android/support/car/widget/PagedListViewTest.java b/car/tests/src/android/support/car/widget/PagedListViewTest.java
index 71a975c..0d07fbd 100644
--- a/car/tests/src/android/support/car/widget/PagedListViewTest.java
+++ b/car/tests/src/android/support/car/widget/PagedListViewTest.java
@@ -214,6 +214,7 @@
         onView(withId(R.id.page_down)).check(matches(not(isEnabled())));
     }
 
+    @Suppress
     @Test
     public void resetMaxPagesToDefaultUnlimitedExtendsList() throws Throwable {
         if (!isAutoDevice()) {
@@ -297,6 +298,7 @@
         assertThat(mPagedListView.getFirstFullyVisibleChildPosition(), is(equalTo(topPosition)));
     }
 
+    @Suppress
     @Test
     public void setItemSpacing() throws Throwable {
         final int itemCount = 3;
diff --git a/compat/src/main/java/android/support/v4/accessibilityservice/package.html b/compat/src/main/java/android/support/v4/accessibilityservice/package.html
deleted file mode 100755
index 3d017b0..0000000
--- a/compat/src/main/java/android/support/v4/accessibilityservice/package.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<body>
-
-Support android.accessibilityservice classes to assist with development of applications for
-android API level 4 or later.
-
-</body>
diff --git a/compat/src/main/java/android/support/v4/app/package.html b/compat/src/main/java/android/support/v4/app/package.html
deleted file mode 100755
index 02d1b79..0000000
--- a/compat/src/main/java/android/support/v4/app/package.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<body>
-
-Support android.app classes to assist with development of applications for
-android API level 4 or later.  The main features here are backwards-compatible
-versions of {@link android.support.v4.app.FragmentManager} and
-{@link android.support.v4.app.LoaderManager}.
-
-</body>
diff --git a/compat/src/main/java/android/support/v4/content/package.html b/compat/src/main/java/android/support/v4/content/package.html
deleted file mode 100755
index 33bf4b5..0000000
--- a/compat/src/main/java/android/support/v4/content/package.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<body>
-
-Support android.content classes to assist with development of applications for
-android API level 4 or later.  The main features here are
-{@link android.support.v4.content.Loader} and related classes and
-{@link android.support.v4.content.LocalBroadcastManager} to
-provide a cleaner implementation of broadcasts that don't need to go outside
-of an app.
-
-</body>
diff --git a/compat/src/main/java/android/support/v4/content/pm/package.html b/compat/src/main/java/android/support/v4/content/pm/package.html
deleted file mode 100755
index da850bd..0000000
--- a/compat/src/main/java/android/support/v4/content/pm/package.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<body>
-
-Support android.content.pm classes to assist with development of applications for
-android API level 4 or later.
-
-</body>
diff --git a/compat/src/main/java/android/support/v4/database/package.html b/compat/src/main/java/android/support/v4/database/package.html
deleted file mode 100755
index 25ac59a..0000000
--- a/compat/src/main/java/android/support/v4/database/package.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<body>
-
-Support android.database classes to assist with development of applications for
-android API level 4 or later.
-
-</body>
diff --git a/compat/src/main/java/android/support/v4/os/package.html b/compat/src/main/java/android/support/v4/os/package.html
deleted file mode 100755
index 929c967..0000000
--- a/compat/src/main/java/android/support/v4/os/package.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<body>
-
-Support android.os classes to assist with development of applications for
-android API level 4 or later.
-
-</body>
diff --git a/compat/src/main/java/android/support/v4/util/package.html b/compat/src/main/java/android/support/v4/util/package.html
deleted file mode 100644
index afde9b7..0000000
--- a/compat/src/main/java/android/support/v4/util/package.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<body>
-
-Support android.util classes to assist with development of applications for
-android API level 4 or later.
-
-</body>
diff --git a/compat/src/main/java/android/support/v4/view/NestedScrollingParent2.java b/compat/src/main/java/android/support/v4/view/NestedScrollingParent2.java
index db41c46..2ab463e 100644
--- a/compat/src/main/java/android/support/v4/view/NestedScrollingParent2.java
+++ b/compat/src/main/java/android/support/v4/view/NestedScrollingParent2.java
@@ -18,7 +18,6 @@
 package android.support.v4.view;
 
 import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
 import android.support.v4.view.ViewCompat.NestedScrollType;
 import android.support.v4.view.ViewCompat.ScrollAxis;
 import android.view.MotionEvent;
@@ -144,7 +143,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
      */
-    void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed,
+    void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
             @NestedScrollType int type);
 
 }
diff --git a/compat/src/main/java/android/support/v4/view/accessibility/package.html b/compat/src/main/java/android/support/v4/view/accessibility/package.html
deleted file mode 100755
index 57b084f..0000000
--- a/compat/src/main/java/android/support/v4/view/accessibility/package.html
+++ /dev/null
@@ -1,5 +0,0 @@
-<body>
-
-Support classes to access some of the android.view.accessibility package features introduced after API level 4 in a backwards compatible fashion.
-
-</body>
diff --git a/compat/src/main/java/android/support/v4/view/package.html b/compat/src/main/java/android/support/v4/view/package.html
deleted file mode 100755
index d80ef70..0000000
--- a/compat/src/main/java/android/support/v4/view/package.html
+++ /dev/null
@@ -1,11 +0,0 @@
-<body>
-
-Support android.util classes to assist with development of applications for
-android API level 4 or later.  The main features here are a variety of classes
-for handling backwards compatibility with views (for example
-{@link android.support.v4.view.MotionEventCompat} allows retrieving multi-touch
-data if available), and a new
-{@link android.support.v4.view.ViewPager} widget (which at some point should be moved over
-to the widget package).
-
-</body>
diff --git a/compat/src/main/java/android/support/v4/widget/package.html b/compat/src/main/java/android/support/v4/widget/package.html
deleted file mode 100755
index e2c636d..0000000
--- a/compat/src/main/java/android/support/v4/widget/package.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<body>
-
-Support android.widget classes to assist with development of applications for
-android API level 4 or later.  This includes a complete modern implementation
-of {@link android.support.v4.widget.CursorAdapter} and related classes, which
-is needed for use with {@link android.support.v4.content.CursorLoader}.
-
-</body>
diff --git a/core-ui/Android.mk b/core-ui/Android.mk
index f6099b9..1473381 100644
--- a/core-ui/Android.mk
+++ b/core-ui/Android.mk
@@ -36,4 +36,5 @@
 LOCAL_JAR_EXCLUDE_FILES := none
 LOCAL_JAVA_LANGUAGE_VERSION := 1.7
 LOCAL_AAPT_FLAGS := --add-javadoc-annotation doconly
+LOCAL_EXPORT_PROGUARD_FLAG_FILES := proguard-rules.pro
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/core-ui/src/main/java/android/support/design/widget/CoordinatorLayout.java b/core-ui/src/main/java/android/support/design/widget/CoordinatorLayout.java
index 477a8d6..c45810e 100644
--- a/core-ui/src/main/java/android/support/design/widget/CoordinatorLayout.java
+++ b/core-ui/src/main/java/android/support/design/widget/CoordinatorLayout.java
@@ -56,6 +56,8 @@
 import android.support.v4.view.ViewCompat.NestedScrollType;
 import android.support.v4.view.ViewCompat.ScrollAxis;
 import android.support.v4.view.WindowInsetsCompat;
+import android.support.v4.widget.DirectedAcyclicGraph;
+import android.support.v4.widget.ViewGroupUtils;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
diff --git a/core-ui/src/main/java/android/support/v4/app/package.html b/core-ui/src/main/java/android/support/v4/app/package.html
deleted file mode 100755
index 02d1b79..0000000
--- a/core-ui/src/main/java/android/support/v4/app/package.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<body>
-
-Support android.app classes to assist with development of applications for
-android API level 4 or later.  The main features here are backwards-compatible
-versions of {@link android.support.v4.app.FragmentManager} and
-{@link android.support.v4.app.LoaderManager}.
-
-</body>
diff --git a/core-ui/src/main/java/android/support/v4/view/package.html b/core-ui/src/main/java/android/support/v4/view/package.html
deleted file mode 100755
index d80ef70..0000000
--- a/core-ui/src/main/java/android/support/v4/view/package.html
+++ /dev/null
@@ -1,11 +0,0 @@
-<body>
-
-Support android.util classes to assist with development of applications for
-android API level 4 or later.  The main features here are a variety of classes
-for handling backwards compatibility with views (for example
-{@link android.support.v4.view.MotionEventCompat} allows retrieving multi-touch
-data if available), and a new
-{@link android.support.v4.view.ViewPager} widget (which at some point should be moved over
-to the widget package).
-
-</body>
diff --git a/core-ui/src/main/java/android/support/design/widget/DirectedAcyclicGraph.java b/core-ui/src/main/java/android/support/v4/widget/DirectedAcyclicGraph.java
similarity index 85%
rename from core-ui/src/main/java/android/support/design/widget/DirectedAcyclicGraph.java
rename to core-ui/src/main/java/android/support/v4/widget/DirectedAcyclicGraph.java
index 85a32cd..83c62c0 100644
--- a/core-ui/src/main/java/android/support/design/widget/DirectedAcyclicGraph.java
+++ b/core-ui/src/main/java/android/support/v4/widget/DirectedAcyclicGraph.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * 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.
@@ -14,10 +14,13 @@
  * limitations under the License.
  */
 
-package android.support.design.widget;
+package android.support.v4.widget;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
 
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
 import android.support.v4.util.Pools;
 import android.support.v4.util.SimpleArrayMap;
 
@@ -27,8 +30,13 @@
 
 /**
  * A class which represents a simple directed acyclic graph.
+ *
+ * @param <T> Class for the data objects of this graph.
+ *
+ * @hide
  */
-final class DirectedAcyclicGraph<T> {
+@RestrictTo(LIBRARY)
+public final class DirectedAcyclicGraph<T> {
     private final Pools.Pool<ArrayList<T>> mListPool = new Pools.SimplePool<>(10);
     private final SimpleArrayMap<T, ArrayList<T>> mGraph = new SimpleArrayMap<>();
 
@@ -42,7 +50,7 @@
      *
      * @param node the node to add
      */
-    void addNode(@NonNull T node) {
+    public void addNode(@NonNull T node) {
         if (!mGraph.containsKey(node)) {
             mGraph.put(node, null);
         }
@@ -51,7 +59,7 @@
     /**
      * Returns true if the node is already present in the graph, false otherwise.
      */
-    boolean contains(@NonNull T node) {
+    public boolean contains(@NonNull T node) {
         return mGraph.containsKey(node);
     }
 
@@ -64,7 +72,7 @@
      * @param node the parent node
      * @param incomingEdge the node which has is an incoming edge to {@code node}
      */
-    void addEdge(@NonNull T node, @NonNull T incomingEdge) {
+    public void addEdge(@NonNull T node, @NonNull T incomingEdge) {
         if (!mGraph.containsKey(node) || !mGraph.containsKey(incomingEdge)) {
             throw new IllegalArgumentException("All nodes must be present in the graph before"
                     + " being added as an edge");
@@ -86,7 +94,7 @@
      * @return a list containing any incoming edges, or null if there are none.
      */
     @Nullable
-    List getIncomingEdges(@NonNull T node) {
+    public List getIncomingEdges(@NonNull T node) {
         return mGraph.get(node);
     }
 
@@ -97,7 +105,7 @@
      * @return a list containing any outgoing edges, or null if there are none.
      */
     @Nullable
-    List<T> getOutgoingEdges(@NonNull T node) {
+    public List<T> getOutgoingEdges(@NonNull T node) {
         ArrayList<T> result = null;
         for (int i = 0, size = mGraph.size(); i < size; i++) {
             ArrayList<T> edges = mGraph.valueAt(i);
@@ -111,7 +119,14 @@
         return result;
     }
 
-    boolean hasOutgoingEdges(@NonNull T node) {
+    /**
+     * Checks whether we have any outgoing edges for the given node (i.e. nodes which have
+     * an incoming edge from the given node).
+     *
+     * @return <code>true</code> if the node has any outgoing edges, <code>false</code>
+     * otherwise.
+     */
+    public boolean hasOutgoingEdges(@NonNull T node) {
         for (int i = 0, size = mGraph.size(); i < size; i++) {
             ArrayList<T> edges = mGraph.valueAt(i);
             if (edges != null && edges.contains(node)) {
@@ -124,7 +139,7 @@
     /**
      * Clears the internal graph, and releases resources to pools.
      */
-    void clear() {
+    public void clear() {
         for (int i = 0, size = mGraph.size(); i < size; i++) {
             ArrayList<T> edges = mGraph.valueAt(i);
             if (edges != null) {
@@ -143,7 +158,7 @@
      * of the graph. The node at the end of the list will have no dependencies on other nodes.</p>
      */
     @NonNull
-    ArrayList<T> getSortedList() {
+    public ArrayList<T> getSortedList() {
         mSortResult.clear();
         mSortTmpMarked.clear();
 
@@ -198,4 +213,4 @@
         list.clear();
         mListPool.release(list);
     }
-}
\ No newline at end of file
+}
diff --git a/core-ui/src/main/java/android/support/design/widget/ViewGroupUtils.java b/core-ui/src/main/java/android/support/v4/widget/ViewGroupUtils.java
similarity index 89%
rename from core-ui/src/main/java/android/support/design/widget/ViewGroupUtils.java
rename to core-ui/src/main/java/android/support/v4/widget/ViewGroupUtils.java
index 5d8b5c7..986b4c2 100644
--- a/core-ui/src/main/java/android/support/design/widget/ViewGroupUtils.java
+++ b/core-ui/src/main/java/android/support/v4/widget/ViewGroupUtils.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * 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.
@@ -14,16 +14,23 @@
  * limitations under the License.
  */
 
-package android.support.design.widget;
+package android.support.v4.widget;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
 
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.support.annotation.RestrictTo;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewParent;
 
-class ViewGroupUtils {
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public class ViewGroupUtils {
     private static final ThreadLocal<Matrix> sMatrix = new ThreadLocal<>();
     private static final ThreadLocal<RectF> sRectF = new ThreadLocal<>();
 
@@ -65,7 +72,7 @@
      * @param descendant descendant view to reference
      * @param out rect to set to the bounds of the descendant view
      */
-    static void getDescendantRect(ViewGroup parent, View descendant, Rect out) {
+    public static void getDescendantRect(ViewGroup parent, View descendant, Rect out) {
         out.set(0, 0, descendant.getWidth(), descendant.getHeight());
         offsetDescendantRect(parent, descendant, out);
     }
diff --git a/core-ui/src/main/java/android/support/v4/widget/package.html b/core-ui/src/main/java/android/support/v4/widget/package.html
deleted file mode 100755
index e2c636d..0000000
--- a/core-ui/src/main/java/android/support/v4/widget/package.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<body>
-
-Support android.widget classes to assist with development of applications for
-android API level 4 or later.  This includes a complete modern implementation
-of {@link android.support.v4.widget.CursorAdapter} and related classes, which
-is needed for use with {@link android.support.v4.content.CursorLoader}.
-
-</body>
diff --git a/core-ui/src/test/NO_DOCS b/core-ui/src/test/NO_DOCS
deleted file mode 100644
index 092a39c..0000000
--- a/core-ui/src/test/NO_DOCS
+++ /dev/null
@@ -1,17 +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.
-
-Having this file, named NO_DOCS, in a directory will prevent
-Android javadocs from being generated for java files under
-the directory. This is especially useful for test projects.
diff --git a/core-ui/src/test/java/android/support/design/widget/DirectedAcyclicGraphTest.java b/core-ui/tests/java/android/support/v4/widget/DirectedAcyclicGraphTest.java
similarity index 98%
rename from core-ui/src/test/java/android/support/design/widget/DirectedAcyclicGraphTest.java
rename to core-ui/tests/java/android/support/v4/widget/DirectedAcyclicGraphTest.java
index ec7687d..8355fcc 100644
--- a/core-ui/src/test/java/android/support/design/widget/DirectedAcyclicGraphTest.java
+++ b/core-ui/tests/java/android/support/v4/widget/DirectedAcyclicGraphTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.support.design.widget;
+package android.support.v4.widget;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
diff --git a/core-utils/java/android/support/v4/app/package.html b/core-utils/java/android/support/v4/app/package.html
deleted file mode 100755
index 02d1b79..0000000
--- a/core-utils/java/android/support/v4/app/package.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<body>
-
-Support android.app classes to assist with development of applications for
-android API level 4 or later.  The main features here are backwards-compatible
-versions of {@link android.support.v4.app.FragmentManager} and
-{@link android.support.v4.app.LoaderManager}.
-
-</body>
diff --git a/core-utils/java/android/support/v4/content/package.html b/core-utils/java/android/support/v4/content/package.html
deleted file mode 100755
index 33bf4b5..0000000
--- a/core-utils/java/android/support/v4/content/package.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<body>
-
-Support android.content classes to assist with development of applications for
-android API level 4 or later.  The main features here are
-{@link android.support.v4.content.Loader} and related classes and
-{@link android.support.v4.content.LocalBroadcastManager} to
-provide a cleaner implementation of broadcasts that don't need to go outside
-of an app.
-
-</body>
diff --git a/design/Android.mk b/design/Android.mk
index 26017b4..ceb87d9 100644
--- a/design/Android.mk
+++ b/design/Android.mk
@@ -49,4 +49,5 @@
 LOCAL_AAPT_FLAGS := \
     --no-version-vectors \
     --add-javadoc-annotation doconly
+LOCAL_EXPORT_PROGUARD_FLAG_FILES := proguard-rules.pro
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/design/src/android/support/design/widget/BottomSheetBehavior.java b/design/src/android/support/design/widget/BottomSheetBehavior.java
index aaa9b80..00ce8f9 100644
--- a/design/src/android/support/design/widget/BottomSheetBehavior.java
+++ b/design/src/android/support/design/widget/BottomSheetBehavior.java
@@ -559,7 +559,7 @@
      * Gets the current state of the bottom sheet.
      *
      * @return One of {@link #STATE_EXPANDED}, {@link #STATE_COLLAPSED}, {@link #STATE_DRAGGING},
-     * and {@link #STATE_SETTLING}.
+     * {@link #STATE_SETTLING}, and {@link #STATE_HIDDEN}.
      */
     @State
     public final int getState() {
diff --git a/design/src/android/support/design/widget/CollapsingToolbarLayout.java b/design/src/android/support/design/widget/CollapsingToolbarLayout.java
index 0051de9..8c9b7d4 100644
--- a/design/src/android/support/design/widget/CollapsingToolbarLayout.java
+++ b/design/src/android/support/design/widget/CollapsingToolbarLayout.java
@@ -44,6 +44,7 @@
 import android.support.v4.view.GravityCompat;
 import android.support.v4.view.ViewCompat;
 import android.support.v4.view.WindowInsetsCompat;
+import android.support.v4.widget.ViewGroupUtils;
 import android.support.v7.widget.Toolbar;
 import android.text.TextUtils;
 import android.util.AttributeSet;
diff --git a/design/src/android/support/design/widget/FloatingActionButton.java b/design/src/android/support/design/widget/FloatingActionButton.java
index 79a4363..f37b379 100644
--- a/design/src/android/support/design/widget/FloatingActionButton.java
+++ b/design/src/android/support/design/widget/FloatingActionButton.java
@@ -36,6 +36,7 @@
 import android.support.design.R;
 import android.support.design.widget.FloatingActionButtonImpl.InternalVisibilityChangedListener;
 import android.support.v4.view.ViewCompat;
+import android.support.v4.widget.ViewGroupUtils;
 import android.support.v7.widget.AppCompatImageHelper;
 import android.util.AttributeSet;
 import android.util.Log;
diff --git a/design/src/android/support/design/widget/TextInputEditText.java b/design/src/android/support/design/widget/TextInputEditText.java
index 7235ec2..ee6c32c 100644
--- a/design/src/android/support/design/widget/TextInputEditText.java
+++ b/design/src/android/support/design/widget/TextInputEditText.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.support.v7.widget.AppCompatEditText;
+import android.support.v7.widget.WithHint;
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.ViewParent;
@@ -48,12 +49,12 @@
     public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
         final InputConnection ic = super.onCreateInputConnection(outAttrs);
         if (ic != null && outAttrs.hintText == null) {
-            // If we don't have a hint and our parent is a TextInputLayout, use it's hint for the
+            // If we don't have a hint and our parent implements WithHint, use its hint for the
             // EditorInfo. This allows us to display a hint in 'extract mode'.
             ViewParent parent = getParent();
             while (parent instanceof View) {
-                if (parent instanceof TextInputLayout) {
-                    outAttrs.hintText = ((TextInputLayout) parent).getHint();
+                if (parent instanceof WithHint) {
+                    outAttrs.hintText = ((WithHint) parent).getHint();
                     break;
                 }
                 parent = parent.getParent();
diff --git a/design/src/android/support/design/widget/TextInputLayout.java b/design/src/android/support/design/widget/TextInputLayout.java
index c9e8010..0540678 100644
--- a/design/src/android/support/design/widget/TextInputLayout.java
+++ b/design/src/android/support/design/widget/TextInputLayout.java
@@ -49,10 +49,12 @@
 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
 import android.support.v4.widget.Space;
 import android.support.v4.widget.TextViewCompat;
+import android.support.v4.widget.ViewGroupUtils;
 import android.support.v7.content.res.AppCompatResources;
 import android.support.v7.widget.AppCompatDrawableManager;
 import android.support.v7.widget.AppCompatTextView;
 import android.support.v7.widget.TintTypedArray;
+import android.support.v7.widget.WithHint;
 import android.text.Editable;
 import android.text.TextUtils;
 import android.text.TextWatcher;
@@ -113,7 +115,7 @@
  * may not return the TextInputLayout itself, but rather an intermediate View. If you need
  * to access a View directly, set an {@code android:id} and use {@link View#findViewById(int)}.
  */
-public class TextInputLayout extends LinearLayout {
+public class TextInputLayout extends LinearLayout implements WithHint {
 
     private static final int ANIMATION_DURATION = 200;
     private static final int INVALID_MAX_LENGTH = -1;
@@ -497,6 +499,7 @@
      *
      * @attr ref android.support.design.R.styleable#TextInputLayout_android_hint
      */
+    @Override
     @Nullable
     public CharSequence getHint() {
         return mHintEnabled ? mHint : null;
diff --git a/fragment/src/main/java/android/support/v4/app/package.html b/fragment/src/main/java/android/support/v4/app/package.html
deleted file mode 100755
index 02d1b79..0000000
--- a/fragment/src/main/java/android/support/v4/app/package.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<body>
-
-Support android.app classes to assist with development of applications for
-android API level 4 or later.  The main features here are backwards-compatible
-versions of {@link android.support.v4.app.FragmentManager} and
-{@link android.support.v4.app.LoaderManager}.
-
-</body>
diff --git a/graphics/drawable/static/src/main/java/android/support/graphics/drawable/VectorDrawableCompat.java b/graphics/drawable/static/src/main/java/android/support/graphics/drawable/VectorDrawableCompat.java
index 2c7ae41..a34fe2b 100644
--- a/graphics/drawable/static/src/main/java/android/support/graphics/drawable/VectorDrawableCompat.java
+++ b/graphics/drawable/static/src/main/java/android/support/graphics/drawable/VectorDrawableCompat.java
@@ -173,6 +173,10 @@
  * <dd>Sets the lineJoin for a stroked path: miter,round,bevel. Default is miter.</dd>
  * <dt><code>android:strokeMiterLimit</code></dt>
  * <dd>Sets the Miter limit for a stroked path. Default is 4.</dd>
+ * <dt><code>android:fillType</code></dt>
+ * <dd>Sets the fillType for a path. The types can be either "evenOdd" or "nonZero". They behave the
+ * same as SVG's "fill-rule" properties. Default is nonZero. For more details, see
+ * <a href="https://www.w3.org/TR/SVG/painting.html#FillRuleProperty">FillRuleProperty</a></dd>
  * </dl></dd>
  * </dl>
  *
diff --git a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/LifecycleProcessor.kt b/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/LifecycleProcessor.kt
index 2edb234..5355615 100644
--- a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/LifecycleProcessor.kt
+++ b/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/LifecycleProcessor.kt
@@ -24,7 +24,6 @@
 import javax.lang.model.element.TypeElement
 
 @SupportedAnnotationTypes("android.arch.lifecycle.OnLifecycleEvent")
-@SupportedSourceVersion(SourceVersion.RELEASE_7)
 class LifecycleProcessor : AbstractProcessor() {
     override fun process(annotations: MutableSet<out TypeElement>,
                          roundEnv: RoundEnvironment): Boolean {
@@ -32,4 +31,8 @@
         writeModels(transformToOutput(processingEnv, input), processingEnv)
         return true
     }
+
+    override fun getSupportedSourceVersion(): SourceVersion {
+        return SourceVersion.latest()
+    }
 }
diff --git a/lifecycle/extensions/build.gradle b/lifecycle/extensions/build.gradle
index 082e7f1..6bd1bce 100644
--- a/lifecycle/extensions/build.gradle
+++ b/lifecycle/extensions/build.gradle
@@ -30,21 +30,18 @@
     buildTypes.all {
         consumerProguardFiles 'proguard-rules.pro'
     }
-
-    testOptions {
-        unitTests.returnDefaultValues = true
-    }
 }
-dependencies {
-    compile project(":lifecycle:runtime")
-    compile project(":arch:common")
-    compile project(":arch:runtime")
-    compile libs.support.fragments, libs.support_exclude_config
-    compile project(":lifecycle:common")
 
-    testCompile project(":arch:core-testing")
-    testCompile libs.junit
-    testCompile libs.mockito_core
+dependencies {
+    api project(":lifecycle:runtime")
+    api project(":arch:common")
+    api project(":arch:runtime")
+    api libs.support.fragments, libs.support_exclude_config
+    api project(":lifecycle:common")
+
+    testImplementation project(":arch:core-testing")
+    testImplementation libs.junit
+    testImplementation libs.mockito_core
 
     androidTestImplementation libs.test_runner,      { exclude module: 'support-annotations' }
     androidTestImplementation libs.espresso_core,    { exclude module: 'support-annotations' }
diff --git a/lifecycle/integration-tests/testapp/build.gradle b/lifecycle/integration-tests/testapp/build.gradle
index a1a16cb..64159bd 100644
--- a/lifecycle/integration-tests/testapp/build.gradle
+++ b/lifecycle/integration-tests/testapp/build.gradle
@@ -32,6 +32,13 @@
         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
     }
 
+    signingConfigs {
+        debug {
+            // Use a local debug keystore to avoid build server issues.
+            storeFile project.rootProject.init.debugKeystore
+        }
+    }
+
     testOptions {
         unitTests.returnDefaultValues = true
     }
@@ -53,19 +60,21 @@
 
 dependencies {
     // IJ canont figure out transitive dependencies so need to declare them.
-    compile project(":lifecycle:common")
-    compile project(":lifecycle:runtime")
-    compile project(":lifecycle:extensions")
+    implementation project(":lifecycle:common")
+    implementation project(":lifecycle:runtime")
+    implementation project(":lifecycle:extensions")
     annotationProcessor project(":lifecycle:compiler")
+
     androidTestAnnotationProcessor project(":lifecycle:compiler")
-    androidTestCompile(libs.test_runner) {
+    androidTestImplementation(libs.test_runner) {
         exclude module: 'support-annotations'
     }
-    androidTestCompile(libs.espresso_core, {
+    androidTestImplementation(libs.espresso_core, {
         exclude group: 'com.android.support', module: 'support-annotations'
     })
-    testCompile libs.junit
-    testCompile libs.mockito_core
+
+    testImplementation libs.junit
+    testImplementation libs.mockito_core
     testAnnotationProcessor project(":lifecycle:compiler")
 }
 createAndroidCheckstyle(project)
diff --git a/lifecycle/reactivestreams/build.gradle b/lifecycle/reactivestreams/build.gradle
index 8f5a571..d2beeed 100644
--- a/lifecycle/reactivestreams/build.gradle
+++ b/lifecycle/reactivestreams/build.gradle
@@ -26,28 +26,24 @@
     defaultConfig {
         minSdkVersion flatfoot.min_sdk
     }
-
-    testOptions {
-        unitTests.returnDefaultValues = true
-    }
 }
 
 allprojects {
     dependencies {
-        compile project(":arch:common")
-        compile project(":lifecycle:common")
-        compile project(":lifecycle:extensions")
-        compile project(":lifecycle:runtime")
-        compile libs.support.annotations
-        compile libs.reactive_streams
+        api project(":arch:common")
+        api project(":lifecycle:common")
+        api project(":lifecycle:extensions")
+        api project(":lifecycle:runtime")
+        api libs.support.annotations
+        api libs.reactive_streams
 
-        testCompile libs.junit
-        testCompile libs.rx_java
+        testImplementation libs.junit
+        testImplementation libs.rx_java
 
-        testCompile(libs.test_runner) {
+        testImplementation(libs.test_runner) {
             exclude module: 'support-annotations'
         }
-        androidTestCompile libs.support.app_compat, libs.support_exclude_config
+        androidTestImplementation libs.support.app_compat, libs.support_exclude_config
     }
 }
 
diff --git a/lifecycle/runtime/build.gradle b/lifecycle/runtime/build.gradle
index 4b7235b..01a8abb 100644
--- a/lifecycle/runtime/build.gradle
+++ b/lifecycle/runtime/build.gradle
@@ -14,20 +14,16 @@
     buildTypes.all {
         consumerProguardFiles 'proguard-rules.pro'
     }
-
-    testOptions {
-        unitTests.returnDefaultValues = true
-    }
 }
 
 dependencies {
-    compile project(":lifecycle:common")
-    compile project(":arch:common")
+    api project(":lifecycle:common")
+    api project(":arch:common")
     // necessary for IJ to resolve dependencies.
-    compile libs.support.annotations
+    api libs.support.annotations
 
-    testCompile libs.junit
-    testCompile libs.mockito_core
+    testImplementation libs.junit
+    testImplementation libs.mockito_core
 
     androidTestImplementation libs.junit
     androidTestImplementation libs.test_runner,      { exclude module: 'support-annotations' }
diff --git a/media-compat/java/android/support/v4/media/MediaBrowserCompat.java b/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
index 7adf7d7..1b27925 100644
--- a/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
+++ b/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
@@ -41,10 +41,12 @@
 import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_QUERY;
 import static android.support.v4.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION;
 import static android.support.v4.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER;
+import static android.support.v4.media.MediaBrowserProtocol.EXTRA_SERVICE_VERSION;
 import static android.support.v4.media.MediaBrowserProtocol.EXTRA_SESSION_BINDER;
 import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT;
 import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT_FAILED;
 import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN;
+import static android.support.v4.media.MediaBrowserProtocol.SERVICE_VERSION_2;
 
 import android.content.ComponentName;
 import android.content.Context;
@@ -1581,6 +1583,7 @@
         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;
@@ -1850,6 +1853,7 @@
             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);
@@ -1956,7 +1960,9 @@
         @Override
         public void subscribe(@NonNull String parentId, @Nullable Bundle options,
                 @NonNull SubscriptionCallback callback) {
-            if (mServiceBinderWrapper == null) {
+            // 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);
@@ -1971,7 +1977,9 @@
 
         @Override
         public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) {
-            if (mServiceBinderWrapper == null) {
+            // 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 {
diff --git a/media-compat/java/android/support/v4/media/MediaBrowserProtocol.java b/media-compat/java/android/support/v4/media/MediaBrowserProtocol.java
index 7c23d26..8ed152d 100644
--- a/media-compat/java/android/support/v4/media/MediaBrowserProtocol.java
+++ b/media-compat/java/android/support/v4/media/MediaBrowserProtocol.java
@@ -45,7 +45,15 @@
      * MediaBrowserServiceCompat.
      */
     public static final int SERVICE_VERSION_1 = 1;
-    public static final int SERVICE_VERSION_CURRENT = SERVICE_VERSION_1;
+
+    /**
+     * To prevent ClassNotFoundException from Parcelables, MediaBrowser(Service)Compat tries to
+     * avoid using framework code as much as possible (b/62648808). For backward compatibility,
+     * service v2 is introduced so that the browser can distinguish whether the service supports
+     * subscribing through compat code.
+     */
+    public static final int SERVICE_VERSION_2 = 2;
+    public static final int SERVICE_VERSION_CURRENT = SERVICE_VERSION_2;
 
     /*
      * Messages sent from the media browser service compat to the media browser compat.
diff --git a/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java b/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
index debc66e..27bf0e3 100644
--- a/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
+++ b/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
@@ -278,28 +278,8 @@
 
         @Override
         public void notifyChildrenChanged(final String parentId, final Bundle options) {
-            if (mMessenger == null) {
-                MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId);
-            } else {
-                mHandler.post(new Runnable() {
-                    @Override
-                    public void run() {
-                        for (IBinder binder : mConnections.keySet()) {
-                            ConnectionRecord connection = mConnections.get(binder);
-                            List<Pair<IBinder, Bundle>> callbackList =
-                                    connection.subscriptions.get(parentId);
-                            if (callbackList != null) {
-                                for (Pair<IBinder, Bundle> callback : callbackList) {
-                                    if (MediaBrowserCompatUtils.hasDuplicatedItems(
-                                            options, callback.second)) {
-                                        performLoadChildren(parentId, connection, callback.second);
-                                    }
-                                }
-                            }
-                        }
-                    }
-                });
-            }
+            notifyChildrenChangedForFramework(parentId, options);
+            notifyChildrenChangedForCompat(parentId, options);
         }
 
         @Override
@@ -373,6 +353,31 @@
             };
             MediaBrowserServiceCompat.this.onLoadChildren(parentId, result);
         }
+
+        void notifyChildrenChangedForFramework(final String parentId, final Bundle options) {
+            MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId);
+        }
+
+        void notifyChildrenChangedForCompat(final String parentId, final Bundle options) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    for (IBinder binder : mConnections.keySet()) {
+                        ConnectionRecord connection = mConnections.get(binder);
+                        List<Pair<IBinder, Bundle>> callbackList =
+                                connection.subscriptions.get(parentId);
+                        if (callbackList != null) {
+                            for (Pair<IBinder, Bundle> callback : callbackList) {
+                                if (MediaBrowserCompatUtils.hasDuplicatedItems(
+                                        options, callback.second)) {
+                                    performLoadChildren(parentId, connection, callback.second);
+                                }
+                            }
+                        }
+                    }
+                }
+            });
+        }
     }
 
     @RequiresApi(23)
@@ -421,20 +426,6 @@
         }
 
         @Override
-        public void notifyChildrenChanged(final String parentId, final Bundle options) {
-            if (mMessenger == null) {
-                if (options == null) {
-                    MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId);
-                } else {
-                    MediaBrowserServiceCompatApi26.notifyChildrenChanged(mServiceObj, parentId,
-                            options);
-                }
-            } else {
-                super.notifyChildrenChanged(parentId, options);
-            }
-        }
-
-        @Override
         public void onLoadChildren(String parentId,
                 final MediaBrowserServiceCompatApi26.ResultWrapper resultWrapper, Bundle options) {
             final Result<List<MediaBrowserCompat.MediaItem>> result
@@ -470,6 +461,16 @@
             }
             return MediaBrowserServiceCompatApi26.getBrowserRootHints(mServiceObj);
         }
+
+        @Override
+        void notifyChildrenChangedForFramework(final String parentId, final Bundle options) {
+            if (options != null) {
+                MediaBrowserServiceCompatApi26.notifyChildrenChanged(mServiceObj, parentId,
+                        options);
+            } else {
+                super.notifyChildrenChangedForFramework(parentId, options);
+            }
+        }
     }
 
     private final class ServiceHandler extends Handler {
diff --git a/media-compat/tests/AndroidManifest.xml b/media-compat/tests/AndroidManifest.xml
index 6d9a4f6..09c004d 100644
--- a/media-compat/tests/AndroidManifest.xml
+++ b/media-compat/tests/AndroidManifest.xml
@@ -24,23 +24,6 @@
                 <action android:name="android.intent.action.MEDIA_BUTTON"/>
             </intent-filter>
         </receiver>
-        <service android:name="android.support.v4.media.StubMediaBrowserServiceCompat">
-            <intent-filter>
-                <action android:name="android.media.browse.MediaBrowserService"/>
-            </intent-filter>
-        </service>
-        <service android:name="android.support.v4.media.StubRemoteMediaBrowserServiceCompat"
-                 android:process=":remote">
-            <intent-filter>
-                <action android:name="android.media.browse.MediaBrowserService"/>
-            </intent-filter>
-        </service>
-        <service
-            android:name="android.support.v4.media.StubMediaBrowserServiceCompatWithDelayedMediaSession">
-            <intent-filter>
-                <action android:name="android.media.browse.MediaBrowserService"/>
-            </intent-filter>
-        </service>
     </application>
 
 </manifest>
diff --git a/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java b/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
deleted file mode 100644
index 6356e33..0000000
--- a/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
+++ /dev/null
@@ -1,786 +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.support.v4.media;
-
-import static android.support.test.InstrumentationRegistry.getInstrumentation;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertNotNull;
-import static junit.framework.Assert.assertNull;
-import static junit.framework.Assert.assertTrue;
-import static junit.framework.Assert.fail;
-
-import android.content.ComponentName;
-import android.os.Bundle;
-import android.os.Handler;
-import android.support.test.filters.LargeTest;
-import android.support.test.filters.MediumTest;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.testutils.PollingCheck;
-import android.support.v4.media.MediaBrowserCompat.MediaItem;
-import android.support.v4.media.session.MediaControllerCompat;
-import android.support.v4.media.session.MediaSessionCompat;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Test {@link android.support.v4.media.MediaBrowserCompat}.
- */
-@RunWith(AndroidJUnit4.class)
-public class MediaBrowserCompatTest {
-
-    // The maximum time to wait for an operation.
-    private static final long TIME_OUT_MS = 3000L;
-
-    /**
-     * To check {@link MediaBrowserCompat#unsubscribe} works properly,
-     * we notify to the browser after the unsubscription that the media items have changed.
-     * Then {@link MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded} should not be called.
-     *
-     * The measured time from calling {@link StubMediaBrowserServiceCompat#notifyChildrenChanged}
-     * to {@link MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded} being called is about
-     * 50ms.
-     * So we make the thread sleep for 100ms to properly check that the callback is not called.
-     */
-    private static final long SLEEP_MS = 100L;
-    private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
-            "android.support.mediacompat.test",
-            "android.support.v4.media.StubMediaBrowserServiceCompat");
-    private static final ComponentName TEST_REMOTE_BROWSER_SERVICE = new ComponentName(
-            "android.support.mediacompat.test",
-            "android.support.v4.media.StubRemoteMediaBrowserServiceCompat");
-    private static final ComponentName TEST_INVALID_BROWSER_SERVICE = new ComponentName(
-            "invalid.package", "invalid.ServiceClassName");
-
-    private MediaBrowserCompat mMediaBrowser;
-    private StubConnectionCallback mConnectionCallback;
-    private StubSubscriptionCallback mSubscriptionCallback;
-    private StubItemCallback mItemCallback;
-
-    @Before
-    public void setUp() {
-        mConnectionCallback = new StubConnectionCallback();
-        mSubscriptionCallback = new StubSubscriptionCallback();
-        mItemCallback = new StubItemCallback();
-    }
-
-    @Test
-    @SmallTest
-    public void testMediaBrowser() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        assertFalse(mMediaBrowser.isConnected());
-
-        connectMediaBrowserService();
-        assertTrue(mMediaBrowser.isConnected());
-
-        assertEquals(TEST_BROWSER_SERVICE, mMediaBrowser.getServiceComponent());
-        assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, mMediaBrowser.getRoot());
-        assertEquals(StubMediaBrowserServiceCompat.EXTRAS_VALUE,
-                mMediaBrowser.getExtras().getString(StubMediaBrowserServiceCompat.EXTRAS_KEY));
-        assertEquals(StubMediaBrowserServiceCompat.sSession.getSessionToken(),
-                mMediaBrowser.getSessionToken());
-
-        mMediaBrowser.disconnect();
-        new PollingCheck(TIME_OUT_MS) {
-            @Override
-            protected boolean check() {
-                return !mMediaBrowser.isConnected();
-            }
-        }.run();
-    }
-
-    @Test
-    @SmallTest
-    public void testMediaBrowserWithRemoteService() throws Exception {
-        createMediaBrowser(TEST_REMOTE_BROWSER_SERVICE);
-        assertFalse(mMediaBrowser.isConnected());
-
-        connectMediaBrowserService();
-        assertTrue(mMediaBrowser.isConnected());
-
-        assertEquals(TEST_REMOTE_BROWSER_SERVICE, mMediaBrowser.getServiceComponent());
-        assertEquals(StubRemoteMediaBrowserServiceCompat.MEDIA_ID_ROOT, mMediaBrowser.getRoot());
-        assertEquals(StubRemoteMediaBrowserServiceCompat.EXTRAS_VALUE,
-                mMediaBrowser.getExtras().getString(
-                        StubRemoteMediaBrowserServiceCompat.EXTRAS_KEY));
-
-        mMediaBrowser.disconnect();
-        new PollingCheck(TIME_OUT_MS) {
-            @Override
-            protected boolean check() {
-                return !mMediaBrowser.isConnected();
-            }
-        }.run();
-    }
-
-    @Test
-    @SmallTest
-    public void testSessionReadyWithRemoteService() throws Exception {
-        if (android.os.Build.VERSION.SDK_INT < 21) {
-            // This test is for API 21+
-            return;
-        }
-
-        createMediaBrowser(TEST_REMOTE_BROWSER_SERVICE);
-        assertFalse(mMediaBrowser.isConnected());
-
-        connectMediaBrowserService();
-        assertTrue(mMediaBrowser.isConnected());
-
-        // Create a session token by removing the extra binder of the token from MediaBrowserCompat.
-        final MediaSessionCompat.Token tokenWithoutExtraBinder = MediaSessionCompat.Token.fromToken(
-                mMediaBrowser.getSessionToken().getToken());
-
-        final MediaControllerCallback callback = new MediaControllerCallback();
-        synchronized (callback.mWaitLock) {
-            getInstrumentation().runOnMainSync(new Runnable() {
-                @Override
-                public void run() {
-                    try {
-                        MediaControllerCompat controller = new MediaControllerCompat(
-                                getInstrumentation().getTargetContext(), tokenWithoutExtraBinder);
-                        controller.registerCallback(callback, new Handler());
-                        assertFalse(controller.isSessionReady());
-                    } catch (Exception e) {
-                        fail();
-                    }
-                }
-            });
-            callback.mWaitLock.wait(TIME_OUT_MS);
-            assertTrue(callback.mOnSessionReadyCalled);
-        }
-
-        mMediaBrowser.disconnect();
-    }
-
-    @Test
-    @SmallTest
-    public void testSubscriptionWithCustomOptionsWithRemoteService() throws Exception {
-        final String mediaId = "1000";
-        createMediaBrowser(TEST_REMOTE_BROWSER_SERVICE);
-        assertFalse(mMediaBrowser.isConnected());
-
-        connectMediaBrowserService();
-        assertTrue(mMediaBrowser.isConnected());
-
-        MediaMetadataCompat mediaMetadataCompat = new MediaMetadataCompat.Builder()
-                .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId)
-                .putString(MediaMetadataCompat.METADATA_KEY_TITLE, "title")
-                .putRating(MediaMetadataCompat.METADATA_KEY_RATING,
-                        RatingCompat.newPercentageRating(0.5f))
-                .putRating(MediaMetadataCompat.METADATA_KEY_USER_RATING,
-                        RatingCompat.newPercentageRating(0.8f))
-                .build();
-        Bundle options = new Bundle();
-        options.putParcelable(
-                StubRemoteMediaBrowserServiceCompat.MEDIA_METADATA, mediaMetadataCompat);
-
-        // Remote MediaBrowserService will create a media item with the given MediaMetadataCompat
-        mMediaBrowser.subscribe(mMediaBrowser.getRoot(), options, mSubscriptionCallback);
-        synchronized (mSubscriptionCallback.mWaitLock) {
-            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertEquals(1, mSubscriptionCallback.mChildrenLoadedWithOptionCount);
-            assertEquals(1, mSubscriptionCallback.mLastChildMediaItems.size());
-            assertEquals(mediaId, mSubscriptionCallback.mLastChildMediaItems.get(0).getMediaId());
-        }
-        mMediaBrowser.disconnect();
-    }
-
-    @Test
-    @SmallTest
-    public void testConnectTwice() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        connectMediaBrowserService();
-        try {
-            mMediaBrowser.connect();
-            fail();
-        } catch (IllegalStateException e) {
-            // expected
-        }
-        mMediaBrowser.disconnect();
-    }
-
-    @Test
-    @SmallTest
-    public void testConnectionFailed() throws Exception {
-        createMediaBrowser(TEST_INVALID_BROWSER_SERVICE);
-
-        synchronized (mConnectionCallback.mWaitLock) {
-            mMediaBrowser.connect();
-            mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
-        }
-        assertTrue(mConnectionCallback.mConnectionFailedCount > 0);
-        assertEquals(0, mConnectionCallback.mConnectedCount);
-        assertEquals(0, mConnectionCallback.mConnectionSuspendedCount);
-        mMediaBrowser.disconnect();
-    }
-
-    @Test
-    @SmallTest
-    public void testReconnection() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-
-        getInstrumentation().runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                mMediaBrowser.connect();
-                // Reconnect before the first connection was established.
-                mMediaBrowser.disconnect();
-                mMediaBrowser.connect();
-            }
-        });
-
-        synchronized (mConnectionCallback.mWaitLock) {
-            mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertEquals(1, mConnectionCallback.mConnectedCount);
-        }
-
-        synchronized (mSubscriptionCallback.mWaitLock) {
-            // Test subscribe.
-            resetCallbacks();
-            mMediaBrowser.subscribe(
-                    StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, mSubscriptionCallback);
-            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertTrue(mSubscriptionCallback.mChildrenLoadedCount > 0);
-            assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT,
-                    mSubscriptionCallback.mLastParentId);
-        }
-
-        synchronized (mItemCallback.mWaitLock) {
-            // Test getItem.
-            resetCallbacks();
-            mMediaBrowser.getItem(
-                    StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN[0], mItemCallback);
-            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN[0],
-                    mItemCallback.mLastMediaItem.getMediaId());
-        }
-
-        // Reconnect after connection was established.
-        mMediaBrowser.disconnect();
-        resetCallbacks();
-        connectMediaBrowserService();
-
-        synchronized (mItemCallback.mWaitLock) {
-            // Test getItem.
-            resetCallbacks();
-            mMediaBrowser.getItem(
-                    StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN[0], mItemCallback);
-            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN[0],
-                    mItemCallback.mLastMediaItem.getMediaId());
-        }
-        mMediaBrowser.disconnect();
-    }
-
-    @Test
-    @SmallTest
-    public void testConnectionCallbackNotCalledAfterDisconnect() {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-
-        getInstrumentation().runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                mMediaBrowser.connect();
-                mMediaBrowser.disconnect();
-                resetCallbacks();
-            }
-        });
-
-        try {
-            Thread.sleep(SLEEP_MS);
-        } catch (InterruptedException e) {
-            fail("Unexpected InterruptedException occurred.");
-        }
-        assertEquals(0, mConnectionCallback.mConnectedCount);
-        assertEquals(0, mConnectionCallback.mConnectionFailedCount);
-        assertEquals(0, mConnectionCallback.mConnectionSuspendedCount);
-    }
-
-    @Test
-    @SmallTest
-    public void testGetServiceComponentBeforeConnection() {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        try {
-            ComponentName serviceComponent = mMediaBrowser.getServiceComponent();
-            fail();
-        } catch (IllegalStateException e) {
-            // expected
-        }
-    }
-
-    @Test
-    @SmallTest
-    public void testSubscribe() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        connectMediaBrowserService();
-
-        synchronized (mSubscriptionCallback.mWaitLock) {
-            mMediaBrowser.subscribe(
-                    StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, mSubscriptionCallback);
-            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertTrue(mSubscriptionCallback.mChildrenLoadedCount > 0);
-            assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT,
-                    mSubscriptionCallback.mLastParentId);
-            assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN.length,
-                    mSubscriptionCallback.mLastChildMediaItems.size());
-            for (int i = 0; i < StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN.length; ++i) {
-                assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN[i],
-                        mSubscriptionCallback.mLastChildMediaItems.get(i).getMediaId());
-            }
-        }
-
-        // Test unsubscribe.
-        resetCallbacks();
-        mMediaBrowser.unsubscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
-
-        // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
-        // changed.
-        StubMediaBrowserServiceCompat.sInstance.notifyChildrenChanged(
-                StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
-        try {
-            Thread.sleep(SLEEP_MS);
-        } catch (InterruptedException e) {
-            fail("Unexpected InterruptedException occurred.");
-        }
-        // onChildrenLoaded should not be called.
-        assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
-        mMediaBrowser.disconnect();
-    }
-
-    @Test
-    @SmallTest
-    public void testSubscribeWithOptions() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        connectMediaBrowserService();
-        final int pageSize = 3;
-        final int lastPage =
-                (StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN.length - 1) / pageSize;
-        Bundle options = new Bundle();
-        options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
-
-        synchronized (mSubscriptionCallback.mWaitLock) {
-            for (int page = 0; page <= lastPage; ++page) {
-                resetCallbacks();
-                options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
-                mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, options,
-                        mSubscriptionCallback);
-                mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
-                assertTrue(mSubscriptionCallback.mChildrenLoadedWithOptionCount > 0);
-                assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT,
-                        mSubscriptionCallback.mLastParentId);
-                if (page != lastPage) {
-                    assertEquals(pageSize, mSubscriptionCallback.mLastChildMediaItems.size());
-                } else {
-                    assertEquals(
-                            (StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN.length - 1) % pageSize
-                                    + 1,
-                            mSubscriptionCallback.mLastChildMediaItems.size());
-                }
-                // Check whether all the items in the current page are loaded.
-                for (int i = 0; i < mSubscriptionCallback.mLastChildMediaItems.size(); ++i) {
-                    assertEquals(
-                            StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN[page * pageSize + i],
-                            mSubscriptionCallback.mLastChildMediaItems.get(i).getMediaId());
-                }
-            }
-        }
-
-        // Test unsubscribe with callback argument.
-        resetCallbacks();
-        mMediaBrowser.unsubscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT,
-                mSubscriptionCallback);
-
-        // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
-        // changed.
-        StubMediaBrowserServiceCompat.sInstance.notifyChildrenChanged(
-                StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
-        try {
-            Thread.sleep(SLEEP_MS);
-        } catch (InterruptedException e) {
-            fail("Unexpected InterruptedException occurred.");
-        }
-        // onChildrenLoaded should not be called.
-        assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
-        mMediaBrowser.disconnect();
-    }
-
-    @Test
-    @SmallTest
-    public void testSubscribeInvalidItem() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        connectMediaBrowserService();
-
-        synchronized (mSubscriptionCallback.mWaitLock) {
-            mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_INVALID,
-                    mSubscriptionCallback);
-            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_INVALID,
-                    mSubscriptionCallback.mLastErrorId);
-        }
-        mMediaBrowser.disconnect();
-    }
-
-    @Test
-    @SmallTest
-    public void testSubscribeInvalidItemWithOptions() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        connectMediaBrowserService();
-
-        final int pageSize = 5;
-        final int page = 2;
-        Bundle options = new Bundle();
-        options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
-        options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
-
-        synchronized (mSubscriptionCallback.mWaitLock) {
-            mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_INVALID, options,
-                    mSubscriptionCallback);
-            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_INVALID,
-                    mSubscriptionCallback.mLastErrorId);
-            assertNotNull(mSubscriptionCallback.mLastOptions);
-            assertEquals(page,
-                    mSubscriptionCallback.mLastOptions.getInt(MediaBrowserCompat.EXTRA_PAGE));
-            assertEquals(pageSize,
-                    mSubscriptionCallback.mLastOptions.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE));
-        }
-        mMediaBrowser.disconnect();
-    }
-
-    @Test
-    @SmallTest
-    public void testUnsubscribeForMultipleSubscriptions() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        connectMediaBrowserService();
-        final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
-        final int pageSize = 1;
-
-        // Subscribe four pages, one item per page.
-        for (int page = 0; page < 4; page++) {
-            final StubSubscriptionCallback callback = new StubSubscriptionCallback();
-            subscriptionCallbacks.add(callback);
-
-            Bundle options = new Bundle();
-            options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
-            options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
-            mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, options,
-                    callback);
-            synchronized (callback.mWaitLock) {
-                callback.mWaitLock.wait(TIME_OUT_MS);
-            }
-            // Each onChildrenLoaded() must be called.
-            assertEquals(1, callback.mChildrenLoadedWithOptionCount);
-        }
-
-        // Reset callbacks and unsubscribe.
-        for (StubSubscriptionCallback callback : subscriptionCallbacks) {
-            callback.reset();
-        }
-        mMediaBrowser.unsubscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
-
-        // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
-        // changed.
-        StubMediaBrowserServiceCompat.sInstance.notifyChildrenChanged(
-                StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
-        try {
-            Thread.sleep(SLEEP_MS);
-        } catch (InterruptedException e) {
-            fail("Unexpected InterruptedException occurred.");
-        }
-
-        // onChildrenLoaded should not be called.
-        for (StubSubscriptionCallback callback : subscriptionCallbacks) {
-            assertEquals(0, callback.mChildrenLoadedWithOptionCount);
-        }
-        mMediaBrowser.disconnect();
-    }
-
-    @Test
-    @MediumTest
-    public void testUnsubscribeWithSubscriptionCallbackForMultipleSubscriptions() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        connectMediaBrowserService();
-        final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
-        final int pageSize = 1;
-
-        // Subscribe four pages, one item per page.
-        for (int page = 0; page < 4; page++) {
-            final StubSubscriptionCallback callback = new StubSubscriptionCallback();
-            subscriptionCallbacks.add(callback);
-
-            Bundle options = new Bundle();
-            options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
-            options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
-            mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, options,
-                    callback);
-            synchronized (callback.mWaitLock) {
-                callback.mWaitLock.wait(TIME_OUT_MS);
-            }
-            // Each onChildrenLoaded() must be called.
-            assertEquals(1, callback.mChildrenLoadedWithOptionCount);
-        }
-
-        // Unsubscribe existing subscriptions one-by-one.
-        final int[] orderOfRemovingCallbacks = {2, 0, 3, 1};
-        for (int i = 0; i < orderOfRemovingCallbacks.length; i++) {
-            // Reset callbacks
-            for (StubSubscriptionCallback callback : subscriptionCallbacks) {
-                callback.reset();
-            }
-
-            // Remove one subscription
-            mMediaBrowser.unsubscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT,
-                    subscriptionCallbacks.get(orderOfRemovingCallbacks[i]));
-
-            // Make StubMediaBrowserServiceCompat notify that the children are changed.
-            StubMediaBrowserServiceCompat.sInstance.notifyChildrenChanged(
-                    StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
-            try {
-                Thread.sleep(SLEEP_MS);
-            } catch (InterruptedException e) {
-                fail("Unexpected InterruptedException occurred.");
-            }
-
-            // Only the remaining subscriptionCallbacks should be called.
-            for (int j = 0; j < 4; j++) {
-                int childrenLoadedWithOptionsCount = subscriptionCallbacks
-                        .get(orderOfRemovingCallbacks[j]).mChildrenLoadedWithOptionCount;
-                if (j <= i) {
-                    assertEquals(0, childrenLoadedWithOptionsCount);
-                } else {
-                    assertEquals(1, childrenLoadedWithOptionsCount);
-                }
-            }
-        }
-        mMediaBrowser.disconnect();
-    }
-
-    @Test
-    @SmallTest
-    public void testGetItem() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        connectMediaBrowserService();
-
-        synchronized (mItemCallback.mWaitLock) {
-            mMediaBrowser.getItem(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN[0],
-                    mItemCallback);
-            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertNotNull(mItemCallback.mLastMediaItem);
-            assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN[0],
-                    mItemCallback.mLastMediaItem.getMediaId());
-        }
-        mMediaBrowser.disconnect();
-    }
-
-    @Test
-    @LargeTest
-    public void testGetItemWhenOnLoadItemIsNotImplemented() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        connectMediaBrowserService();
-        synchronized (mItemCallback.mWaitLock) {
-            mMediaBrowser.getItem(
-                    StubMediaBrowserServiceCompat.MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED,
-                    mItemCallback);
-            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED,
-                    mItemCallback.mLastErrorId);
-        }
-        mMediaBrowser.disconnect();
-    }
-
-    @Test
-    @SmallTest
-    public void testGetItemWhenMediaIdIsInvalid() throws Exception {
-        mItemCallback.mLastMediaItem = new MediaItem(new MediaDescriptionCompat.Builder()
-                .setMediaId("dummy_id").build(), MediaItem.FLAG_BROWSABLE);
-
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        connectMediaBrowserService();
-        synchronized (mItemCallback.mWaitLock) {
-            mMediaBrowser.getItem(StubMediaBrowserServiceCompat.MEDIA_ID_INVALID, mItemCallback);
-            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertNull(mItemCallback.mLastMediaItem);
-            assertNull(mItemCallback.mLastErrorId);
-        }
-        mMediaBrowser.disconnect();
-    }
-
-    private void createMediaBrowser(final ComponentName component) {
-        getInstrumentation().runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
-                        component, mConnectionCallback, null);
-            }
-        });
-    }
-
-    private void connectMediaBrowserService() throws Exception {
-        synchronized (mConnectionCallback.mWaitLock) {
-            mMediaBrowser.connect();
-            mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
-        }
-    }
-
-    private void resetCallbacks() {
-        mConnectionCallback.reset();
-        mSubscriptionCallback.reset();
-        mItemCallback.reset();
-    }
-
-    private class StubConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
-        Object mWaitLock = new Object();
-        volatile int mConnectedCount;
-        volatile int mConnectionFailedCount;
-        volatile int mConnectionSuspendedCount;
-
-        public void reset() {
-            mConnectedCount = 0;
-            mConnectionFailedCount = 0;
-            mConnectionSuspendedCount = 0;
-        }
-
-        @Override
-        public void onConnected() {
-            synchronized (mWaitLock) {
-                mConnectedCount++;
-                mWaitLock.notify();
-            }
-        }
-
-        @Override
-        public void onConnectionFailed() {
-            synchronized (mWaitLock) {
-                mConnectionFailedCount++;
-                mWaitLock.notify();
-            }
-        }
-
-        @Override
-        public void onConnectionSuspended() {
-            synchronized (mWaitLock) {
-                mConnectionSuspendedCount++;
-                mWaitLock.notify();
-            }
-        }
-    }
-
-    private class StubSubscriptionCallback extends MediaBrowserCompat.SubscriptionCallback {
-        Object mWaitLock = new Object();
-        private volatile int mChildrenLoadedCount;
-        private volatile int mChildrenLoadedWithOptionCount;
-        private volatile String mLastErrorId;
-        private volatile String mLastParentId;
-        private volatile Bundle mLastOptions;
-        private volatile List<MediaItem> mLastChildMediaItems;
-
-        public void reset() {
-            mChildrenLoadedCount = 0;
-            mChildrenLoadedWithOptionCount = 0;
-            mLastErrorId = null;
-            mLastParentId = null;
-            mLastOptions = null;
-            mLastChildMediaItems = null;
-        }
-
-        @Override
-        public void onChildrenLoaded(String parentId, List<MediaItem> children) {
-            synchronized (mWaitLock) {
-                mChildrenLoadedCount++;
-                mLastParentId = parentId;
-                mLastChildMediaItems = children;
-                mWaitLock.notify();
-            }
-        }
-
-        @Override
-        public void onChildrenLoaded(String parentId, List<MediaItem> children, Bundle options) {
-            synchronized (mWaitLock) {
-                mChildrenLoadedWithOptionCount++;
-                mLastParentId = parentId;
-                mLastOptions = options;
-                mLastChildMediaItems = children;
-                mWaitLock.notify();
-            }
-        }
-
-        @Override
-        public void onError(String id) {
-            synchronized (mWaitLock) {
-                mLastErrorId = id;
-                mWaitLock.notify();
-            }
-        }
-
-        @Override
-        public void onError(String id, Bundle options) {
-            synchronized (mWaitLock) {
-                mLastErrorId = id;
-                mLastOptions = options;
-                mWaitLock.notify();
-            }
-        }
-    }
-
-    private class StubItemCallback extends MediaBrowserCompat.ItemCallback {
-        Object mWaitLock = new Object();
-        private volatile MediaItem mLastMediaItem;
-        private volatile String mLastErrorId;
-
-        public void reset() {
-            mLastMediaItem = null;
-            mLastErrorId = null;
-        }
-
-        @Override
-        public void onItemLoaded(MediaItem item) {
-            synchronized (mWaitLock) {
-                mLastMediaItem = item;
-                mWaitLock.notify();
-            }
-        }
-
-        @Override
-        public void onError(String id) {
-            synchronized (mWaitLock) {
-                mLastErrorId = id;
-                mWaitLock.notify();
-            }
-        }
-    }
-
-    private class MediaControllerCallback extends MediaControllerCompat.Callback {
-        Object mWaitLock = new Object();
-        private volatile boolean mOnSessionReadyCalled;
-
-        @Override
-        public void onSessionReady() {
-            synchronized (mWaitLock) {
-                mOnSessionReadyCalled = true;
-                mWaitLock.notify();
-            }
-        }
-    }
-}
diff --git a/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java b/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java
deleted file mode 100644
index 4ceac10..0000000
--- a/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java
+++ /dev/null
@@ -1,586 +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.support.v4.media;
-
-import static android.support.test.InstrumentationRegistry.getInstrumentation;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertNotNull;
-import static junit.framework.Assert.assertNull;
-import static junit.framework.Assert.assertTrue;
-
-import android.content.ComponentName;
-import android.os.Build;
-import android.os.Bundle;
-import android.support.test.filters.MediumTest;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.testutils.PollingCheck;
-import android.support.v4.media.MediaBrowserCompat.MediaItem;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.List;
-
-/**
- * Test {@link android.support.v4.media.MediaBrowserServiceCompat}.
- */
-@RunWith(AndroidJUnit4.class)
-public class MediaBrowserServiceCompatTest {
-    // The maximum time to wait for an operation.
-    private static final long TIME_OUT_MS = 3000L;
-    private static final long WAIT_TIME_FOR_NO_RESPONSE_MS = 500L;
-    private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
-            "android.support.mediacompat.test",
-            "android.support.v4.media.StubMediaBrowserServiceCompat");
-    private static final ComponentName TEST_BROWSER_SERVICE_DELAYED_MEDIA_SESSION =
-            new ComponentName(
-                    "android.support.mediacompat.test",
-                    "android.support.v4.media"
-                            + ".StubMediaBrowserServiceCompatWithDelayedMediaSession");
-    private static final String TEST_KEY_1 = "key_1";
-    private static final String TEST_VALUE_1 = "value_1";
-    private static final String TEST_KEY_2 = "key_2";
-    private static final String TEST_VALUE_2 = "value_2";
-    private static final String TEST_KEY_3 = "key_3";
-    private static final String TEST_VALUE_3 = "value_3";
-    private static final String TEST_KEY_4 = "key_4";
-    private static final String TEST_VALUE_4 = "value_4";
-    private final Object mWaitLock = new Object();
-
-    private final ConnectionCallback mConnectionCallback = new ConnectionCallback();
-    private final SubscriptionCallback mSubscriptionCallback = new SubscriptionCallback();
-    private final ItemCallback mItemCallback = new ItemCallback();
-    private final SearchCallback mSearchCallback = new SearchCallback();
-
-    private MediaBrowserCompat mMediaBrowser;
-    private MediaBrowserCompat mMediaBrowserForDelayedMediaSession;
-    private StubMediaBrowserServiceCompat mMediaBrowserService;
-    private Bundle mRootHints;
-
-    @Before
-    public void setUp() throws Exception {
-        getInstrumentation().runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                mRootHints = new Bundle();
-                mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT, true);
-                mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_OFFLINE, true);
-                mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_SUGGESTED, true);
-                mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
-                        TEST_BROWSER_SERVICE, mConnectionCallback, mRootHints);
-            }
-        });
-        synchronized (mWaitLock) {
-            mMediaBrowser.connect();
-            mWaitLock.wait(TIME_OUT_MS);
-        }
-        assertNotNull(mMediaBrowserService);
-        mMediaBrowserService.mCustomActionExtras = null;
-        mMediaBrowserService.mCustomActionResult = null;
-    }
-
-    @Test
-    @SmallTest
-    public void testGetSessionToken() {
-        assertEquals(StubMediaBrowserServiceCompat.sSession.getSessionToken(),
-                mMediaBrowserService.getSessionToken());
-    }
-
-    @Test
-    @SmallTest
-    public void testNotifyChildrenChanged() throws Exception {
-        synchronized (mWaitLock) {
-            mSubscriptionCallback.reset();
-            mMediaBrowser.subscribe(
-                    StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, mSubscriptionCallback);
-            mWaitLock.wait(TIME_OUT_MS);
-            assertTrue(mSubscriptionCallback.mOnChildrenLoaded);
-
-            mSubscriptionCallback.reset();
-            mMediaBrowserService.notifyChildrenChanged(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
-            mWaitLock.wait(TIME_OUT_MS);
-            assertTrue(mSubscriptionCallback.mOnChildrenLoaded);
-        }
-    }
-
-    @Test
-    @SmallTest
-    public void testNotifyChildrenChangedWithPagination() throws Exception {
-        synchronized (mWaitLock) {
-            final int pageSize = 5;
-            final int page = 2;
-            Bundle options = new Bundle();
-            options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
-            options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
-
-            mSubscriptionCallback.reset();
-            mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, options,
-                    mSubscriptionCallback);
-            mWaitLock.wait(TIME_OUT_MS);
-            assertTrue(mSubscriptionCallback.mOnChildrenLoadedWithOptions);
-
-            mSubscriptionCallback.reset();
-            mMediaBrowserService.notifyChildrenChanged(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
-            mWaitLock.wait(TIME_OUT_MS);
-            assertTrue(mSubscriptionCallback.mOnChildrenLoadedWithOptions);
-        }
-    }
-
-    @Test
-    @MediumTest
-    public void testDelayedNotifyChildrenChanged() throws Exception {
-        synchronized (mWaitLock) {
-            mSubscriptionCallback.reset();
-            mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN_DELAYED,
-                    mSubscriptionCallback);
-            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
-            assertFalse(mSubscriptionCallback.mOnChildrenLoaded);
-
-            mMediaBrowserService.sendDelayedNotifyChildrenChanged();
-            mWaitLock.wait(TIME_OUT_MS);
-            assertTrue(mSubscriptionCallback.mOnChildrenLoaded);
-
-            mSubscriptionCallback.reset();
-            mMediaBrowserService.notifyChildrenChanged(
-                    StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN_DELAYED);
-            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
-            assertFalse(mSubscriptionCallback.mOnChildrenLoaded);
-
-            mMediaBrowserService.sendDelayedNotifyChildrenChanged();
-            mWaitLock.wait(TIME_OUT_MS);
-            assertTrue(mSubscriptionCallback.mOnChildrenLoaded);
-        }
-    }
-
-    @Test
-    @MediumTest
-    public void testDelayedItem() throws Exception {
-        synchronized (mWaitLock) {
-            mItemCallback.reset();
-            mMediaBrowser.getItem(
-                    StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN_DELAYED, mItemCallback);
-            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
-            assertFalse(mItemCallback.mOnItemLoaded);
-
-            mItemCallback.reset();
-            mMediaBrowserService.sendDelayedItemLoaded();
-            mWaitLock.wait(TIME_OUT_MS);
-            assertTrue(mItemCallback.mOnItemLoaded);
-        }
-    }
-
-    @Test
-    @SmallTest
-    public void testSearch() throws Exception {
-        final String key = "test-key";
-        final String val = "test-val";
-
-        synchronized (mWaitLock) {
-            mSearchCallback.reset();
-            mMediaBrowser.search(StubMediaBrowserServiceCompat.SEARCH_QUERY_FOR_NO_RESULT, null,
-                    mSearchCallback);
-            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
-            assertTrue(mSearchCallback.mOnSearchResult);
-            assertTrue(mSearchCallback.mSearchResults != null
-                    && mSearchCallback.mSearchResults.size() == 0);
-            assertEquals(null, mSearchCallback.mSearchExtras);
-
-            mSearchCallback.reset();
-            mMediaBrowser.search(StubMediaBrowserServiceCompat.SEARCH_QUERY_FOR_ERROR, null,
-                    mSearchCallback);
-            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
-            assertTrue(mSearchCallback.mOnSearchResult);
-            assertNull(mSearchCallback.mSearchResults);
-            assertEquals(null, mSearchCallback.mSearchExtras);
-
-            mSearchCallback.reset();
-            Bundle extras = new Bundle();
-            extras.putString(key, val);
-            mMediaBrowser.search(StubMediaBrowserServiceCompat.SEARCH_QUERY, extras,
-                    mSearchCallback);
-            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
-            assertTrue(mSearchCallback.mOnSearchResult);
-            assertNotNull(mSearchCallback.mSearchResults);
-            for (MediaItem item : mSearchCallback.mSearchResults) {
-                assertNotNull(item.getMediaId());
-                assertTrue(item.getMediaId().contains(StubMediaBrowserServiceCompat.SEARCH_QUERY));
-            }
-            assertNotNull(mSearchCallback.mSearchExtras);
-            assertEquals(val, mSearchCallback.mSearchExtras.getString(key));
-        }
-    }
-
-    @Test
-    @SmallTest
-    public void testSendCustomAction() throws Exception {
-        synchronized (mWaitLock) {
-            CustomActionCallback callback = new CustomActionCallback();
-            Bundle extras = new Bundle();
-            extras.putString(TEST_KEY_1, TEST_VALUE_1);
-            mMediaBrowser.sendCustomAction(StubMediaBrowserServiceCompat.CUSTOM_ACTION, extras,
-                    callback);
-            new PollingCheck(TIME_OUT_MS) {
-                @Override
-                protected boolean check() {
-                    return mMediaBrowserService.mCustomActionResult != null;
-                }
-            }.run();
-            assertNotNull(mMediaBrowserService.mCustomActionResult);
-            assertNotNull(mMediaBrowserService.mCustomActionExtras);
-            assertEquals(TEST_VALUE_1,
-                    mMediaBrowserService.mCustomActionExtras.getString(TEST_KEY_1));
-
-            callback.reset();
-            Bundle bundle1 = new Bundle();
-            bundle1.putString(TEST_KEY_2, TEST_VALUE_2);
-            mMediaBrowserService.mCustomActionResult.sendProgressUpdate(bundle1);
-            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
-            assertTrue(callback.mOnProgressUpdateCalled);
-            assertNotNull(callback.mExtras);
-            assertEquals(TEST_VALUE_1, callback.mExtras.getString(TEST_KEY_1));
-            assertNotNull(callback.mData);
-            assertEquals(TEST_VALUE_2, callback.mData.getString(TEST_KEY_2));
-
-            callback.reset();
-            Bundle bundle2 = new Bundle();
-            bundle2.putString(TEST_KEY_3, TEST_VALUE_3);
-            mMediaBrowserService.mCustomActionResult.sendProgressUpdate(bundle2);
-            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
-            assertTrue(callback.mOnProgressUpdateCalled);
-            assertNotNull(callback.mExtras);
-            assertEquals(TEST_VALUE_1, callback.mExtras.getString(TEST_KEY_1));
-            assertNotNull(callback.mData);
-            assertEquals(TEST_VALUE_3, callback.mData.getString(TEST_KEY_3));
-
-            Bundle bundle3 = new Bundle();
-            bundle3.putString(TEST_KEY_4, TEST_VALUE_4);
-            callback.reset();
-            mMediaBrowserService.mCustomActionResult.sendResult(bundle3);
-            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
-            assertTrue(callback.mOnResultCalled);
-            assertNotNull(callback.mExtras);
-            assertEquals(TEST_VALUE_1, callback.mExtras.getString(TEST_KEY_1));
-            assertNotNull(callback.mData);
-            assertEquals(TEST_VALUE_4, callback.mData.getString(TEST_KEY_4));
-        }
-    }
-
-    @Test
-    @SmallTest
-    public void testSendCustomActionWithDetachedError() throws Exception {
-        synchronized (mWaitLock) {
-            CustomActionCallback callback = new CustomActionCallback();
-            Bundle extras = new Bundle();
-            extras.putString(TEST_KEY_1, TEST_VALUE_1);
-            mMediaBrowser.sendCustomAction(StubMediaBrowserServiceCompat.CUSTOM_ACTION, extras,
-                    callback);
-            new PollingCheck(TIME_OUT_MS) {
-                @Override
-                protected boolean check() {
-                    return mMediaBrowserService.mCustomActionResult != null;
-                }
-            }.run();
-            assertNotNull(mMediaBrowserService.mCustomActionResult);
-            assertNotNull(mMediaBrowserService.mCustomActionExtras);
-            assertEquals(TEST_VALUE_1,
-                    mMediaBrowserService.mCustomActionExtras.getString(TEST_KEY_1));
-
-            callback.reset();
-            Bundle bundle1 = new Bundle();
-            bundle1.putString(TEST_KEY_2, TEST_VALUE_2);
-            mMediaBrowserService.mCustomActionResult.sendProgressUpdate(bundle1);
-            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
-            assertTrue(callback.mOnProgressUpdateCalled);
-            assertNotNull(callback.mExtras);
-            assertEquals(TEST_VALUE_1, callback.mExtras.getString(TEST_KEY_1));
-            assertNotNull(callback.mData);
-            assertEquals(TEST_VALUE_2, callback.mData.getString(TEST_KEY_2));
-
-            callback.reset();
-            Bundle bundle2 = new Bundle();
-            bundle2.putString(TEST_KEY_3, TEST_VALUE_3);
-            mMediaBrowserService.mCustomActionResult.sendError(bundle2);
-            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
-            assertTrue(callback.mOnErrorCalled);
-            assertNotNull(callback.mExtras);
-            assertEquals(TEST_VALUE_1, callback.mExtras.getString(TEST_KEY_1));
-            assertNotNull(callback.mData);
-            assertEquals(TEST_VALUE_3, callback.mData.getString(TEST_KEY_3));
-        }
-    }
-
-    @Test
-    @SmallTest
-    public void testSendCustomActionWithNullCallback() throws Exception {
-        Bundle extras = new Bundle();
-        extras.putString(TEST_KEY_1, TEST_VALUE_1);
-        mMediaBrowser.sendCustomAction(StubMediaBrowserServiceCompat.CUSTOM_ACTION, extras, null);
-        new PollingCheck(TIME_OUT_MS) {
-            @Override
-            protected boolean check() {
-                return mMediaBrowserService.mCustomActionResult != null;
-            }
-        }.run();
-        assertNotNull(mMediaBrowserService.mCustomActionResult);
-        assertNotNull(mMediaBrowserService.mCustomActionExtras);
-        assertEquals(TEST_VALUE_1, mMediaBrowserService.mCustomActionExtras.getString(TEST_KEY_1));
-    }
-
-    @Test
-    @SmallTest
-    public void testSendCustomActionWithError() throws Exception {
-        synchronized (mWaitLock) {
-            CustomActionCallback callback = new CustomActionCallback();
-            mMediaBrowser.sendCustomAction(StubMediaBrowserServiceCompat.CUSTOM_ACTION_FOR_ERROR,
-                    null, callback);
-            new PollingCheck(TIME_OUT_MS) {
-                @Override
-                protected boolean check() {
-                    return mMediaBrowserService.mCustomActionResult != null;
-                }
-            }.run();
-            assertNotNull(mMediaBrowserService.mCustomActionResult);
-            assertNull(mMediaBrowserService.mCustomActionExtras);
-            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
-            assertTrue(callback.mOnErrorCalled);
-        }
-    }
-
-    @Test
-    @SmallTest
-    public void testBrowserRoot() {
-        final String id = "test-id";
-        final String key = "test-key";
-        final String val = "test-val";
-        final Bundle extras = new Bundle();
-        extras.putString(key, val);
-
-        MediaBrowserServiceCompat.BrowserRoot browserRoot =
-                new MediaBrowserServiceCompat.BrowserRoot(id, extras);
-        assertEquals(id, browserRoot.getRootId());
-        assertEquals(val, browserRoot.getExtras().getString(key));
-    }
-
-    @Test
-    @SmallTest
-    public void testDelayedSetSessionToken() throws Exception {
-        // This test has no meaning in API 21. The framework MediaBrowserService just connects to
-        // the media browser without waiting setMediaSession() to be called.
-        if (Build.VERSION.SDK_INT == 21) {
-            return;
-        }
-        final ConnectionCallbackForDelayedMediaSession callback =
-                new ConnectionCallbackForDelayedMediaSession();
-
-        getInstrumentation().runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                mMediaBrowserForDelayedMediaSession =
-                        new MediaBrowserCompat(getInstrumentation().getTargetContext(),
-                                TEST_BROWSER_SERVICE_DELAYED_MEDIA_SESSION, callback, null);
-            }
-        });
-
-        synchronized (mWaitLock) {
-            mMediaBrowserForDelayedMediaSession.connect();
-            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
-            assertEquals(0, callback.mConnectedCount);
-
-            StubMediaBrowserServiceCompatWithDelayedMediaSession.sInstance.callSetSessionToken();
-            mWaitLock.wait(TIME_OUT_MS);
-            assertEquals(1, callback.mConnectedCount);
-
-            if (Build.VERSION.SDK_INT >= 21) {
-                assertNotNull(
-                        mMediaBrowserForDelayedMediaSession.getSessionToken().getExtraBinder());
-            }
-        }
-    }
-
-    private void assertRootHints(MediaItem item) {
-        Bundle rootHints = item.getDescription().getExtras();
-        assertNotNull(rootHints);
-        assertEquals(mRootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT),
-                rootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT));
-        assertEquals(mRootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_OFFLINE),
-                rootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_OFFLINE));
-        assertEquals(mRootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_SUGGESTED),
-                rootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_SUGGESTED));
-    }
-
-    private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
-        @Override
-        public void onConnected() {
-            synchronized (mWaitLock) {
-                mMediaBrowserService = StubMediaBrowserServiceCompat.sInstance;
-                mWaitLock.notify();
-            }
-        }
-    }
-
-    private class SubscriptionCallback extends MediaBrowserCompat.SubscriptionCallback {
-        boolean mOnChildrenLoaded;
-        boolean mOnChildrenLoadedWithOptions;
-
-        @Override
-        public void onChildrenLoaded(String parentId, List<MediaItem> children) {
-            synchronized (mWaitLock) {
-                mOnChildrenLoaded = true;
-                if (children != null) {
-                    for (MediaItem item : children) {
-                        assertRootHints(item);
-                    }
-                }
-                mWaitLock.notify();
-            }
-        }
-
-        @Override
-        public void onChildrenLoaded(String parentId, List<MediaItem> children, Bundle options) {
-            synchronized (mWaitLock) {
-                mOnChildrenLoadedWithOptions = true;
-                if (children != null) {
-                    for (MediaItem item : children) {
-                        assertRootHints(item);
-                    }
-                }
-                mWaitLock.notify();
-            }
-        }
-
-        public void reset() {
-            mOnChildrenLoaded = false;
-            mOnChildrenLoadedWithOptions = false;
-        }
-    }
-
-    private class ItemCallback extends MediaBrowserCompat.ItemCallback {
-        boolean mOnItemLoaded;
-
-        @Override
-        public void onItemLoaded(MediaItem item) {
-            synchronized (mWaitLock) {
-                mOnItemLoaded = true;
-                assertRootHints(item);
-                mWaitLock.notify();
-            }
-        }
-
-        public void reset() {
-            mOnItemLoaded = false;
-        }
-    }
-
-    private class SearchCallback extends MediaBrowserCompat.SearchCallback {
-        boolean mOnSearchResult;
-        Bundle mSearchExtras;
-        List<MediaItem> mSearchResults;
-
-        @Override
-        public void onSearchResult(String query, Bundle extras, List<MediaItem> items) {
-            synchronized (mWaitLock) {
-                mOnSearchResult = true;
-                mSearchResults = items;
-                mSearchExtras = extras;
-                mWaitLock.notify();
-            }
-        }
-
-        @Override
-        public void onError(String query, Bundle extras) {
-            synchronized (mWaitLock) {
-                mOnSearchResult = true;
-                mSearchResults = null;
-                mSearchExtras = extras;
-                mWaitLock.notify();
-            }
-        }
-
-        public void reset() {
-            mOnSearchResult = false;
-            mSearchExtras = null;
-            mSearchResults = null;
-        }
-    }
-
-    private class CustomActionCallback extends MediaBrowserCompat.CustomActionCallback {
-        String mAction;
-        Bundle mExtras;
-        Bundle mData;
-        boolean mOnProgressUpdateCalled;
-        boolean mOnResultCalled;
-        boolean mOnErrorCalled;
-
-        @Override
-        public void onProgressUpdate(String action, Bundle extras, Bundle data) {
-            synchronized (mWaitLock) {
-                mOnProgressUpdateCalled = true;
-                mAction = action;
-                mExtras = extras;
-                mData = data;
-                mWaitLock.notify();
-            }
-        }
-
-        @Override
-        public void onResult(String action, Bundle extras, Bundle resultData) {
-            synchronized (mWaitLock) {
-                mOnResultCalled = true;
-                mAction = action;
-                mExtras = extras;
-                mData = resultData;
-                mWaitLock.notify();
-            }
-        }
-
-        @Override
-        public void onError(String action, Bundle extras, Bundle data) {
-            synchronized (mWaitLock) {
-                mOnErrorCalled = true;
-                mAction = action;
-                mExtras = extras;
-                mData = data;
-                mWaitLock.notify();
-            }
-        }
-
-        public void reset() {
-            mOnResultCalled = false;
-            mOnProgressUpdateCalled = false;
-            mOnErrorCalled = false;
-            mAction = null;
-            mExtras = null;
-            mData = null;
-        }
-    }
-
-    private class ConnectionCallbackForDelayedMediaSession extends
-            MediaBrowserCompat.ConnectionCallback {
-        private int mConnectedCount = 0;
-
-        @Override
-        public void onConnected() {
-            synchronized (mWaitLock) {
-                mConnectedCount++;
-                mWaitLock.notify();
-            }
-        }
-    }
-}
diff --git a/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompat.java b/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompat.java
deleted file mode 100644
index c817dce..0000000
--- a/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompat.java
+++ /dev/null
@@ -1,178 +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.support.v4.media;
-
-import android.os.Bundle;
-import android.support.v4.media.MediaBrowserCompat.MediaItem;
-import android.support.v4.media.session.MediaSessionCompat;
-
-import junit.framework.Assert;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Stub implementation of {@link android.support.v4.media.MediaBrowserServiceCompat}.
- */
-public class StubMediaBrowserServiceCompat extends MediaBrowserServiceCompat {
-    static final String EXTRAS_KEY = "test_extras_key";
-    static final String EXTRAS_VALUE = "test_extras_value";
-
-    static final String MEDIA_ID = "test_media_id";
-    static final String MEDIA_ID_INVALID = "test_media_id_invalid";
-    static final String MEDIA_ID_ROOT = "test_media_id_root";
-    static final String MEDIA_ID_CHILDREN_DELAYED = "test_media_id_children_delayed";
-    static final String MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED =
-            "test_media_id_on_load_item_not_implemented";
-
-    static final String[] MEDIA_ID_CHILDREN = new String[]{
-            "test_media_id_children_0", "test_media_id_children_1",
-            "test_media_id_children_2", "test_media_id_children_3",
-            MEDIA_ID_CHILDREN_DELAYED
-    };
-
-    static final String SEARCH_QUERY = "children_2";
-    static final String SEARCH_QUERY_FOR_NO_RESULT = "query no result";
-    static final String SEARCH_QUERY_FOR_ERROR = "query for error";
-
-    static final String CUSTOM_ACTION = "CUSTOM_ACTION";
-    static final String CUSTOM_ACTION_FOR_ERROR = "CUSTOM_ACTION_FOR_ERROR";
-
-    static StubMediaBrowserServiceCompat sInstance;
-
-    /* package private */ static MediaSessionCompat sSession;
-    private Bundle mExtras;
-    private Result<List<MediaItem>> mPendingLoadChildrenResult;
-    private Result<MediaItem> mPendingLoadItemResult;
-    private Bundle mPendingRootHints;
-
-    /* package private */ Bundle mCustomActionExtras;
-    /* package private */ Result<Bundle> mCustomActionResult;
-
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        sInstance = this;
-        sSession = new MediaSessionCompat(this, "StubMediaBrowserServiceCompat");
-        setSessionToken(sSession.getSessionToken());
-    }
-
-    @Override
-    public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
-        mExtras = new Bundle();
-        mExtras.putString(EXTRAS_KEY, EXTRAS_VALUE);
-        return new BrowserRoot(MEDIA_ID_ROOT, mExtras);
-    }
-
-    @Override
-    public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
-        List<MediaItem> mediaItems = new ArrayList<>();
-        if (MEDIA_ID_ROOT.equals(parentMediaId)) {
-            Bundle rootHints = getBrowserRootHints();
-            for (String id : MEDIA_ID_CHILDREN) {
-                mediaItems.add(createMediaItem(id));
-            }
-            result.sendResult(mediaItems);
-        } else if (MEDIA_ID_CHILDREN_DELAYED.equals(parentMediaId)) {
-            Assert.assertNull(mPendingLoadChildrenResult);
-            mPendingLoadChildrenResult = result;
-            mPendingRootHints = getBrowserRootHints();
-            result.detach();
-        } else if (MEDIA_ID_INVALID.equals(parentMediaId)) {
-            result.sendResult(null);
-        }
-    }
-
-    @Override
-    public void onLoadItem(String itemId, Result<MediaItem> result) {
-        if (MEDIA_ID_CHILDREN_DELAYED.equals(itemId)) {
-            mPendingLoadItemResult = result;
-            mPendingRootHints = getBrowserRootHints();
-            result.detach();
-            return;
-        }
-
-        if (MEDIA_ID_INVALID.equals(itemId)) {
-            result.sendResult(null);
-            return;
-        }
-
-        for (String id : MEDIA_ID_CHILDREN) {
-            if (id.equals(itemId)) {
-                result.sendResult(createMediaItem(id));
-                return;
-            }
-        }
-
-        // Test the case where onLoadItem is not implemented.
-        super.onLoadItem(itemId, result);
-    }
-
-    @Override
-    public void onSearch(String query, Bundle extras, Result<List<MediaItem>> result) {
-        if (SEARCH_QUERY_FOR_NO_RESULT.equals(query)) {
-            result.sendResult(Collections.<MediaItem>emptyList());
-        } else if (SEARCH_QUERY_FOR_ERROR.equals(query)) {
-            result.sendResult(null);
-        } else if (SEARCH_QUERY.equals(query)) {
-            List<MediaItem> items = new ArrayList<>();
-            for (String id : MEDIA_ID_CHILDREN) {
-                if (id.contains(query)) {
-                    items.add(createMediaItem(id));
-                }
-            }
-            result.sendResult(items);
-        }
-    }
-
-    @Override
-    public void onCustomAction(String action, Bundle extras,
-            Result<Bundle> result) {
-        mCustomActionResult = result;
-        mCustomActionExtras = extras;
-        if (CUSTOM_ACTION_FOR_ERROR.equals(action)) {
-            result.sendError(null);
-        } else if (CUSTOM_ACTION.equals(action)) {
-            result.detach();
-        }
-    }
-
-    public void sendDelayedNotifyChildrenChanged() {
-        if (mPendingLoadChildrenResult != null) {
-            mPendingLoadChildrenResult.sendResult(Collections.<MediaItem>emptyList());
-            mPendingRootHints = null;
-            mPendingLoadChildrenResult = null;
-        }
-    }
-
-    public void sendDelayedItemLoaded() {
-        if (mPendingLoadItemResult != null) {
-            mPendingLoadItemResult.sendResult(new MediaItem(new MediaDescriptionCompat.Builder()
-                    .setMediaId(MEDIA_ID_CHILDREN_DELAYED).setExtras(mPendingRootHints).build(),
-                    MediaItem.FLAG_BROWSABLE));
-            mPendingRootHints = null;
-            mPendingLoadItemResult = null;
-        }
-    }
-
-    private MediaItem createMediaItem(String id) {
-        return new MediaItem(new MediaDescriptionCompat.Builder()
-                .setMediaId(id).setExtras(getBrowserRootHints()).build(),
-                MediaItem.FLAG_BROWSABLE);
-    }
-}
diff --git a/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompatWithDelayedMediaSession.java b/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
deleted file mode 100644
index e93c940..0000000
--- a/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
+++ /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.support.v4.media;
-
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v4.media.session.MediaSessionCompat;
-
-import java.util.List;
-
-/**
- * Stub implementation of {@link MediaBrowserServiceCompat}.
- * This implementation does not call
- * {@link MediaBrowserServiceCompat#setSessionToken(MediaSessionCompat.Token)} in its
- * {@link android.app.Service#onCreate}.
- */
-public class StubMediaBrowserServiceCompatWithDelayedMediaSession extends
-        MediaBrowserServiceCompat {
-
-    static StubMediaBrowserServiceCompatWithDelayedMediaSession sInstance;
-    private MediaSessionCompat mSession;
-
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        sInstance = this;
-        mSession = new MediaSessionCompat(
-                this, "StubMediaBrowserServiceCompatWithDelayedMediaSession");
-    }
-
-    @Nullable
-    @Override
-    public BrowserRoot onGetRoot(@NonNull String clientPackageName,
-            int clientUid, @Nullable Bundle rootHints) {
-        return new BrowserRoot("StubRootId", null);
-    }
-
-    @Override
-    public void onLoadChildren(@NonNull String parentId,
-            @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
-        result.detach();
-    }
-
-    void callSetSessionToken() {
-        setSessionToken(mSession.getSessionToken());
-    }
-}
diff --git a/media-compat/tests/src/android/support/v4/media/StubRemoteMediaBrowserServiceCompat.java b/media-compat/tests/src/android/support/v4/media/StubRemoteMediaBrowserServiceCompat.java
deleted file mode 100644
index 8e03ab2..0000000
--- a/media-compat/tests/src/android/support/v4/media/StubRemoteMediaBrowserServiceCompat.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR 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.os.Bundle;
-import android.support.v4.media.MediaBrowserCompat.MediaItem;
-import android.support.v4.media.session.MediaSessionCompat;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Stub implementation of {@link android.support.v4.media.MediaBrowserServiceCompat}.
- */
-public class StubRemoteMediaBrowserServiceCompat extends MediaBrowserServiceCompat {
-    static final String EXTRAS_KEY = "test_extras_key";
-    static final String EXTRAS_VALUE = "test_extras_value";
-
-    static final String MEDIA_ID_ROOT = "test_media_id_root";
-    static final String MEDIA_METADATA = "test_media_metadata";
-
-    static final String[] MEDIA_ID_CHILDREN = new String[]{
-            "test_media_id_children_0", "test_media_id_children_1",
-            "test_media_id_children_2", "test_media_id_children_3"
-    };
-
-    private static MediaSessionCompat mSession;
-    private Bundle mExtras;
-
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        mSession = new MediaSessionCompat(this, "StubRemoteMediaBrowserServiceCompat");
-        setSessionToken(mSession.getSessionToken());
-    }
-
-    @Override
-    public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
-        mExtras = new Bundle();
-        mExtras.putString(EXTRAS_KEY, EXTRAS_VALUE);
-        return new BrowserRoot(MEDIA_ID_ROOT, mExtras);
-    }
-
-    @Override
-    public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
-        List<MediaItem> mediaItems = new ArrayList<>();
-        if (MEDIA_ID_ROOT.equals(parentMediaId)) {
-            Bundle rootHints = getBrowserRootHints();
-            for (String id : MEDIA_ID_CHILDREN) {
-                mediaItems.add(createMediaItem(id));
-            }
-            result.sendResult(mediaItems);
-        }
-    }
-
-    @Override
-    public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result,
-            final Bundle options) {
-        MediaMetadataCompat metadata = options.getParcelable(MEDIA_METADATA);
-        if (metadata == null) {
-            super.onLoadChildren(parentMediaId, result, options);
-        } else {
-            List<MediaItem> mediaItems = new ArrayList<>();
-            mediaItems.add(new MediaItem(metadata.getDescription(), MediaItem.FLAG_PLAYABLE));
-            result.sendResult(mediaItems);
-        }
-    }
-
-    private MediaItem createMediaItem(String id) {
-        return new MediaItem(new MediaDescriptionCompat.Builder()
-                .setMediaId(id).setExtras(getBrowserRootHints()).build(),
-                MediaItem.FLAG_BROWSABLE);
-    }
-}
diff --git a/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java b/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
index 31bdb7a..7aaee60 100644
--- a/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
+++ b/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
@@ -112,6 +112,8 @@
                     "android.support.mediacompat.service.test",
                     "android.support.mediacompat.service"
                             + ".StubMediaBrowserServiceCompatWithDelayedMediaSession");
+    private static final ComponentName TEST_INVALID_BROWSER_SERVICE = new ComponentName(
+            "invalid.package", "invalid.ServiceClassName");
 
     private String mServiceVersion;
     private MediaBrowserCompat mMediaBrowser;
@@ -157,6 +159,21 @@
 
     @Test
     @SmallTest
+    public void testBrowserRoot() {
+        final String id = "test-id";
+        final String key = "test-key";
+        final String val = "test-val";
+        final Bundle extras = new Bundle();
+        extras.putString(key, val);
+
+        MediaBrowserServiceCompat.BrowserRoot browserRoot =
+                new MediaBrowserServiceCompat.BrowserRoot(id, extras);
+        assertEquals(id, browserRoot.getRootId());
+        assertEquals(val, browserRoot.getExtras().getString(key));
+    }
+
+    @Test
+    @SmallTest
     public void testMediaBrowser() throws Exception {
         assertFalse(mMediaBrowser.isConnected());
 
@@ -178,6 +195,37 @@
 
     @Test
     @SmallTest
+    public void testGetServiceComponentBeforeConnection() {
+        try {
+            ComponentName serviceComponent = mMediaBrowser.getServiceComponent();
+            fail();
+        } catch (IllegalStateException e) {
+            // expected
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testConnectionFailed() throws Exception {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
+                        TEST_INVALID_BROWSER_SERVICE, mConnectionCallback, mRootHints);
+            }
+        });
+
+        synchronized (mConnectionCallback.mWaitLock) {
+            mMediaBrowser.connect();
+            mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
+        }
+        assertTrue(mConnectionCallback.mConnectionFailedCount > 0);
+        assertEquals(0, mConnectionCallback.mConnectedCount);
+        assertEquals(0, mConnectionCallback.mConnectionSuspendedCount);
+    }
+
+    @Test
+    @SmallTest
     public void testConnectTwice() throws Exception {
         connectMediaBrowserService();
         try {
@@ -259,8 +307,8 @@
         assertEquals(0, mConnectionCallback.mConnectionSuspendedCount);
     }
 
-//    @Test
-//    @MediumTest
+    @Test
+    @MediumTest
     public void testSubscribe() throws Exception {
         connectMediaBrowserService();
 
@@ -299,8 +347,8 @@
         assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
     }
 
-//    @Test
-//    @MediumTest
+    @Test
+    @MediumTest
     public void testSubscribeWithOptions() throws Exception {
         connectMediaBrowserService();
         final int pageSize = 3;
@@ -448,8 +496,8 @@
         }
     }
 
-//    @Test
-//    @MediumTest
+    @Test
+    @MediumTest
     public void testUnsubscribeWithSubscriptionCallbackForMultipleSubscriptions() throws Exception {
         connectMediaBrowserService();
         final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
@@ -611,6 +659,7 @@
             customActionExtras.putString(TEST_KEY_1, TEST_VALUE_1);
             mMediaBrowser.sendCustomAction(
                     CUSTOM_ACTION, customActionExtras, mCustomActionCallback);
+            mCustomActionCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
 
             mCustomActionCallback.reset();
             Bundle data1 = new Bundle();
@@ -701,6 +750,8 @@
         Bundle customActionExtras = new Bundle();
         customActionExtras.putString(TEST_KEY_1, TEST_VALUE_1);
         mMediaBrowser.sendCustomAction(CUSTOM_ACTION, customActionExtras, null);
+        // Wait some time so that the service can get a result receiver for the custom action.
+        Thread.sleep(WAIT_TIME_FOR_NO_RESPONSE_MS);
 
         // These calls should not make any exceptions.
         callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_PROGRESS_UPDATE, new Bundle(),
diff --git a/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java b/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java
index 79993ef..5dac1b6 100644
--- a/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java
+++ b/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java
@@ -55,9 +55,11 @@
 import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_RATING;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import android.app.PendingIntent;
 import android.content.ComponentName;
@@ -84,8 +86,6 @@
 import android.support.v4.media.session.PlaybackStateCompat;
 import android.util.Log;
 
-import junit.framework.Assert;
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -119,6 +119,7 @@
     private MediaBrowserCompat mMediaBrowser;
     private ConnectionCallback mConnectionCallback = new ConnectionCallback();
 
+    private MediaSessionCompat.Token mSessionToken;
     private MediaControllerCompat mController;
     private MediaControllerCallback mMediaControllerCallback = new MediaControllerCallback();
 
@@ -140,11 +141,11 @@
             mMediaBrowser.connect();
             mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
             if (!mMediaBrowser.isConnected()) {
-                Assert.fail("Browser failed to connect!");
+                fail("Browser failed to connect!");
             }
         }
-        mController =
-                new MediaControllerCompat(getTargetContext(), mMediaBrowser.getSessionToken());
+        mSessionToken = mMediaBrowser.getSessionToken();
+        mController = new MediaControllerCompat(getTargetContext(), mSessionToken);
         mController.registerCallback(mMediaControllerCallback, mHandler);
     }
 
@@ -541,6 +542,36 @@
         }.run();
     }
 
+    @Test
+    @SmallTest
+    public void testSessionReady() throws Exception {
+        if (android.os.Build.VERSION.SDK_INT < 21) {
+            return;
+        }
+
+        final MediaSessionCompat.Token tokenWithoutExtraBinder =
+                MediaSessionCompat.Token.fromToken(mSessionToken.getToken());
+
+        final MediaControllerCallback callback = new MediaControllerCallback();
+        synchronized (mWaitLock) {
+            getInstrumentation().runOnMainSync(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        MediaControllerCompat controller = new MediaControllerCompat(
+                                getInstrumentation().getTargetContext(), tokenWithoutExtraBinder);
+                        controller.registerCallback(callback, new Handler());
+                        assertFalse(controller.isSessionReady());
+                    } catch (Exception e) {
+                        fail();
+                    }
+                }
+            });
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(callback.mOnSessionReadyCalled);
+        }
+    }
+
     private void assertQueueEquals(List<QueueItem> expected, List<QueueItem> observed) {
         if (expected == null || observed == null) {
             assertTrue(expected == observed);
@@ -570,6 +601,7 @@
         private volatile boolean mOnCaptioningEnabledChangedCalled;
         private volatile boolean mOnRepeatModeChangedCalled;
         private volatile boolean mOnShuffleModeChangedCalled;
+        private volatile boolean mOnSessionReadyCalled;
 
         private volatile PlaybackStateCompat mPlaybackState;
         private volatile MediaMetadataCompat mMediaMetadata;
@@ -703,6 +735,14 @@
                 mWaitLock.notify();
             }
         }
+
+        @Override
+        public void onSessionReady() {
+            synchronized (mWaitLock) {
+                mOnSessionReadyCalled = true;
+                mWaitLock.notify();
+            }
+        }
     }
 
     private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
diff --git a/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java b/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
index 31bdb7a..7aaee60 100644
--- a/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
+++ b/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
@@ -112,6 +112,8 @@
                     "android.support.mediacompat.service.test",
                     "android.support.mediacompat.service"
                             + ".StubMediaBrowserServiceCompatWithDelayedMediaSession");
+    private static final ComponentName TEST_INVALID_BROWSER_SERVICE = new ComponentName(
+            "invalid.package", "invalid.ServiceClassName");
 
     private String mServiceVersion;
     private MediaBrowserCompat mMediaBrowser;
@@ -157,6 +159,21 @@
 
     @Test
     @SmallTest
+    public void testBrowserRoot() {
+        final String id = "test-id";
+        final String key = "test-key";
+        final String val = "test-val";
+        final Bundle extras = new Bundle();
+        extras.putString(key, val);
+
+        MediaBrowserServiceCompat.BrowserRoot browserRoot =
+                new MediaBrowserServiceCompat.BrowserRoot(id, extras);
+        assertEquals(id, browserRoot.getRootId());
+        assertEquals(val, browserRoot.getExtras().getString(key));
+    }
+
+    @Test
+    @SmallTest
     public void testMediaBrowser() throws Exception {
         assertFalse(mMediaBrowser.isConnected());
 
@@ -178,6 +195,37 @@
 
     @Test
     @SmallTest
+    public void testGetServiceComponentBeforeConnection() {
+        try {
+            ComponentName serviceComponent = mMediaBrowser.getServiceComponent();
+            fail();
+        } catch (IllegalStateException e) {
+            // expected
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testConnectionFailed() throws Exception {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
+                        TEST_INVALID_BROWSER_SERVICE, mConnectionCallback, mRootHints);
+            }
+        });
+
+        synchronized (mConnectionCallback.mWaitLock) {
+            mMediaBrowser.connect();
+            mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
+        }
+        assertTrue(mConnectionCallback.mConnectionFailedCount > 0);
+        assertEquals(0, mConnectionCallback.mConnectedCount);
+        assertEquals(0, mConnectionCallback.mConnectionSuspendedCount);
+    }
+
+    @Test
+    @SmallTest
     public void testConnectTwice() throws Exception {
         connectMediaBrowserService();
         try {
@@ -259,8 +307,8 @@
         assertEquals(0, mConnectionCallback.mConnectionSuspendedCount);
     }
 
-//    @Test
-//    @MediumTest
+    @Test
+    @MediumTest
     public void testSubscribe() throws Exception {
         connectMediaBrowserService();
 
@@ -299,8 +347,8 @@
         assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
     }
 
-//    @Test
-//    @MediumTest
+    @Test
+    @MediumTest
     public void testSubscribeWithOptions() throws Exception {
         connectMediaBrowserService();
         final int pageSize = 3;
@@ -448,8 +496,8 @@
         }
     }
 
-//    @Test
-//    @MediumTest
+    @Test
+    @MediumTest
     public void testUnsubscribeWithSubscriptionCallbackForMultipleSubscriptions() throws Exception {
         connectMediaBrowserService();
         final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
@@ -611,6 +659,7 @@
             customActionExtras.putString(TEST_KEY_1, TEST_VALUE_1);
             mMediaBrowser.sendCustomAction(
                     CUSTOM_ACTION, customActionExtras, mCustomActionCallback);
+            mCustomActionCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
 
             mCustomActionCallback.reset();
             Bundle data1 = new Bundle();
@@ -701,6 +750,8 @@
         Bundle customActionExtras = new Bundle();
         customActionExtras.putString(TEST_KEY_1, TEST_VALUE_1);
         mMediaBrowser.sendCustomAction(CUSTOM_ACTION, customActionExtras, null);
+        // Wait some time so that the service can get a result receiver for the custom action.
+        Thread.sleep(WAIT_TIME_FOR_NO_RESPONSE_MS);
 
         // These calls should not make any exceptions.
         callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_PROGRESS_UPDATE, new Bundle(),
diff --git a/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java b/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java
index 79993ef..4466721 100644
--- a/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java
+++ b/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java
@@ -55,9 +55,11 @@
 import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_RATING;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import android.app.PendingIntent;
 import android.content.ComponentName;
@@ -84,8 +86,6 @@
 import android.support.v4.media.session.PlaybackStateCompat;
 import android.util.Log;
 
-import junit.framework.Assert;
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -119,6 +119,7 @@
     private MediaBrowserCompat mMediaBrowser;
     private ConnectionCallback mConnectionCallback = new ConnectionCallback();
 
+    private MediaSessionCompat.Token mSessionToken;
     private MediaControllerCompat mController;
     private MediaControllerCallback mMediaControllerCallback = new MediaControllerCallback();
 
@@ -140,11 +141,11 @@
             mMediaBrowser.connect();
             mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
             if (!mMediaBrowser.isConnected()) {
-                Assert.fail("Browser failed to connect!");
+                fail("Browser failed to connect!");
             }
         }
-        mController =
-                new MediaControllerCompat(getTargetContext(), mMediaBrowser.getSessionToken());
+        mSessionToken = mMediaBrowser.getSessionToken();
+        mController = new MediaControllerCompat(getTargetContext(), mSessionToken);
         mController.registerCallback(mMediaControllerCallback, mHandler);
     }
 
@@ -487,7 +488,7 @@
                         && info.getMaxVolume() == TEST_MAX_VOLUME
                         && info.getVolumeControl() == VolumeProviderCompat.VOLUME_CONTROL_FIXED
                         && info.getPlaybackType()
-                                == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
+                        == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
                     break;
                 }
             }
@@ -541,6 +542,36 @@
         }.run();
     }
 
+    @Test
+    @SmallTest
+    public void testSessionReady() throws Exception {
+        if (android.os.Build.VERSION.SDK_INT < 21) {
+            return;
+        }
+
+        final MediaSessionCompat.Token tokenWithoutExtraBinder =
+                MediaSessionCompat.Token.fromToken(mSessionToken.getToken());
+
+        final MediaControllerCallback callback = new MediaControllerCallback();
+        synchronized (mWaitLock) {
+            getInstrumentation().runOnMainSync(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        MediaControllerCompat controller = new MediaControllerCompat(
+                                getInstrumentation().getTargetContext(), tokenWithoutExtraBinder);
+                        controller.registerCallback(callback, new Handler());
+                        assertFalse(controller.isSessionReady());
+                    } catch (Exception e) {
+                        fail();
+                    }
+                }
+            });
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(callback.mOnSessionReadyCalled);
+        }
+    }
+
     private void assertQueueEquals(List<QueueItem> expected, List<QueueItem> observed) {
         if (expected == null || observed == null) {
             assertTrue(expected == observed);
@@ -570,6 +601,7 @@
         private volatile boolean mOnCaptioningEnabledChangedCalled;
         private volatile boolean mOnRepeatModeChangedCalled;
         private volatile boolean mOnShuffleModeChangedCalled;
+        private volatile boolean mOnSessionReadyCalled;
 
         private volatile PlaybackStateCompat mPlaybackState;
         private volatile MediaMetadataCompat mMediaMetadata;
@@ -703,6 +735,14 @@
                 mWaitLock.notify();
             }
         }
+
+        @Override
+        public void onSessionReady() {
+            synchronized (mWaitLock) {
+                mOnSessionReadyCalled = true;
+                mWaitLock.notify();
+            }
+        }
     }
 
     private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
diff --git a/media-compat/version-compat-tests/runtest.sh b/media-compat/version-compat-tests/runtest.sh
new file mode 100755
index 0000000..d1a3c3a
--- /dev/null
+++ b/media-compat/version-compat-tests/runtest.sh
@@ -0,0 +1,105 @@
+#!/bin/bash
+# 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.
+
+# A script that runs media-compat-test between different versions.
+#
+# Preconditions:
+#  - Exactly one test device should be connected.
+#
+# TODO:
+#  - The test result should be easily seen. (Can we report the results to the Sponge?)
+#  - Run specific combination of the test (e.g. Only want to test ToT-ToT)
+#  - Run specific test class / method by using argument.
+#  - Support simultaneous multiple device connection
+
+# Usage './runtest.sh'
+
+CLIENT_MODULE_NAME_BASE="support-media-compat-test-client"
+SERVICE_MODULE_NAME_BASE="support-media-compat-test-service"
+CLIENT_VERSION=""
+SERVICE_VERSION=""
+
+function runTest() {
+  echo "Running test: Client-$CLIENT_VERSION / Service-$SERVICE_VERSION"
+
+  local CLIENT_MODULE_NAME="$CLIENT_MODULE_NAME_BASE$([ "$CLIENT_VERSION" = "tot" ] || echo "-previous")"
+  local SERVICE_MODULE_NAME="$SERVICE_MODULE_NAME_BASE$([ "$SERVICE_VERSION" = "tot" ] || echo "-previous")"
+
+  # Build test apks
+  ./gradlew $CLIENT_MODULE_NAME:assembleDebugAndroidTest || (echo "Build failed. Aborting."; return 1)
+  ./gradlew $SERVICE_MODULE_NAME:assembleDebugAndroidTest || (echo "Build failed. Aborting."; return 1)
+
+  # Install the apks
+  adb install -r -d "../../out/dist/$CLIENT_MODULE_NAME.apk" || (echo "Apk installation failed. Aborting."; return 1)
+  adb install -r -d "../../out/dist/$SERVICE_MODULE_NAME.apk" || (echo "Apk installation failed. Aborting."; return 1)
+
+  # Run the tests
+  echo ">>>>>>>>>>>>>>>>>>>>>>>> Test Started: Client-$CLIENT_VERSION & Service-$SERVICE_VERSION <<<<<<<<<<<<<<<<<<<<<<<<"
+  adb shell am instrument -w -r -e package android.support.mediacompat.client -e debug false -e client_version $CLIENT_VERSION \
+     -e service_version $SERVICE_VERSION android.support.mediacompat.client.test/android.support.test.runner.AndroidJUnitRunner
+  adb shell am instrument -w -r -e package android.support.mediacompat.service -e debug false -e client_version $CLIENT_VERSION \
+     -e service_version $SERVICE_VERSION android.support.mediacompat.service.test/android.support.test.runner.AndroidJUnitRunner
+  echo ">>>>>>>>>>>>>>>>>>>>>>>> Test Ended: Client-$CLIENT_VERSION & Service-$SERVICE_VERSION <<<<<<<<<<<<<<<<<<<<<<<<<<"
+}
+
+
+OLD_PWD=$(pwd)
+if [[ $OLD_PWD != *"frameworks/support"* ]]; then
+  echo "Current working directory is" $OLD_PWD.
+  echo "Please re-run this script in any folder under frameworks/support."
+  exit 1;
+else
+  # Change working directory to frameworks/support
+  cd "$(echo $OLD_PWD | awk -F'frameworks/support' '{print $1}')"/frameworks/support
+fi
+
+echo "Choose the support library versions of the test you want to run:"
+echo "    1. Client-ToT             / Service-ToT"
+echo "    2. Client-ToT             / Service-Latest release"
+echo "    3. Client-Latest release  / Service-ToT"
+echo "    4. Run all of the above"
+printf "Pick one of them: "
+
+read ANSWER
+case $ANSWER in
+  1)
+     CLIENT_VERSION="tot"
+     SERVICE_VERSION="tot"
+     runTest
+     ;;
+  2)
+     CLIENT_VERSION="tot"
+     SERVICE_VERSION="previous"
+     runTest
+     ;;
+  3)
+     CLIENT_VERSION="previous"
+     SERVICE_VERSION="tot"
+     runTest
+     ;;
+  4)
+     CLIENT_VERSION="tot"
+     SERVICE_VERSION="tot"
+     runTest
+
+     CLIENT_VERSION="tot"
+     SERVICE_VERSION="previous"
+     runTest
+
+     CLIENT_VERSION="previous"
+     SERVICE_VERSION="tot"
+     runTest
+     ;;
+esac
diff --git a/paging/common/src/main/java/android/arch/paging/BoundedDataSource.java b/paging/common/src/main/java/android/arch/paging/BoundedDataSource.java
deleted file mode 100644
index 0656490..0000000
--- a/paging/common/src/main/java/android/arch/paging/BoundedDataSource.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.paging;
-
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.WorkerThread;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Simplest data source form that provides all of its data through a single loadRange() method.
- * <p>
- * Requires that your data resides in positions <code>0</code> through <code>N</code>, where
- * <code>N</code> is the value returned from {@link #countItems()}. You must return the exact number
- * requested, so that the data as returned can be safely prepended/appended to what has already
- * been loaded.
- * <p>
- * For more flexibility in how many items to load, or to avoid counting your data source, override
- * {@link PositionalDataSource} directly.
- *
- * @param <Value> Value type returned by the data source.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public abstract class BoundedDataSource<Value> extends PositionalDataSource<Value> {
-    /**
-     * Called to load items at from the specified position range.
-     *
-     * @param startPosition Index of first item to load.
-     * @param loadCount     Exact number of items to load. Returning a different number will cause
-     *                      an exception to be thrown.
-     * @return List of loaded items. Null if the BoundedDataSource is no longer valid, and should
-     *         not be queried again.
-     */
-    @WorkerThread
-    @Nullable
-    public abstract List<Value> loadRange(int startPosition, int loadCount);
-
-    @WorkerThread
-    @Nullable
-    @Override
-    public List<Value> loadAfter(int startIndex, int pageSize) {
-        return loadRange(startIndex, pageSize);
-    }
-
-    @WorkerThread
-    @Nullable
-    @Override
-    public List<Value> loadBefore(int startIndex, int pageSize) {
-        if (startIndex < 0) {
-            return new ArrayList<>();
-        }
-        int loadSize = Math.min(pageSize, startIndex + 1);
-        startIndex = startIndex - loadSize + 1;
-        List<Value> result = loadRange(startIndex, loadSize);
-        if (result != null) {
-            if (result.size() != loadSize) {
-                throw new IllegalStateException("invalid number of items returned.");
-            }
-        }
-        return result;
-    }
-}
diff --git a/paging/common/src/main/java/android/arch/paging/ContiguousDataSource.java b/paging/common/src/main/java/android/arch/paging/ContiguousDataSource.java
index 414c4ff..03f2e86 100644
--- a/paging/common/src/main/java/android/arch/paging/ContiguousDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/ContiguousDataSource.java
@@ -19,44 +19,21 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 
-import java.util.List;
-
 abstract class ContiguousDataSource<Key, Value> extends DataSource<Key, Value> {
     @Override
     boolean isContiguous() {
         return true;
     }
 
-    abstract void loadInitial(Key key, int initialLoadSize, boolean enablePlaceholders,
-            @NonNull PageResult.Receiver<Key, Value> receiver);
+    public abstract void loadInitial(@Nullable Key key, int initialLoadSize,
+            boolean enablePlaceholders,
+            @NonNull InitialLoadCallback<Value> callback);
 
-    void loadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize,
-            @NonNull PageResult.Receiver<Key, Value> receiver) {
-        if (!isInvalid()) {
-            List<Value> list = loadAfterImpl(currentEndIndex, currentEndItem, pageSize);
+    abstract void loadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize,
+            @NonNull LoadCallback<Value> callback);
 
-            if (list != null && !isInvalid()) {
-                receiver.postOnPageResult(new PageResult<>(
-                        PageResult.APPEND, new Page<Key, Value>(list), 0, 0, 0));
-                return;
-            }
-        }
-        receiver.postOnPageResult(new PageResult<Key, Value>(PageResult.APPEND));
-    }
-
-    void loadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, int pageSize,
-            @NonNull PageResult.Receiver<Key, Value> receiver) {
-        if (!isInvalid()) {
-            List<Value> list = loadBeforeImpl(currentBeginIndex, currentBeginItem, pageSize);
-
-            if (list != null && !isInvalid()) {
-                receiver.postOnPageResult(new PageResult<>(
-                        PageResult.PREPEND, new Page<Key, Value>(list), 0, 0, 0));
-                return;
-            }
-        }
-        receiver.postOnPageResult(new PageResult<Key, Value>(PageResult.PREPEND));
-    }
+    abstract void loadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, int pageSize,
+            @NonNull LoadCallback<Value> callback);
 
     /**
      * Get the key from either the position, or item, or null if position/item invalid.
@@ -65,12 +42,4 @@
      * that isn't yet loaded, a fallback item (last loaded item accessed) will be passed.
      */
     abstract Key getKey(int position, Value item);
-
-    @Nullable
-    abstract List<Value> loadAfterImpl(int currentEndIndex,
-            @NonNull Value currentEndItem, int pageSize);
-
-    @Nullable
-    abstract List<Value> loadBeforeImpl(int currentBeginIndex,
-            @NonNull Value currentBeginItem, int pageSize);
 }
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 cdff391..302263d 100644
--- a/paging/common/src/main/java/android/arch/paging/ContiguousPagedList.java
+++ b/paging/common/src/main/java/android/arch/paging/ContiguousPagedList.java
@@ -21,6 +21,7 @@
 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 {
@@ -31,28 +32,14 @@
     private int mPrependItemsRequested = 0;
     private int mAppendItemsRequested = 0;
 
-    @SuppressWarnings("unchecked")
-    private final PagedStorage<K, V> mKeyedStorage = (PagedStorage<K, V>) mStorage;
-
-    private final PageResult.Receiver<K, V> mReceiver = new PageResult.Receiver<K, V>() {
-        @AnyThread
-        @Override
-        public void postOnPageResult(@NonNull final PageResult<K, V> pageResult) {
-            // NOTE: if we're already on main thread, this can delay page receive by a frame
-            mMainThreadExecutor.execute(new Runnable() {
-                @Override
-                public void run() {
-                    onPageResult(pageResult);
-                }
-            });
-        }
-
+    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(@NonNull PageResult<K, V> pageResult) {
-            if (pageResult.page == null) {
+        public void onPageResult(@PageResult.ResultType int resultType,
+                @NonNull PageResult<V> pageResult) {
+            if (pageResult.isInvalid()) {
                 detach();
                 return;
             }
@@ -62,25 +49,30 @@
                 return;
             }
 
-            Page<K, V> page = pageResult.page;
-            if (pageResult.type == PageResult.INIT) {
-                mKeyedStorage.init(pageResult.leadingNulls, page, pageResult.trailingNulls,
+            List<V> page = pageResult.page;
+            if (resultType == PageResult.INIT) {
+                mStorage.init(pageResult.leadingNulls, page, pageResult.trailingNulls,
                         pageResult.positionOffset, ContiguousPagedList.this);
-                notifyInserted(0, mKeyedStorage.size());
-            } else if (pageResult.type == PageResult.APPEND) {
-                mKeyedStorage.appendPage(page, ContiguousPagedList.this);
-            } else if (pageResult.type == PageResult.PREPEND) {
-                mKeyedStorage.prependPage(page, ContiguousPagedList.this);
+
+                // notifyInserted is safe here, since if we're not on main thread, we won't have any
+                // Callbacks registered to listen to the notify.
+                notifyInserted(0, mStorage.size());
+
+                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);
             }
 
             if (mBoundaryCallback != null) {
                 boolean deferEmpty = mStorage.size() == 0;
                 boolean deferBegin = !deferEmpty
-                        && pageResult.type == PageResult.PREPEND
-                        && pageResult.page.items.size() == 0;
+                        && resultType == PageResult.PREPEND
+                        && pageResult.page.size() == 0;
                 boolean deferEnd = !deferEmpty
-                        && pageResult.type == PageResult.APPEND
-                        && pageResult.page.items.size() == 0;
+                        && resultType == PageResult.APPEND
+                        && pageResult.page.size() == 0;
                 deferBoundaryCallbacks(deferEmpty, deferBegin, deferEnd);
             }
         }
@@ -93,24 +85,36 @@
             @Nullable BoundaryCallback<V> boundaryCallback,
             @NonNull Config config,
             final @Nullable K key) {
-        super(new PagedStorage<K, V>(), mainThreadExecutor, backgroundThreadExecutor,
+        super(new PagedStorage<V>(), mainThreadExecutor, backgroundThreadExecutor,
                 boundaryCallback, config);
         mDataSource = dataSource;
 
-        // blocking init just triggers the initial load on the construction thread -
-        // Could still be posted with callback, if desired.
-        mDataSource.loadInitial(key,
-                mConfig.initialLoadSizeHint,
-                mConfig.enablePlaceholders,
-                mReceiver);
+        if (mDataSource.isInvalid()) {
+            detach();
+        } else {
+            @DataSource.LoadCountType int type = mConfig.enablePlaceholders
+                    ? DataSource.LOAD_COUNT_ACCEPTED
+                    : DataSource.LOAD_COUNT_PREVENTED;
+
+            DataSource.InitialLoadCallback<V> callback = new DataSource.InitialLoadCallback<>(
+                    type, mConfig.pageSize, mDataSource, mReceiver);
+            mDataSource.loadInitial(key,
+                    mConfig.initialLoadSizeHint,
+                    mConfig.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.setPostExecutor(mMainThreadExecutor);
+        }
     }
 
     @MainThread
     @Override
     void dispatchUpdatesSinceSnapshot(
             @NonNull PagedList<V> pagedListSnapshot, @NonNull Callback callback) {
-
-        final PagedStorage<?, V> snapshot = pagedListSnapshot.mStorage;
+        final PagedStorage<V> snapshot = pagedListSnapshot.mStorage;
 
         final int newlyAppended = mStorage.getNumberAppended() - snapshot.getNumberAppended();
         final int newlyPrepended = mStorage.getNumberPrepended() - snapshot.getNumberPrepended();
@@ -120,7 +124,8 @@
 
         // 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 (newlyAppended < 0
+        if (snapshot.isEmpty()
+                || newlyAppended < 0
                 || newlyPrepended < 0
                 || mStorage.getTrailingNullCount() != Math.max(previousTrailing - newlyAppended, 0)
                 || mStorage.getLeadingNullCount() != Math.max(previousLeading - newlyPrepended, 0)
@@ -190,7 +195,14 @@
                 if (isDetached()) {
                     return;
                 }
-                mDataSource.loadBefore(position, item, mConfig.pageSize, mReceiver);
+                if (mDataSource.isInvalid()) {
+                    detach();
+                } else {
+                    DataSource.LoadCallback<V> callback = new DataSource.LoadCallback<>(
+                            PageResult.PREPEND, mMainThreadExecutor, mDataSource, mReceiver);
+                    mDataSource.loadBefore(position, item, mConfig.pageSize, callback);
+                }
+
             }
         });
     }
@@ -213,7 +225,13 @@
                 if (isDetached()) {
                     return;
                 }
-                mDataSource.loadAfter(position, item, mConfig.pageSize, mReceiver);
+                if (mDataSource.isInvalid()) {
+                    detach();
+                } else {
+                    DataSource.LoadCallback<V> callback = new DataSource.LoadCallback<>(
+                            PageResult.APPEND, mMainThreadExecutor, mDataSource, mReceiver);
+                    mDataSource.loadAfter(position, item, mConfig.pageSize, callback);
+                }
             }
         });
     }
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 ff44521..2e41cf6 100644
--- a/paging/common/src/main/java/android/arch/paging/DataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/DataSource.java
@@ -16,30 +16,76 @@
 
 package android.arch.paging;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 import android.support.annotation.AnyThread;
+import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
 import android.support.annotation.WorkerThread;
 
+import java.lang.annotation.Retention;
+import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
- * Base class for incremental data loading, used in list paging. To implement, extend either the
- * {@link KeyedDataSource}, or {@link TiledDataSource} subclass.
+ * Base class for loading pages of snapshot data into a {@link PagedList}.
  * <p>
- * Choose based on whether each load operation is based on the position of the data in the list.
+ * 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.
  * <p>
- * Use {@link KeyedDataSource} if you need to use data from item <code>N-1</code> to load item
- * <code>N</code>. For example, if requesting the backend for the next comments in the list
+ * A PagedList / DataSource pair serve as a snapshot of the data set being loaded. 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 either the {@link KeyedDataSource}, or {@link PositionalDataSource}
+ * subclass. Choose based on whether each load operation is based on the position of the data in the
+ * list.
+ * <p>
+ * Use {@link KeyedDataSource} 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 TiledDataSource} if you can load arbitrary pages based solely on position information,
- * and can provide a fixed item count. TiledDataSource supports querying pages at arbitrary
- * positions, so can provide data to PagedLists in arbitrary order.
+ * Use {@link PositionalDataSource} if you can load arbitrary pages based solely on position
+ * information, and can provide a fixed item count. PositionalDataSource supports querying pages at
+ * arbitrary positions, so can provide data to PagedLists in arbitrary order.
  * <p>
- * Because a <code>null</code> item indicates a placeholder in {@link PagedList}, DataSource may not
- * return <code>null</code> items in lists that it loads. This is so that users of the PagedList
+ * 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.
@@ -47,8 +93,36 @@
  */
 @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&lt;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 interface Factory<Key, Value> {
+        /**
+         * Create a DataSource.
+         * <p>
+         * The DataSource should invalidate itself if the snapshot is no longer valid, and a new
+         * DataSource should be queried from the Factory.
+         *
+         * @return the new DataSource.
+         */
         DataSource<Key, Value> create();
     }
 
@@ -58,19 +132,206 @@
     }
 
     /**
-     * If returned by countItems(), indicates an undefined number of items are provided by the data
-     * source. Continued querying in either direction may continue to produce more data.
-     */
-    @SuppressWarnings("WeakerAccess")
-    public static int COUNT_UNDEFINED = -1;
-
-    /**
      * Returns true if the data source guaranteed to produce a contiguous set of items,
      * never producing gaps.
      */
     abstract boolean isContiguous();
 
     /**
+     * Callback for DataSource initial loading methods to return data and position/count
+     * information.
+     * <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 <T> Type of items being loaded.
+     */
+    public static class InitialLoadCallback<T> extends LoadCallback<T> {
+        private final int mPageSize;
+
+        InitialLoadCallback(@LoadCountType int countType, int pageSize,
+                DataSource dataSource, PageResult.Receiver<T> receiver) {
+            super(PageResult.INIT, countType, dataSource, receiver);
+            mPageSize = pageSize;
+            if (mPageSize < 1) {
+                throw new IllegalArgumentException("Page size must be non-negative");
+            }
+        }
+
+        /**
+         * 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.
+         *
+         * @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, relative to the total
+         *                 count. 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 void onResult(@NonNull List<T> 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.");
+            }
+            if (mCountType == LOAD_COUNT_REQUIRED_TILED
+                    && 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.");
+            }
+
+            int trailingUnloadedCount = totalCount - position - data.size();
+            if (mCountType == LOAD_COUNT_REQUIRED_TILED || mCountType == LOAD_COUNT_ACCEPTED) {
+                dispatchResultToReceiver(new PageResult<>(
+                        data, position, trailingUnloadedCount, 0));
+            } else {
+                dispatchResultToReceiver(new PageResult<>(data, position));
+            }
+        }
+
+        /**
+         * Called to pass initial load state from a DataSource without supporting placeholders.
+         * <p>
+         * Call this method from your DataSource's {@code loadInitial} function to return data,
+         * if position is known but total size is not. If counting is not expensive, consider
+         * calling 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}.
+         */
+        void onResult(@NonNull List<T> data, int position) {
+            // not counting, don't need to check mAcceptCount
+            dispatchResultToReceiver(new PageResult<>(
+                    data, 0, 0, position));
+        }
+    }
+
+    @Retention(SOURCE)
+    @IntDef({LOAD_COUNT_PREVENTED, LOAD_COUNT_ACCEPTED, LOAD_COUNT_REQUIRED_TILED})
+    @interface LoadCountType {}
+    static final int LOAD_COUNT_PREVENTED = 0;
+    static final int LOAD_COUNT_ACCEPTED = 1;
+    static final int LOAD_COUNT_REQUIRED_TILED = 2;
+
+    /**
+     * Callback for DataSource loading methods 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 <T> Type of items being loaded.
+     */
+    public static class LoadCallback<T> {
+        @PageResult.ResultType
+        final int mResultType;
+        @LoadCountType
+        final int mCountType;
+        private final DataSource mDataSource;
+        private final PageResult.Receiver<T> mReceiver;
+
+        private int mPositionOffset = 0;
+
+        // mSignalLock protects mPostExecutor, and mHasSignalled
+        private final Object mSignalLock = new Object();
+        private Executor mPostExecutor = null;
+        private boolean mHasSignalled = false;
+
+        private LoadCallback(@PageResult.ResultType int resultType, @LoadCountType int countType,
+                DataSource dataSource, PageResult.Receiver<T> receiver) {
+            mResultType = resultType;
+            mCountType = countType;
+            mDataSource = dataSource;
+            mReceiver = receiver;
+        }
+
+        LoadCallback(int type, Executor mainThreadExecutor,
+                DataSource dataSource, PageResult.Receiver<T> receiver) {
+            mResultType = type;
+            mCountType = LOAD_COUNT_PREVENTED;
+            mPostExecutor = mainThreadExecutor;
+            mDataSource = dataSource;
+            mReceiver = receiver;
+        }
+
+        void setPositionOffset(int positionOffset) {
+            mPositionOffset = positionOffset;
+        }
+
+        void setPostExecutor(Executor postExecutor) {
+            synchronized (mSignalLock) {
+                mPostExecutor = postExecutor;
+            }
+        }
+
+        /**
+         * Called to pass loaded data from a DataSource.
+         * <p>
+         * Call this method from your DataSource's {@code load} methods to return data.
+         *
+         * @param data List of items loaded from the DataSource.
+         */
+        public void onResult(@NonNull List<T> data) {
+            if (mCountType == LOAD_COUNT_REQUIRED_TILED && !data.isEmpty()) {
+                throw new IllegalArgumentException(
+                        "PositionalDataSource requires calling the three argument version of"
+                                + " InitialLoadCallback.onResult() to pass position information");
+            }
+            dispatchResultToReceiver(new PageResult<>(
+                    data, 0, 0, mPositionOffset));
+        }
+
+        void dispatchResultToReceiver(final @NonNull PageResult<T> result) {
+            Executor executor;
+            synchronized (mSignalLock) {
+                if (mHasSignalled) {
+                    throw new IllegalStateException(
+                            "LoadCallback already dispatched, cannot dispatch again.");
+                }
+                mHasSignalled = true;
+                executor = mPostExecutor;
+            }
+
+            final PageResult<T> resolvedResult =
+                    mDataSource.isInvalid() ? PageResult.<T>getInvalidResult() : result;
+
+            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
diff --git a/paging/common/src/main/java/android/arch/paging/KeyedDataSource.java b/paging/common/src/main/java/android/arch/paging/KeyedDataSource.java
index 3214a4e..b6656f3 100644
--- a/paging/common/src/main/java/android/arch/paging/KeyedDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/KeyedDataSource.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * 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.
@@ -16,223 +16,127 @@
 
 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.ArrayList;
-import java.util.Collections;
 import java.util.List;
-
 /**
  * Incremental data loader for paging keyed content, where loaded content uses previously loaded
  * items as input to future loads.
  * <p>
- * Implement a DataSource using KeyedDataSource if you need to use data from item <code>N-1</code>
- * to load item <code>N</code>. This is common, for example, in sorted database queries where
+ * Implement a DataSource using KeyedDataSource 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>
- * A compute usage pattern with Room SQL queries would look like this (though note, Room plans to
- * provide generation of much of this code in the future):
- * <pre>
- * {@literal @}Dao
- * interface UserDao {
- *     {@literal @}Query("SELECT * from user ORDER BY name DESC LIMIT :limit")
- *     public abstract List&lt;User> userNameInitial(int limit);
- *
- *     {@literal @}Query("SELECT * from user WHERE name &lt; :key ORDER BY name DESC LIMIT :limit")
- *     public abstract List&lt;User> userNameLoadAfter(String key, int limit);
- *
- *     {@literal @}Query("SELECT * from user WHERE name > :key ORDER BY name ASC LIMIT :limit")
- *     public abstract List&lt;User> userNameLoadBefore(String key, int limit);
- * }
- *
- * public class KeyedUserQueryDataSource extends KeyedDataSource&lt;String, User> {
- *     private MyDatabase mDb;
- *     private final UserDao mUserDao;
- *     {@literal @}SuppressWarnings("FieldCanBeLocal")
- *     private final InvalidationTracker.Observer mObserver;
- *
- *     public KeyedUserQueryDataSource(MyDatabase db) {
- *         mDb = db;
- *         mUserDao = db.getUserDao();
- *         mObserver = new InvalidationTracker.Observer("user") {
- *             {@literal @}Override
- *             public void onInvalidated({@literal @}NonNull Set&lt;String> tables) {
- *                 // the user table has been invalidated, invalidate the DataSource
- *                 invalidate();
- *             }
- *         };
- *         db.getInvalidationTracker().addWeakObserver(mObserver);
- *     }
- *
- *     {@literal @}Override
- *     public boolean isInvalid() {
- *         mDb.getInvalidationTracker().refreshVersionsSync();
- *         return super.isInvalid();
- *     }
- *
- *     {@literal @}Override
- *     public String getKey({@literal @}NonNull User item) {
- *         return item.getName();
- *     }
- *
- *     {@literal @}Override
- *     public List&lt;User> loadInitial(int pageSize) {
- *         return mUserDao.userNameInitial(pageSize);
- *     }
- *
- *     {@literal @}Override
- *     public List&lt;User> loadBefore({@literal @}NonNull String userName, int pageSize) {
- *         // Return items adjacent to 'userName' in reverse order
- *         // it's valid to return a different-sized list of items than pageSize, if it's easier
- *         return mUserDao.userNameLoadBefore(userName, pageSize);
- *     }
- *
- *     {@literal @}Override
- *     public List&lt;User> loadAfter({@literal @}Nullable String userName, int pageSize) {
- *         // Return items adjacent to 'userName'
- *         // it's valid to return a different-sized list of items than pageSize, if it's easier
- *         return mUserDao.userNameLoadAfter(userName, pageSize);
- *     }
- * }</pre>
  *
  * @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 KeyedDataSource<Key, Value> extends ContiguousDataSource<Key, Value> {
-
-    @Nullable
     @Override
-    List<Value> loadAfterImpl(int currentEndIndex, @NonNull Value currentEndItem, int pageSize) {
-        return loadAfter(getKey(currentEndItem), pageSize);
+    final void loadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize,
+            @NonNull LoadCallback<Value> callback) {
+        loadAfter(getKey(currentEndItem), pageSize, callback);
+    }
+
+    @Override
+    final void loadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, int pageSize,
+            @NonNull LoadCallback<Value> callback) {
+        loadBefore(getKey(currentBeginItem), pageSize, callback);
     }
 
     @Nullable
     @Override
-    List<Value> loadBeforeImpl(
-            int currentBeginIndex, @NonNull Value currentBeginItem, int pageSize) {
-        List<Value> list = loadBefore(getKey(currentBeginItem), pageSize);
-
-        if (list != null && list.size() > 1) {
-            // TODO: move out of keyed entirely, into the DB DataSource.
-            list = new ArrayList<>(list);
-            Collections.reverse(list);
+    final Key getKey(int position, Value item) {
+        if (item == null) {
+            return null;
         }
-        return list;
+
+        return getKey(item);
     }
 
 
-    @Override
-    void loadInitial(Key key, int initialLoadSize, boolean enablePlaceholders,
-            @NonNull PageResult.Receiver<Key, Value> receiver) {
-
-        PageResult<Key, Value> pageResult =
-                loadInitialInternal(key, initialLoadSize, enablePlaceholders);
-        if (pageResult == null) {
-            // loading failed, return empty page
-            receiver.onPageResult(new PageResult<Key, Value>(PageResult.INIT));
-        } else {
-            receiver.onPageResult(pageResult);
-        }
-    }
-
     /**
-     * Try initial load, and either return the successful initial load to the receiver,
-     * or null if unsuccessful.
+     * 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 DataSource.InitialLoadCallback#onResult(List, int, int)}. This enables PagedLists
+     * presenting data from this source to display placeholders to represent unloaded items.
+     * <p>
+     * {@code initialLoadKey} and {@code requestedLoadSize} are hints, not requirements, so if it is
+     * difficult or impossible to respect them, they may be altered. Note that ignoring the
+     * {@code initialLoadKey} 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 initialLoadKey Load items around this key, or at the beginning of the data set if null
+     *                       is passed.
+     * @param requestedLoadSize Suggested number of items to load.
+     * @param enablePlaceholders Signals whether counting is requested. If false, you can
+     *                           potentially save work by calling the single-parameter variant of
+     *                           {@link DataSource.LoadCallback#onResult(List)} and not counting the
+     *                           number of items in the data set.
+     * @param callback DataSource.LoadCallback that receives initial load data.
      */
-    @Nullable
-    private PageResult<Key, Value> loadInitialInternal(
-            @Nullable Key key, int initialLoadSize, boolean enablePlaceholders) {
-        // check if invalid at beginning, and before returning a valid list
-        if (isInvalid()) {
-            return null;
-        }
+    @Override
+    public abstract void loadInitial(@Nullable Key initialLoadKey, int requestedLoadSize,
+            boolean enablePlaceholders, @NonNull InitialLoadCallback<Value> callback);
 
-        List<Value> list;
-        if (key == null) {
-            // no key, so load initial.
-            list = loadInitial(initialLoadSize);
-            if (list == null) {
-                return null;
-            }
-        } else {
-            List<Value> after = loadAfter(key, initialLoadSize / 2);
-            if (after == null) {
-                return null;
-            }
+    /**
+     * Load list data after the specified item.
+     * <p>
+     * It's valid to return a different list size than the page size, if it's easier for this data
+     * source. It is generally safer to increase the number loaded than reduce.
+     * <p>
+     * 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 currentEndKey Load items after this key. May be null on initial load, to indicate load
+     *                      from beginning.
+     * @param pageSize Suggested number of items to load.
+     * @param callback DataSource.LoadCallback that receives loaded data.
+     */
+    public abstract void loadAfter(@NonNull Key currentEndKey, int pageSize,
+            @NonNull LoadCallback<Value> callback);
 
-            Key loadBeforeKey = after.isEmpty() ? key : getKey(after.get(0));
-            List<Value> before = loadBefore(loadBeforeKey, initialLoadSize / 2);
-            if (before == null) {
-                return null;
-            }
-            if (!after.isEmpty() || !before.isEmpty()) {
-                // one of the lists has data
-                if (after.isEmpty()) {
-                    // retry loading after, since it may be that the key passed points to the end of
-                    // the list, so we need to load after the last item in the before list
-                    after = loadAfter(getKey(before.get(0)), initialLoadSize / 2);
-                    if (after == null) {
-                        return null;
-                    }
-                }
-                // assemble full list
-                list = new ArrayList<>();
-                list.addAll(before);
-                // Note - we reverse the list instead of before, in case before is immutable
-                Collections.reverse(list);
-                list.addAll(after);
-            } else {
-                // load before(key) and load after(key) failed - try load initial to be *sure* we
-                // catch the case where there's only one item, which is loaded by the key case
-                list = loadInitial(initialLoadSize);
-                if (list == null) {
-                    return null;
-                }
-            }
-        }
-
-        final Page<Key, Value> page = new Page<>(list);
-
-        if (list.isEmpty()) {
-            if (isInvalid()) {
-                return null;
-            }
-            // wasn't able to load any items, but not invalid - return an empty page.
-            return new PageResult<>(PageResult.INIT, page, 0, 0, 0);
-        }
-
-        int itemsBefore = COUNT_UNDEFINED;
-        int itemsAfter = COUNT_UNDEFINED;
-        if (enablePlaceholders) {
-            itemsBefore = countItemsBefore(getKey(list.get(0)));
-            itemsAfter = countItemsAfter(getKey(list.get(list.size() - 1)));
-        }
-
-        if (isInvalid()) {
-            return null;
-        }
-        if (itemsBefore == COUNT_UNDEFINED || itemsAfter == COUNT_UNDEFINED) {
-            itemsBefore = 0;
-            itemsAfter = 0;
-        }
-        return new PageResult<>(
-                PageResult.INIT,
-                page,
-                itemsBefore,
-                itemsAfter,
-                0);
-    }
+    /**
+     * Load data before the currently loaded content.
+     * <p>
+     * It's valid to return a different list size than the page size, if it's easier for this data
+     * source. It is generally safer to increase the number loaded than reduce. Note that the last
+     * item returned must be directly adjacent to the key passed, so varying size from the pageSize
+     * requested should effectively grow or shrink the list by modifying the beginning, not the end.
+     * <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.
+     * <p class="note"><strong>Note:</strong> Data must be returned in the order it will be
+     * presented in the list.
+     *
+     * @param currentBeginKey Load items before this key.
+     * @param pageSize Suggested number of items to load.
+     * @param callback DataSource.LoadCallback that receives loaded data.
+     */
+    public abstract void loadBefore(@NonNull Key currentBeginKey, int pageSize,
+            @NonNull LoadCallback<Value> callback);
 
     /**
      * Return a key associated with the given item.
      * <p>
      * If your KeyedDataSource is loading from a source that is sorted and loaded by a unique
      * integer ID, you would return {@code item.getID()} here. This key can then be passed to
-     * {@link #loadBefore(Key, int)} or {@link #loadAfter(Key, int)} to load additional items
-     * adjacent to the item passed to this function.
+     * {@link #loadBefore(Object, int, LoadCallback)} or
+     * {@link #loadAfter(Object, int, 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,
@@ -243,101 +147,5 @@
      * @return Key associated with given item.
      */
     @NonNull
-    @AnyThread
     public abstract Key getKey(@NonNull Value item);
-
-    /**
-     * Return the number of items that occur before the item uniquely identified by {@code key} in
-     * the data set.
-     * <p>
-     * For example, if you're loading items sorted by ID, then this would return the total number of
-     * items with ID less than {@code key}.
-     * <p>
-     * If you return {@link #COUNT_UNDEFINED} here, or from {@link #countItemsAfter(Key)}, your
-     * data source will not present placeholder null items in place of unloaded data.
-     *
-     * @param key A unique identifier of an item in the data set.
-     * @return Number of items in the data set before the item identified by {@code key}, or
-     *         {@link #COUNT_UNDEFINED}.
-     *
-     * @see #countItemsAfter(Key)
-     */
-    @WorkerThread
-    public int countItemsBefore(@NonNull Key key) {
-        return COUNT_UNDEFINED;
-    }
-
-    /**
-     * Return the number of items that occur after the item uniquely identified by {@code key} in
-     * the data set.
-     * <p>
-     * For example, if you're loading items sorted by ID, then this would return the total number of
-     * items with ID greater than {@code key}.
-     * <p>
-     * If you return {@link #COUNT_UNDEFINED} here, or from {@link #countItemsBefore(Key)}, your
-     * data source will not present placeholder null items in place of unloaded data.
-     *
-     * @param key A unique identifier of an item in the data set.
-     * @return Number of items in the data set after the item identified by {@code key}, or
-     *         {@link #COUNT_UNDEFINED}.
-     *
-     * @see #countItemsBefore(Key)
-     */
-    @WorkerThread
-    public int countItemsAfter(@NonNull Key key) {
-        return COUNT_UNDEFINED;
-    }
-
-    @WorkerThread
-    @Nullable
-    public abstract List<Value> loadInitial(int pageSize);
-
-    /**
-     * Load list data after the specified item.
-     * <p>
-     * It's valid to return a different list size than the page size, if it's easier for this data
-     * source. It is generally safer to increase the number loaded than reduce.
-     *
-     * @param currentEndKey Load items after this key. May be null on initial load, to indicate load
-     *                      from beginning.
-     * @param pageSize      Suggested number of items to load.
-     * @return List of items, starting after the specified item. Null if the data source is
-     * no longer valid, and should not be queried again.
-     */
-    @SuppressWarnings("WeakerAccess")
-    @WorkerThread
-    @Nullable
-    public abstract List<Value> loadAfter(@NonNull Key currentEndKey, int pageSize);
-
-    /**
-     * Load data before the currently loaded content, starting at the provided index,
-     * in reverse-display order.
-     * <p>
-     * It's valid to return a different list size than the page size, if it's easier for this data
-     * source. It is generally safer to increase the number loaded than reduce.
-     * <p class="note"><strong>Note:</strong> Items returned from loadBefore <em>must</em> be in
-     * reverse order from how they will be presented in the list. The first item in the return list
-     * will be prepended immediately before the current beginning of the list. This is so that the
-     * KeyedDataSource may return a different number of items from the requested {@code pageSize} by
-     * shortening or lengthening the return list as it desires.
-     * <p>
-     *
-     * @param currentBeginKey Load items before this key.
-     * @param pageSize         Suggested number of items to load.
-     * @return List of items, in descending order, starting after the specified item. Null if the
-     * data source is no longer valid, and should not be queried again.
-     */
-    @SuppressWarnings("WeakerAccess")
-    @WorkerThread
-    @Nullable
-    public abstract List<Value> loadBefore(@NonNull Key currentBeginKey, int pageSize);
-
-    @Nullable
-    @Override
-    Key getKey(int position, Value item) {
-        if (item == null) {
-            return null;
-        }
-        return getKey(item);
-    }
 }
diff --git a/paging/common/src/main/java/android/arch/paging/ListDataSource.java b/paging/common/src/main/java/android/arch/paging/ListDataSource.java
index d3a171e..b6f366a 100644
--- a/paging/common/src/main/java/android/arch/paging/ListDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/ListDataSource.java
@@ -16,10 +16,12 @@
 
 package android.arch.paging;
 
+import android.support.annotation.NonNull;
+
 import java.util.ArrayList;
 import java.util.List;
 
-public class ListDataSource<T> extends TiledDataSource<T> {
+class ListDataSource<T> extends PositionalDataSource<T> {
     private final List<T> mList;
 
     public ListDataSource(List<T> list) {
@@ -27,13 +29,22 @@
     }
 
     @Override
-    public int countItems() {
-        return mList.size();
+    public void loadInitial(int requestedStartPosition, int requestedLoadSize, int pageSize,
+            @NonNull InitialLoadCallback<T> callback) {
+        final int totalCount = mList.size();
+
+        final int firstLoadPosition = computeFirstLoadPosition(
+                requestedStartPosition, requestedLoadSize, pageSize, totalCount);
+        final int firstLoadSize = Math.min(totalCount - firstLoadPosition, requestedLoadSize);
+
+        // for simplicity, we could return everything immediately,
+        // but we tile here since it's expected behavior
+        List<T> sublist = mList.subList(firstLoadPosition, firstLoadPosition + firstLoadSize);
+        callback.onResult(sublist, firstLoadPosition, totalCount);
     }
 
     @Override
-    public List<T> loadRange(int startPosition, int count) {
-        int endExclusive = Math.min(mList.size(), startPosition + count);
-        return mList.subList(startPosition, endExclusive);
+    public void loadRange(int startPosition, int count, @NonNull LoadCallback<T> callback) {
+        callback.onResult(mList.subList(startPosition, startPosition + count));
     }
 }
diff --git a/paging/common/src/main/java/android/arch/paging/Page.java b/paging/common/src/main/java/android/arch/paging/Page.java
deleted file mode 100644
index e9890ed..0000000
--- a/paging/common/src/main/java/android/arch/paging/Page.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 android.support.annotation.Nullable;
-
-import java.util.List;
-
-/**
- * Immutable class representing a page of data loaded from a DataSource.
- * <p>
- * Optionally stores before/after keys for cases where they cannot be computed, but the DataSource
- * can provide them as part of loading a page.
- * <p>
- * A page's list must never be modified.
- */
-class Page<K, V> {
-    @SuppressWarnings("WeakerAccess")
-    @Nullable
-    public final K beforeKey;
-    @NonNull
-    public final List<V> items;
-    @SuppressWarnings("WeakerAccess")
-    @Nullable
-    public K afterKey;
-
-    Page(@NonNull List<V> items) {
-        this(null, items, null);
-    }
-
-    Page(@Nullable K beforeKey, @NonNull List<V> items, @Nullable K afterKey) {
-        this.beforeKey = beforeKey;
-        this.items = items;
-        this.afterKey = afterKey;
-    }
-}
diff --git a/paging/common/src/main/java/android/arch/paging/PageResult.java b/paging/common/src/main/java/android/arch/paging/PageResult.java
index 55d5fb7..cf2216f 100644
--- a/paging/common/src/main/java/android/arch/paging/PageResult.java
+++ b/paging/common/src/main/java/android/arch/paging/PageResult.java
@@ -16,11 +16,31 @@
 
 package android.arch.paging;
 
-import android.support.annotation.AnyThread;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.support.annotation.IntDef;
 import android.support.annotation.MainThread;
 import android.support.annotation.NonNull;
 
-class PageResult<K, V> {
+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
@@ -30,8 +50,8 @@
     // non-contiguous, tile result
     static final int TILE = 3;
 
-    public final int type;
-    public final Page<K, V> page;
+    @NonNull
+    public final List<T> page;
     @SuppressWarnings("WeakerAccess")
     public final int leadingNulls;
     @SuppressWarnings("WeakerAccess")
@@ -39,26 +59,34 @@
     @SuppressWarnings("WeakerAccess")
     public final int positionOffset;
 
-    PageResult(int type, Page<K, V> page, int leadingNulls, int trailingNulls, int positionOffset) {
-        this.type = type;
-        this.page = page;
+    PageResult(@NonNull List<T> list, int leadingNulls, int trailingNulls, int positionOffset) {
+        this.page = list;
         this.leadingNulls = leadingNulls;
         this.trailingNulls = trailingNulls;
         this.positionOffset = positionOffset;
     }
 
-    PageResult(int type) {
-        this.type = type;
-        this.page = null;
+    PageResult(@NonNull List<T> list, int positionOffset) {
+        this.page = list;
         this.leadingNulls = 0;
         this.trailingNulls = 0;
-        this.positionOffset = 0;
+        this.positionOffset = positionOffset;
     }
 
-    interface Receiver<K, V> {
-        @AnyThread
-        void postOnPageResult(@NonNull PageResult<K, V> pageResult);
+    @Override
+    public String toString() {
+        return "Result " + leadingNulls
+                + ", " + page
+                + ", " + trailingNulls
+                + ", offset " + positionOffset;
+    }
+
+    public boolean isInvalid() {
+        return this == INVALID_RESULT;
+    }
+
+    abstract static class Receiver<T> {
         @MainThread
-        void onPageResult(@NonNull PageResult<K, V> pageResult);
+        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
index f18e108..70d4075 100644
--- a/paging/common/src/main/java/android/arch/paging/PagedList.java
+++ b/paging/common/src/main/java/android/arch/paging/PagedList.java
@@ -17,6 +17,7 @@
 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;
@@ -51,7 +52,7 @@
  * PagedList can present data for an unbounded, infinite scrolling list, or a very large but
  * countable list. Use {@link Config} to control how many items a PagedList loads, and when.
  * <p>
- * If you use {@link LivePagedListProvider} to get a
+ * If you use {@link LivePagedListBuilder} to get a
  * {@link android.arch.lifecycle.LiveData}&lt;PagedList>, it will initialize PagedLists on a
  * background thread for you.
  * <h4>Placeholders</h4>
@@ -88,9 +89,8 @@
  * </ul>
  * <p>
  * Placeholders are enabled by default, but can be disabled in two ways. They are disabled if the
- * DataSource returns {@link DataSource#COUNT_UNDEFINED} from any item counting method, or if
- * {@code false} is passed to {@link Config.Builder#setEnablePlaceholders(boolean)} when building a
- * {@link Config}.
+ * 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}.
  *
  * @param <T> The type of the entries in the list.
  */
@@ -104,7 +104,7 @@
     @NonNull
     final Config mConfig;
     @NonNull
-    final PagedStorage<?, T> mStorage;
+    final PagedStorage<T> mStorage;
 
     int mLastLoad = 0;
     T mLastItem = null;
@@ -123,7 +123,7 @@
 
     protected final ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();
 
-    PagedList(@NonNull PagedStorage<?, T> storage,
+    PagedList(@NonNull PagedStorage<T> storage,
             @NonNull Executor mainThreadExecutor,
             @NonNull Executor backgroundThreadExecutor,
             @Nullable BoundaryCallback<T> boundaryCallback,
@@ -162,7 +162,8 @@
         if (dataSource.isContiguous() || !config.enablePlaceholders) {
             if (!dataSource.isContiguous()) {
                 //noinspection unchecked
-                dataSource = (DataSource<K, T>) ((TiledDataSource<T>) dataSource).getAsContiguous();
+                dataSource = (DataSource<K, T>) ((PositionalDataSource<T>) dataSource)
+                        .wrapAsContiguousWithoutPlaceholders();
             }
             ContiguousDataSource<K, T> contigDataSource = (ContiguousDataSource<K, T>) dataSource;
             return new ContiguousPagedList<>(contigDataSource,
@@ -172,7 +173,7 @@
                     config,
                     key);
         } else {
-            return new TiledPagedList<>((TiledDataSource<T>) dataSource,
+            return new TiledPagedList<>((PositionalDataSource<T>) dataSource,
                     mainThreadExecutor,
                     backgroundThreadExecutor,
                     boundaryCallback,
@@ -184,18 +185,14 @@
     /**
      * Builder class for PagedList.
      * <p>
-     * DataSource, main thread and background executor, and Config must all be provided.
+     * DataSource, Config, main thread and background executor must all be provided.
      * <p>
-     * A valid PagedList may not be constructed without data, so building a PagedList queries
-     * initial data from the data source. This is done because it's generally undesired to present a
-     * PagedList with no data in it to the UI. It's better to present initial data, so that the UI
-     * doesn't show an empty list, or placeholders for a few frames, just before showing initial
-     * content.
+     * 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>
-     * Because PagedLists are initialized with data, PagedLists must be built on a background
-     * thread.
-     * <p>
-     * {@link LivePagedListProvider} does this creation on a background thread automatically, if you
+     * {@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.
@@ -203,26 +200,48 @@
      */
     @SuppressWarnings("WeakerAccess")
     public static class Builder<Key, Value> {
-        private DataSource<Key, Value> mDataSource;
+        private final DataSource<Key, Value> mDataSource;
+        private final Config mConfig;
         private Executor mMainThreadExecutor;
         private Executor mBackgroundThreadExecutor;
         private BoundaryCallback mBoundaryCallback;
-        private Config mConfig;
         private Key mInitialKey;
 
         /**
-         * The source of data that the PagedList should load from.
-         * @param dataSource Source of data for the PagedList.
+         * Create a PagedList.Builder with the provided {@link DataSource} and {@link Config}.
          *
-         * @return this
+         * @param dataSource DataSource the PagedList will load from.
+         * @param config Config that defines how the PagedList loads data from its DataSource.
          */
-        @NonNull
-        public Builder<Key, Value> setDataSource(@NonNull DataSource<Key, Value> 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;
-            return this;
+            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.
@@ -250,6 +269,15 @@
             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) {
@@ -257,20 +285,6 @@
             return this;
         }
 
-
-        /**
-         * The Config defining how the PagedList should load from the DataSource.
-         *
-         * @param config The config that will define how the PagedList loads from the DataSource.
-         *
-         * @return this
-         */
-        @NonNull
-        public Builder<Key, Value> setConfig(@NonNull Config config) {
-            mConfig = config;
-            return this;
-        }
-
         /**
          * Sets the initial key the DataSource should load around as part of initialization.
          *
@@ -286,8 +300,21 @@
         /**
          * Creates a {@link PagedList} with the given parameters.
          * <p>
-         * This call will initial data and perform any counting needed to initialize the PagedList,
-         * therefore it should only be called on a worker thread.
+         * 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
@@ -300,18 +327,13 @@
         @WorkerThread
         @NonNull
         public PagedList<Value> build() {
-            if (mDataSource == null) {
-                throw new IllegalArgumentException("DataSource required");
-            }
+            // 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");
             }
-            if (mConfig == null) {
-                throw new IllegalArgumentException("Config required");
-            }
 
             //noinspection unchecked
             return PagedList.create(
@@ -451,13 +473,11 @@
         // safe to deref mBoundaryCallback here, since we only defer if mBoundaryCallback present
         if (begin) {
             //noinspection ConstantConditions
-            mBoundaryCallback.onItemAtFrontLoaded(
-                    snapshot(), mStorage.getFirstLoadedItem(), mStorage.size());
+            mBoundaryCallback.onItemAtFrontLoaded(mStorage.getFirstLoadedItem());
         }
         if (end) {
             //noinspection ConstantConditions
-            mBoundaryCallback.onItemAtEndLoaded(
-                    snapshot(), mStorage.getLastLoadedItem(), mStorage.size());
+            mBoundaryCallback.onItemAtEndLoaded(mStorage.getLastLoadedItem());
         }
     }
 
@@ -501,7 +521,6 @@
         if (isImmutable()) {
             return this;
         }
-
         return new SnapshotPagedList<>(this);
     }
 
@@ -522,7 +541,7 @@
      * <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 LivePagedListProvider}, it will do
+     * 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)}.
@@ -556,8 +575,8 @@
     /**
      * Position offset of the data in the list.
      * <p>
-     * If data is supplied by a {@link TiledDataSource}, the item returned from <code>get(i)</code>
-     * has a position of <code>i + getPositionOffset()</code>.
+     * 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 KeyedDataSource}, and thus doesn't use positions, returns 0.
      */
@@ -583,15 +602,25 @@
      * GC'd.
      *
      * @param previousSnapshot Snapshot previously captured from this List, or null.
-     * @param callback         Callback to dispatch to.
+     * @param callback         LoadCallback to dispatch to.
      * @see #removeWeakCallback(Callback)
      */
     @SuppressWarnings("WeakerAccess")
     public void addWeakCallback(@Nullable List<T> previousSnapshot, @NonNull Callback callback) {
         if (previousSnapshot != null && previousSnapshot != this) {
-            PagedList<T> storageSnapshot = (PagedList<T>) previousSnapshot;
-            //noinspection unchecked
-            dispatchUpdatesSinceSnapshot(storageSnapshot, callback);
+
+            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
@@ -608,7 +637,7 @@
     /**
      * Removes a previously added callback.
      *
-     * @param callback Callback, previously added.
+     * @param callback LoadCallback, previously added.
      * @see #addWeakCallback(List, Callback)
      */
     @SuppressWarnings("WeakerAccess")
@@ -645,6 +674,14 @@
         }
     }
 
+
+
+    /**
+     * Dispatch updates since the non-empty snapshot was taken.
+     *
+     * @param snapshot Non-empty snapshot.
+     * @param callback LoadCallback for updates that have occurred since snapshot.
+     */
     abstract void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> snapshot,
             @NonNull Callback callback);
 
@@ -826,13 +863,6 @@
              * 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>
-             * If used with a {@link TiledDataSource}, this value is rounded to the nearest number
-             * of pages, with a minimum of two pages, and loaded with a single call to
-             * {@link TiledDataSource#loadRange(int, int)}.
-             * <p>
-             * If used with a {@link KeyedDataSource}, this value will be passed to
-             * {@link KeyedDataSource#loadInitial(int)}.
-             * <p>
              * If not set, defaults to three times page size.
              *
              * @param initialLoadSizeHint Number of items to load while initializing the PagedList.
@@ -873,13 +903,43 @@
     }
 
     /**
-     * WIP API for load-more-into-local-storage callbacks
+     * Signals when a PagedList has reached the end of available data.
+     * <p>
+     * This can be used to implement paging from the network into a local database - when the
+     * database has no more data to present, a BoundaryCallback can be used to fetch more data.
+     * <p>
+     * If an instance is 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.
+     *
+     * @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 abstract void onZeroItemsLoaded();
-        public abstract void onItemAtFrontLoaded(@NonNull List<T> pagedListSnapshot,
-                @NonNull T itemAtFront, int pagedListSize);
-        public abstract void onItemAtEndLoaded(@NonNull List<T> pagedListSnapshot,
-                @NonNull T itemAtEnd, int pagedListSize);
+
+        /**
+         * 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 abstract 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 abstract 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
index b857462..d4531d3 100644
--- a/paging/common/src/main/java/android/arch/paging/PagedStorage.java
+++ b/paging/common/src/main/java/android/arch/paging/PagedStorage.java
@@ -17,13 +17,21 @@
 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.Collections;
 import java.util.List;
 
-final class PagedStorage<K, V> extends AbstractList<V> {
+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;
     /**
@@ -37,7 +45,7 @@
      * 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<Page<K, V>> mPages;
+    private final ArrayList<List<T>> mPages;
     private int mTrailingNullCount;
 
     private int mPositionOffset;
@@ -53,9 +61,6 @@
     private int mNumberPrepended;
     private int mNumberAppended;
 
-    // only used in tiling case
-    private Page<K, V> mPlaceholderPage;
-
     PagedStorage() {
         mLeadingNullCount = 0;
         mPages = new ArrayList<>();
@@ -67,12 +72,12 @@
         mNumberAppended = 0;
     }
 
-    PagedStorage(int leadingNulls, Page<K, V> page, int trailingNulls) {
+    PagedStorage(int leadingNulls, List<T> page, int trailingNulls) {
         this();
         init(leadingNulls, page, trailingNulls, 0);
     }
 
-    private PagedStorage(PagedStorage<K, V> other) {
+    private PagedStorage(PagedStorage<T> other) {
         mLeadingNullCount = other.mLeadingNullCount;
         mPages = new ArrayList<>(other.mPages);
         mTrailingNullCount = other.mTrailingNullCount;
@@ -81,40 +86,37 @@
         mPageSize = other.mPageSize;
         mNumberPrepended = other.mNumberPrepended;
         mNumberAppended = other.mNumberAppended;
-
-        // preserve placeholder page so we can locate placeholder pages if needed later
-        mPlaceholderPage = other.mPlaceholderPage;
     }
 
-    PagedStorage<K, V> snapshot() {
+    PagedStorage<T> snapshot() {
         return new PagedStorage<>(this);
     }
 
-    private void init(int leadingNulls, Page<K, V> page, int trailingNulls, int positionOffset) {
+    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.items.size();
+        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.items.size();
+        mPageSize = page.size();
 
         mNumberPrepended = 0;
         mNumberAppended = 0;
     }
 
-    void init(int leadingNulls, Page<K, V> page, int trailingNulls, int positionOffset,
+    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 V get(int i) {
+    public T get(int i) {
         if (i < 0 || i >= size()) {
             throw new IndexOutOfBoundsException("Index: " + i + ", Size: " + size());
         }
@@ -138,7 +140,7 @@
             pageInternalIndex = localIndex;
             final int localPageCount = mPages.size();
             for (localPageIndex = 0; localPageIndex < localPageCount; localPageIndex++) {
-                int pageSize = mPages.get(localPageIndex).items.size();
+                int pageSize = mPages.get(localPageIndex).size();
                 if (pageSize > pageInternalIndex) {
                     // stop, found the page
                     break;
@@ -147,12 +149,12 @@
             }
         }
 
-        Page<?, V> page = mPages.get(localPageIndex);
-        if (page == null || page.items.size() == 0) {
+        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.items.get(pageInternalIndex);
+        return page.get(pageInternalIndex);
     }
 
     /**
@@ -207,8 +209,8 @@
         int total = mLeadingNullCount;
         final int pageCount = mPages.size();
         for (int i = 0; i < pageCount; i++) {
-            Page page = mPages.get(i);
-            if (page != null && page != mPlaceholderPage) {
+            List page = mPages.get(i);
+            if (page != null && page != PLACEHOLDER_LIST) {
                 break;
             }
             total += mPageSize;
@@ -219,8 +221,8 @@
     int computeTrailingNulls() {
         int total = mTrailingNullCount;
         for (int i = mPages.size() - 1; i >= 0; i--) {
-            Page page = mPages.get(i);
-            if (page != null && page != mPlaceholderPage) {
+            List page = mPages.get(i);
+            if (page != null && page != PLACEHOLDER_LIST) {
                 break;
             }
             total += mPageSize;
@@ -230,21 +232,21 @@
 
     // ---------------- Contiguous API -------------------
 
-    V getFirstLoadedItem() {
+    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).items.get(0);
+        return mPages.get(0).get(0);
     }
 
-    V getLastLoadedItem() {
+    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
-        Page<K, V> page = mPages.get(mPages.size() - 1);
-        return page.items.get(page.items.size() - 1);
+        List<T> page = mPages.get(mPages.size() - 1);
+        return page.get(page.size() - 1);
     }
 
-    public void prependPage(@NonNull Page<K, V> page, @NonNull Callback callback) {
-        final int count = page.items.size();
+    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;
@@ -274,8 +276,8 @@
         callback.onPagePrepended(mLeadingNullCount, changedCount, addedCount);
     }
 
-    public void appendPage(@NonNull Page<K, V> page, @NonNull Callback callback) {
-        final int count = page.items.size();
+    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;
@@ -284,7 +286,7 @@
         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).items.size() != mPageSize
+            if (mPages.get(mPages.size() - 1).size() != mPageSize
                     || count > mPageSize) {
                 mPageSize = -1;
             }
@@ -306,8 +308,30 @@
 
     // ------------------ Non-Contiguous API (tiling required) ----------------------
 
-    public void insertPage(int position, @NonNull Page<K, V> page, Callback callback) {
-        final int newPageSize = page.items.size();
+    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)
@@ -334,22 +358,15 @@
 
         int localPageIndex = pageIndex - mLeadingNullCount / mPageSize;
 
-        Page<K, V> oldPage = mPages.get(localPageIndex);
-        if (oldPage != null && oldPage != mPlaceholderPage) {
+        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);
-        callback.onPageInserted(position, page.items.size());
-    }
-
-    private Page<K, V> getPlaceholderPage() {
-        if (mPlaceholderPage == null) {
-            @SuppressWarnings("unchecked")
-            List<V> list = Collections.emptyList();
-            mPlaceholderPage = new Page<>(null, list, null);
+        if (callback != null) {
+            callback.onPageInserted(position, page.size());
         }
-        return mPlaceholderPage;
     }
 
     private void allocatePageRange(final int minimumPage, final int maximumPage) {
@@ -399,7 +416,8 @@
         for (int pageIndex = minimumPage; pageIndex <= maximumPage; pageIndex++) {
             int localPageIndex = pageIndex - leadingNullPages;
             if (mPages.get(localPageIndex) == null) {
-                mPages.set(localPageIndex, getPlaceholderPage());
+                //noinspection unchecked
+                mPages.set(localPageIndex, PLACEHOLDER_LIST);
                 callback.onPagePlaceholderInserted(pageIndex);
             }
         }
@@ -414,9 +432,9 @@
             return false;
         }
 
-        Page<K, V> page = mPages.get(index - leadingNullPages);
+        List<T> page = mPages.get(index - leadingNullPages);
 
-        return page != null && page != mPlaceholderPage;
+        return page != null && page != PLACEHOLDER_LIST;
     }
 
     @Override
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 fa2932a..5dd3a83 100644
--- a/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * 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.
@@ -20,115 +20,139 @@
 import android.support.annotation.Nullable;
 import android.support.annotation.WorkerThread;
 
-import java.util.List;
+import java.util.Collections;
 
 /**
- * Incremental data loader for paging positional content, where content can be loaded based on its
- * integer position.
+ * Position-based data loader for a fixed-size, countable data set, supporting loads at arbitrary
+ * positions.
  * <p>
- * Use PositionalDataSource if you only need position as input for item loading - if for example,
- * you're asking the backend for items at positions 10 through 20, or using a limit/offset database
- * query to load items at query position 10 through 20.
+ * Extend PositionalDataSource if you can support counting your data set, and loading based on
+ * position information.
  * <p>
- * Implement a DataSource using PositionalDataSource if position is the only information you need to
- * load items.
+ * Note that unless {@link PagedList.Config#enablePlaceholders placeholders are disabled}
+ * PositionalDataSource requires counting the size of the dataset. This allows pages to be tiled in
+ * at arbitrary, non-contiguous locations based upon what the user observes in a {@link PagedList}.
  * <p>
- * Note that {@link BoundedDataSource} provides a simpler API for positional loading, if your
- * backend or data store doesn't require
- * <p>
- * @param <Value> Value type of items being loaded by the DataSource.
+ * 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&lt;Integer, User> loadUsersByAgeDesc();
+ * }</pre>
+ *
+ * @param <T> Type of items being loaded by the PositionalDataSource.
  */
-abstract class PositionalDataSource<Value> extends ContiguousDataSource<Integer, Value> {
+public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
 
     /**
-     * Number of items that this DataSource can provide in total, or COUNT_UNDEFINED.
-     *
-     * @return number of items that this DataSource can provide in total, or COUNT_UNDEFINED
-     * if difficult or undesired to compute.
-     */
-    public int countItems() {
-        return COUNT_UNDEFINED;
-    }
-
-    @Nullable
-    @Override
-    List<Value> loadAfterImpl(int currentEndIndex, @NonNull Value currentEndItem, int pageSize) {
-        return loadAfter(currentEndIndex + 1, pageSize);
-    }
-
-    @Nullable
-    @Override
-    List<Value> loadBeforeImpl(int currentBeginIndex, @NonNull Value currentBeginItem,
-            int pageSize) {
-        return loadBefore(currentBeginIndex - 1, pageSize);
-    }
-
-    @Override
-    void loadInitial(Integer position, int initialLoadSize, boolean enablePlaceholders,
-            @NonNull PageResult.Receiver<Integer, Value> receiver) {
-
-        final int convertPosition = position == null ? 0 : position;
-        final int loadPosition = Math.max(0, (convertPosition - initialLoadSize / 2));
-
-        int count = COUNT_UNDEFINED;
-        if (enablePlaceholders) {
-            count = countItems();
-        }
-        List<Value> data = loadAfter(loadPosition, initialLoadSize);
-
-        if (data == null) {
-            receiver.onPageResult(new PageResult<Integer, Value>(PageResult.INIT));
-            return;
-        }
-
-        final boolean uncounted = count == COUNT_UNDEFINED;
-        int leadingNullCount = uncounted ? 0 : loadPosition;
-        int trailingNullCount = uncounted ? 0 : count - leadingNullCount - data.size();
-        int positionOffset = uncounted ? loadPosition : 0;
-
-        receiver.onPageResult(new PageResult<>(
-                PageResult.INIT,
-                new Page<Integer, Value>(data),
-                leadingNullCount,
-                trailingNullCount,
-                positionOffset));
-    }
-
-    /**
-     * Load data after currently loaded content, starting at the provided index.
+     * Load initial list data.
      * <p>
-     * It's valid to return a different list size than the page size, if it's easier for this data
-     * source. It is generally safer to increase the number loaded than reduce.
+     * 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 startIndex Load items starting at this index.
-     * @param pageSize Suggested number of items to load.
-     * @return List of items, starting at position currentEndIndex + 1. Null if the data source is
-     *         no longer valid, and should not be queried again.
+     * @param requestedStartPosition Initial load position requested. Note that this may not be
+     *                               within the bounds of your data set, it should be corrected
+     *                               before you make your query.
+     * @param requestedLoadSize Requested number of items to load. Note that this may be larger than
+     *                          available data.
+     * @param pageSize Defines page size acceptable for return values. List of items passed to the
+     *                 callback must be an integer multiple of page size.
+     * @param callback DataSource.InitialLoadCallback that receives initial load data, including
+     *                 position and total data set size.
      */
     @WorkerThread
-    @Nullable
-    public abstract List<Value> loadAfter(int startIndex, int pageSize);
+    public abstract void loadInitial(int requestedStartPosition, int requestedLoadSize,
+            int pageSize, @NonNull InitialLoadCallback<T> callback);
 
     /**
-     * Load data before the currently loaded content, starting at the provided index.
+     * Called to load a range of data from the DataSource.
      * <p>
-     * It's valid to return a different list size than the page size, if it's easier for this data
-     * source. It is generally safer to increase the number loaded than reduce.
+     * This method is called to load additional pages from the DataSource after the
+     * InitialLoadCallback passed to loadInitial has initialized a PagedList.
+     * <p>
+     * Unlike {@link #loadInitial(int, int, int, InitialLoadCallback)}, this method must return the
+     * number of items requested, at the position requested.
      *
-     * @param startIndex Load items, starting at this index.
-     * @param pageSize Suggested number of items to load.
-     * @return List of items, in descending order, starting at position currentBeginIndex - 1. Null
-     *         if the data source is no longer valid, and should not be queried again.
+     * @param startPosition Initial load position.
+     * @param count Number of items to load.
+     * @param callback DataSource.LoadCallback that receives loaded data.
      */
     @WorkerThread
-    @Nullable
-    public abstract List<Value> loadBefore(int startIndex, int pageSize);
+    public abstract void loadRange(int startPosition, int count,
+            @NonNull LoadCallback<T> callback);
 
     @Override
-    Integer getKey(int position, Value item) {
-        if (position < 0) {
-            return null;
+    boolean isContiguous() {
+        return false;
+    }
+
+    @NonNull
+    ContiguousDataSource<Integer, T> wrapAsContiguousWithoutPlaceholders() {
+        return new ContiguousWithoutPlaceholdersWrapper<>(this);
+    }
+
+    static int computeFirstLoadPosition(int position, int firstLoadSize, int pageSize, int size) {
+        int roundedPageStart = Math.round(position / pageSize) * pageSize;
+
+        // maximum start pos is that which will encompass end of list
+        int maximumLoadPage = ((size - firstLoadSize + pageSize - 1) / pageSize) * pageSize;
+        roundedPageStart = Math.min(maximumLoadPage, roundedPageStart);
+
+        // minimum start position is 0
+        roundedPageStart = Math.max(0, roundedPageStart);
+
+        return roundedPageStart;
+    }
+
+    @SuppressWarnings("deprecation")
+    static class ContiguousWithoutPlaceholdersWrapper<Value>
+            extends ContiguousDataSource<Integer, Value> {
+
+        @NonNull
+        final PositionalDataSource<Value> mPositionalDataSource;
+
+        ContiguousWithoutPlaceholdersWrapper(
+                @NonNull PositionalDataSource<Value> positionalDataSource) {
+            mPositionalDataSource = positionalDataSource;
         }
-        return position;
+
+        @Override
+        public void loadInitial(@Nullable Integer position, int initialLoadSize,
+                boolean enablePlaceholders, @NonNull InitialLoadCallback<Value> callback) {
+            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 InitialLoadCallback.
+            mPositionalDataSource.loadInitial(
+                    convertPosition, initialLoadSize, initialLoadSize, callback);
+        }
+
+        @Override
+        void loadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize,
+                @NonNull LoadCallback<Value> callback) {
+            int startIndex = currentEndIndex + 1;
+            mPositionalDataSource.loadRange(startIndex, pageSize, callback);
+        }
+
+        @Override
+        void loadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, int pageSize,
+                @NonNull LoadCallback<Value> callback) {
+            int startIndex = currentBeginIndex - 1;
+            if (startIndex < 0) {
+                callback.onResult(Collections.<Value>emptyList());
+            } else {
+                int loadSize = Math.min(pageSize, startIndex + 1);
+                startIndex = startIndex - loadSize + 1;
+                mPositionalDataSource.loadRange(startIndex, loadSize, callback);
+            }
+        }
+
+        @Override
+        Integer getKey(int position, Value item) {
+            return position;
+        }
     }
 }
diff --git a/paging/common/src/main/java/android/arch/paging/TiledDataSource.java b/paging/common/src/main/java/android/arch/paging/TiledDataSource.java
index 0ea9428..27aeda0 100644
--- a/paging/common/src/main/java/android/arch/paging/TiledDataSource.java
+++ b/paging/common/src/main/java/android/arch/paging/TiledDataSource.java
@@ -16,84 +16,24 @@
 
 package android.arch.paging;
 
-import android.support.annotation.Nullable;
+import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
 import android.support.annotation.WorkerThread;
 
 import java.util.Collections;
 import java.util.List;
 
 /**
- * Position-based data loader for fixed size, arbitrary positioned loading.
- * <p>
- * Extend TiledDataSource if you want to load arbitrary pages based solely on position information,
- * and can generate pages of a provided fixed size.
- * <p>
- * Room can generate a TiledDataSource for you:
- * <pre>
- * {@literal @}Dao
- * interface UserDao {
- *     {@literal @}Query("SELECT * FROM user ORDER BY mAge DESC")
- *     public abstract TiledDataSource&lt;User> loadUsersByAgeDesc();
- * }</pre>
+ * @param <T> Type loaded by the TiledDataSource.
  *
- * Under the hood, Room will generate code equivalent to the below, using a limit/offset SQL query:
- * <pre>
- * {@literal @}Dao
- * interface UserDao {
- *     {@literal @}Query("SELECT COUNT(*) from user")
- *     public abstract Integer getUserCount();
- *
- *     {@literal @}Query("SELECT * from user ORDER BY mName DESC LIMIT :limit OFFSET :offset")
- *     public abstract List&lt;User> userNameLimitOffset(int limit, int offset);
- * }
- *
- * public class OffsetUserQueryDataSource extends TiledDataSource&lt;User> {
- *     private MyDatabase mDb;
- *     private final UserDao mUserDao;
- *     {@literal @}SuppressWarnings("FieldCanBeLocal")
- *     private final InvalidationTracker.Observer mObserver;
- *
- *     public OffsetUserQueryDataSource(MyDatabase db) {
- *         mDb = db;
- *         mUserDao = db.getUserDao();
- *         mObserver = new InvalidationTracker.Observer("user") {
- *             {@literal @}Override
- *             public void onInvalidated({@literal @}NonNull Set&lt;String> tables) {
- *                 // the user table has been invalidated, invalidate the DataSource
- *                 invalidate();
- *             }
- *         };
- *         db.getInvalidationTracker().addWeakObserver(mObserver);
- *     }
- *
- *     {@literal @}Override
- *     public boolean isInvalid() {
- *         mDb.getInvalidationTracker().refreshVersionsSync();
- *         return super.isInvalid();
- *     }
- *
- *     {@literal @}Override
- *     public int countItems() {
- *         return mUserDao.getUserCount();
- *     }
- *
- *     {@literal @}Override
- *     public List&lt;User> loadRange(int startPosition, int loadCount) {
- *         return mUserDao.userNameLimitOffset(loadCount, startPosition);
- *     }
- * }</pre>
- *
- * @param <Type> Type of items being loaded by the TiledDataSource.
+ * @deprecated Use {@link PositionalDataSource}
+ * @hide
  */
-public abstract class TiledDataSource<Type> extends DataSource<Integer, Type> {
+@SuppressWarnings("DeprecatedIsStillUsed")
+@Deprecated
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public abstract class TiledDataSource<T> extends PositionalDataSource<T> {
 
-    private int mItemCount;
-
-    /**
-     * Number of items that this DataSource can provide in total.
-     *
-     * @return Number of items this DataSource can provide. Must be <code>0</code> or greater.
-     */
     @WorkerThread
     public abstract int countItems();
 
@@ -102,111 +42,39 @@
         return false;
     }
 
-    /**
-     * Called to load items at from the specified position range.
-     * <p>
-     * This method must return a list of requested size, unless at the end of list. Fixed size pages
-     * enable TiledDataSource to navigate tiles efficiently, and quickly accesss any position in the
-     * data set.
-     * <p>
-     * If a list of a different size is returned, but it is not the last list in the data set based
-     * on the return value from {@link #countItems()}, an exception will be thrown.
-     *
-     * @param startPosition Index of first item to load.
-     * @param count         Number of items to load.
-     * @return List of loaded items, of the requested length unless at end of list. Null if the
-     *         DataSource is no longer valid, and should not be queried again.
-     */
     @WorkerThread
-    public abstract List<Type> loadRange(int startPosition, int count);
+    public abstract List<T> loadRange(int startPosition, int count);
 
-    /**
-     * blocking, and splits pages
-     */
-    void loadRangeInitial(int startPosition, int count, int pageSize, int itemCount,
-            PageResult.Receiver<Integer, Type> receiver) {
-        mItemCount = itemCount;
-
-        if (itemCount == 0) {
-            // no data to load, just immediately return empty
-            receiver.onPageResult(new PageResult<>(
-                    PageResult.INIT, new Page<Integer, Type>(Collections.<Type>emptyList()),
-                    0, 0, startPosition));
+    @Override
+    public void loadInitial(int requestedStartPosition, int requestedLoadSize, int pageSize,
+            @NonNull InitialLoadCallback<T> callback) {
+        int totalCount = countItems();
+        if (totalCount == 0) {
+            callback.onResult(Collections.<T>emptyList());
             return;
         }
 
-        List<Type> list = loadRangeWrapper(startPosition, count);
+        // bound the size requested, based on known count
+        final int firstLoadPosition = computeFirstLoadPosition(
+                requestedStartPosition, requestedLoadSize, pageSize, totalCount);
+        final int firstLoadSize = Math.min(totalCount - firstLoadPosition, requestedLoadSize);
 
-        count = Math.min(count, itemCount - startPosition);
-
-        if (list == null) {
-            // invalid data, pass to receiver
-            receiver.onPageResult(new PageResult<Integer, Type>(
-                    PageResult.INIT, null, 0, 0, startPosition));
-            return;
-        }
-
-        if (list.size() != count) {
-            throw new IllegalStateException("Invalid list, requested size: " + count
-                    + ", returned size: " + list.size());
-        }
-
-        // emit the results as multiple pages
-        int pageCount = (count + (pageSize - 1)) / pageSize;
-        for (int i = 0; i < pageCount; i++) {
-            int beginInclusive = i * pageSize;
-            int endExclusive = Math.min(count, (i + 1) * pageSize);
-
-            Page<Integer, Type> page = new Page<>(list.subList(beginInclusive, endExclusive));
-
-            int leadingNulls = startPosition + beginInclusive;
-            int trailingNulls = itemCount - leadingNulls - page.items.size();
-            receiver.onPageResult(new PageResult<>(
-                    PageResult.INIT, page, leadingNulls, trailingNulls, 0));
-        }
-    }
-
-    void loadRange(int startPosition, int count, PageResult.Receiver<Integer, Type> receiver) {
-        List<Type> list = loadRangeWrapper(startPosition, count);
-
-        Page<Integer, Type> page = null;
-        int trailingNulls = mItemCount - startPosition;
-
+        // convert from legacy behavior
+        List<T> list = loadRange(firstLoadPosition, firstLoadSize);
         if (list != null) {
-            page = new Page<Integer, Type>(list);
-            trailingNulls -= list.size();
+            callback.onResult(list, firstLoadPosition, totalCount);
+        } else {
+            invalidate();
         }
-        receiver.postOnPageResult(new PageResult<>(
-                PageResult.TILE, page, startPosition, trailingNulls, 0));
     }
 
-    private List<Type> loadRangeWrapper(int startPosition, int count) {
-        if (isInvalid()) {
-            return null;
-        }
-        List<Type> list = loadRange(startPosition, count);
-        if (isInvalid()) {
-            return null;
-        }
-        return list;
-    }
-
-    ContiguousDataSource<Integer, Type> getAsContiguous() {
-        return new TiledAsBoundedDataSource<>(this);
-    }
-
-    static class TiledAsBoundedDataSource<Value> extends BoundedDataSource<Value> {
-        final TiledDataSource<Value> mTiledDataSource;
-
-        TiledAsBoundedDataSource(TiledDataSource<Value> tiledDataSource) {
-            mTiledDataSource = tiledDataSource;
-        }
-
-        @WorkerThread
-        @Nullable
-        @Override
-        public List<Value> loadRange(int startPosition, int loadCount) {
-            return mTiledDataSource.loadRange(startPosition, loadCount);
+    @Override
+    public void loadRange(int startPosition, int count, @NonNull LoadCallback<T> callback) {
+        List<T> list = loadRange(startPosition, count);
+        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
index 76bb682..652b489 100644
--- a/paging/common/src/main/java/android/arch/paging/TiledPagedList.java
+++ b/paging/common/src/main/java/android/arch/paging/TiledPagedList.java
@@ -25,32 +25,16 @@
 
 class TiledPagedList<T> extends PagedList<T>
         implements PagedStorage.Callback {
+    private final PositionalDataSource<T> mDataSource;
 
-    private final TiledDataSource<T> mDataSource;
-
-    @SuppressWarnings("unchecked")
-    private final PagedStorage<Integer, T> mKeyedStorage = (PagedStorage<Integer, T>) mStorage;
-
-    private final PageResult.Receiver<Integer, T> mReceiver =
-            new PageResult.Receiver<Integer, T>() {
-        @AnyThread
-        @Override
-        public void postOnPageResult(@NonNull final PageResult<Integer, T> pageResult) {
-            // NOTE: if we're already on main thread, this can delay page receive by a frame
-            mMainThreadExecutor.execute(new Runnable() {
-                @Override
-                public void run() {
-                    onPageResult(pageResult);
-                }
-            });
-        }
-
+    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(@NonNull PageResult<Integer, T> pageResult) {
-            if (pageResult.page == null) {
+        public void onPageResult(@PageResult.ResultType int type,
+                @NonNull PageResult<T> pageResult) {
+            if (pageResult.isInvalid()) {
                 detach();
                 return;
             }
@@ -61,60 +45,63 @@
             }
 
             if (mStorage.getPageCount() == 0) {
-                mKeyedStorage.init(
+                mStorage.initAndSplit(
                         pageResult.leadingNulls, pageResult.page, pageResult.trailingNulls,
-                        pageResult.positionOffset, TiledPagedList.this);
+                        pageResult.positionOffset, mConfig.pageSize, TiledPagedList.this);
             } else {
-                mKeyedStorage.insertPage(pageResult.leadingNulls, pageResult.page,
+                mStorage.insertPage(pageResult.positionOffset, pageResult.page,
                         TiledPagedList.this);
             }
 
             if (mBoundaryCallback != null) {
                 boolean deferEmpty = mStorage.size() == 0;
-                boolean deferBegin = !deferEmpty && pageResult.leadingNulls == 0;
-                boolean deferEnd = !deferEmpty && pageResult.trailingNulls == 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 TiledDataSource<T> dataSource,
+    TiledPagedList(@NonNull PositionalDataSource<T> dataSource,
             @NonNull Executor mainThreadExecutor,
             @NonNull Executor backgroundThreadExecutor,
             @Nullable BoundaryCallback<T> boundaryCallback,
             @NonNull Config config,
             int position) {
-        super(new PagedStorage<Integer, T>(), mainThreadExecutor, backgroundThreadExecutor,
+        super(new PagedStorage<T>(), mainThreadExecutor, backgroundThreadExecutor,
                 boundaryCallback, config);
         mDataSource = dataSource;
 
         final int pageSize = mConfig.pageSize;
+        mLastLoad = position;
 
-        final int itemCount = mDataSource.countItems();
+        if (mDataSource.isInvalid()) {
+            detach();
+        } else {
+            final int firstLoadSize =
+                    (Math.max(Math.round(mConfig.initialLoadSizeHint / pageSize), 2)) * pageSize;
 
-        final int firstLoadSize = Math.min(itemCount,
-                (Math.max(mConfig.initialLoadSizeHint / pageSize, 2)) * pageSize);
-        final int firstLoadPosition = computeFirstLoadPosition(
-                position, firstLoadSize, pageSize, itemCount);
+            final int idealStart = position - firstLoadSize / 2;
+            final int roundedPageStart = Math.max(0, Math.round(idealStart / pageSize) * pageSize);
 
-        mDataSource.loadRangeInitial(firstLoadPosition, firstLoadSize, pageSize,
-                itemCount, mReceiver);
-    }
+            DataSource.InitialLoadCallback<T> callback = new DataSource.InitialLoadCallback<>(
+                            DataSource.LOAD_COUNT_REQUIRED_TILED,
+                    mConfig.pageSize, mDataSource, mReceiver);
+            mDataSource.loadInitial(roundedPageStart, firstLoadSize, pageSize, callback);
 
-    static int computeFirstLoadPosition(int position, int firstLoadSize, int pageSize, int size) {
-        int idealStart = position - firstLoadSize / 2;
-
-        int roundedPageStart = Math.round(idealStart / pageSize) * pageSize;
-
-        // minimum start position is 0
-        roundedPageStart = Math.max(0, roundedPageStart);
-
-        // maximum start pos is that which will encompass end of list
-        int maximumLoadPage = ((size - firstLoadSize + pageSize - 1) / pageSize) * pageSize;
-        roundedPageStart = Math.min(maximumLoadPage, roundedPageStart);
-
-        return roundedPageStart;
+            // 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.setPostExecutor(mMainThreadExecutor);
+        }
     }
 
     @Override
@@ -132,7 +119,13 @@
     protected void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> pagedListSnapshot,
             @NonNull Callback callback) {
         //noinspection UnnecessaryLocalVariable
-        final PagedStorage<?, T> snapshot = pagedListSnapshot.mStorage;
+        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.
@@ -186,7 +179,17 @@
                     return;
                 }
                 final int pageSize = mConfig.pageSize;
-                mDataSource.loadRange(pageIndex * pageSize, pageSize, mReceiver);
+
+                if (mDataSource.isInvalid()) {
+                    detach();
+                } else {
+                    int startPosition = pageIndex * pageSize;
+                    int count = Math.min(pageSize, mStorage.size() - startPosition);
+                    DataSource.LoadCallback<T> callback = new DataSource.LoadCallback<>(
+                            PageResult.TILE, mMainThreadExecutor, mDataSource, mReceiver);
+                    callback.setPositionOffset(startPosition);
+                    mDataSource.loadRange(startPosition, count, callback);
+                }
             }
         });
     }
diff --git a/paging/common/src/test/java/android/arch/paging/AsyncListDataSource.kt b/paging/common/src/test/java/android/arch/paging/AsyncListDataSource.kt
new file mode 100644
index 0000000..48d6e29
--- /dev/null
+++ b/paging/common/src/test/java/android/arch/paging/AsyncListDataSource.kt
@@ -0,0 +1,41 @@
+/*
+ * 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>() {
+    val workItems: MutableList<() -> Unit> = ArrayList()
+    private val listDataSource = ListDataSource(list)
+
+    override fun loadInitial(requestedStartPosition: Int, requestedLoadSize: Int, pageSize: Int,
+            callback: InitialLoadCallback<T>) {
+        workItems.add {
+            listDataSource.loadInitial(requestedStartPosition, requestedLoadSize, pageSize, callback)
+        }
+    }
+
+    override fun loadRange(startPosition: Int, count: Int, callback: LoadCallback<T>) {
+        workItems.add {
+            listDataSource.loadRange(startPosition, count, 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
index 7331bfe..5ae0f9f 100644
--- a/paging/common/src/test/java/android/arch/paging/ContiguousPagedListTest.kt
+++ b/paging/common/src/test/java/android/arch/paging/ContiguousPagedListTest.kt
@@ -18,17 +18,16 @@
 
 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.Parameterized
-import org.mockito.ArgumentCaptor
-import org.mockito.Mockito.any
-import org.mockito.Mockito.eq
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
 import org.mockito.Mockito.verifyZeroInteractions
-import java.util.Collections
+
 
 @RunWith(Parameterized::class)
 class ContiguousPagedListTest(private val mCounted: Boolean) {
@@ -44,33 +43,46 @@
     }
 
     private inner class TestSource(val listData: List<Item> = ITEMS)
-            : PositionalDataSource<Item>() {
-        override fun countItems(): Int {
-            return if (mCounted) {
-                listData.size
+            : ContiguousDataSource<Int, Item>() {
+        override fun loadInitial(key: Int?, initialLoadSize: Int,
+                enablePlaceholders: Boolean, callback: InitialLoadCallback<Item>) {
+
+            val convertPosition = key ?: 0
+            val loadPosition = Math.max(0, (convertPosition - initialLoadSize / 2))
+
+            val data = getClampedRange(loadPosition, loadPosition + initialLoadSize)
+
+
+            if (enablePlaceholders && mCounted) {
+                callback.onResult(data, loadPosition, listData.size)
             } else {
-                DataSource.COUNT_UNDEFINED
+                // still must pass offset, even if not counted
+                callback.onResult(data, loadPosition)
             }
         }
 
-        private fun getClampedRange(startInc: Int, endExc: Int, reverse: Boolean): List<Item> {
-            val list = listData.subList(Math.max(0, startInc), Math.min(listData.size, endExc))
-            if (reverse) {
-                Collections.reverse(list)
-            }
-            return list
+        override fun loadAfter(currentEndIndex: Int, currentEndItem: Item, pageSize: Int,
+                callback: LoadCallback<Item>) {
+            val startIndex = currentEndIndex + 1
+            callback.onResult(getClampedRange(startIndex, startIndex + pageSize))
         }
 
-        override fun loadAfter(startIndex: Int, pageSize: Int): List<Item>? {
-            return getClampedRange(startIndex, startIndex + pageSize, false)
+        override fun loadBefore(currentBeginIndex: Int, currentBeginItem: Item, pageSize: Int,
+                callback: LoadCallback<Item>) {
+            val startIndex = currentBeginIndex - 1
+            callback.onResult(getClampedRange(startIndex - pageSize + 1, startIndex + 1))
         }
 
-        override fun loadBefore(startIndex: Int, pageSize: Int): List<Item>? {
-            return getClampedRange(startIndex - pageSize + 1, startIndex + 1, true)
+        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))
         }
     }
 
-    private fun verifyRange(start: Int, count: Int, actual: PagedStorage<*, Item>) {
+    private fun verifyRange(start: Int, count: Int, actual: PagedStorage<Item>) {
         if (mCounted) {
             // assert nulls + content
             val expected = arrayOfNulls<Item>(ITEMS.size)
@@ -98,41 +110,6 @@
         verifyRange(start, count, actual.mStorage)
     }
 
-    private fun verifyRange(start: Int, count: Int, actual: PageResult<Int, Item>) {
-        if (mCounted) {
-            assertEquals(start, actual.leadingNulls)
-            assertEquals(ITEMS.size - start - count, actual.trailingNulls)
-            assertEquals(0, actual.positionOffset)
-        } else {
-            assertEquals(0, actual.leadingNulls)
-            assertEquals(0, actual.trailingNulls)
-            assertEquals(start, actual.positionOffset)
-        }
-        assertEquals(ITEMS.subList(start, start + count), actual.page.items)
-    }
-
-    private fun verifyInitialLoad(start: Int, count : Int, initialPosition: Int, initialLoadSize: Int) {
-        @Suppress("UNCHECKED_CAST")
-        val receiver = mock(PageResult.Receiver::class.java) as PageResult.Receiver<Int, Item>
-
-        @Suppress("UNCHECKED_CAST")
-        val captor = ArgumentCaptor.forClass(PageResult::class.java)
-                as ArgumentCaptor<PageResult<Int, Item>>
-
-        TestSource().loadInitial(initialPosition, initialLoadSize, true, receiver)
-
-        verify(receiver).onPageResult(captor.capture())
-        verifyNoMoreInteractions(receiver)
-        verifyRange(start, count, captor.value)
-    }
-
-    @Test
-    fun initialLoad() {
-        verifyInitialLoad(30, 40, 50, 40)
-        verifyInitialLoad(0, 10, 5, 10)
-        verifyInitialLoad(90, 10, 95, 10)
-    }
-
     private fun createCountedPagedList(
             initialPosition: Int,
             pageSize: Int = 20,
@@ -182,7 +159,6 @@
         verifyNoMoreInteractions(callback)
     }
 
-
     @Test
     fun prepend() {
         val pagedList = createCountedPagedList(80)
@@ -311,6 +287,60 @@
     }
 
     @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)
+
+        assertTrue(pagedList.isEmpty())
+        drain()
+        assertTrue(pagedList.isEmpty())
+        asyncDataSource.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() {
+        // 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)
+        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 =
@@ -347,8 +377,7 @@
         pagedList.loadAround(if (mCounted) 99 else 19)
         drain()
         verifyRange(80, 20, pagedList)
-        verify(boundaryCallback).onItemAtEndLoaded(
-                any(), eq(ITEMS.last()), eq(if (mCounted) 100 else 20))
+        verify(boundaryCallback).onItemAtEndLoaded(ITEMS.last())
         verifyNoMoreInteractions(boundaryCallback)
 
 
@@ -371,7 +400,7 @@
         // ... finally try prepend, see 0 items, which will dispatch front callback
         pagedList.loadAround(0)
         drain()
-        verify(boundaryCallback).onItemAtFrontLoaded(any(), eq(ITEMS.first()), eq(100))
+        verify(boundaryCallback).onItemAtFrontLoaded(ITEMS.first())
         verifyNoMoreInteractions(boundaryCallback)
     }
 
diff --git a/paging/common/src/test/java/android/arch/paging/KeyedDataSourceTest.kt b/paging/common/src/test/java/android/arch/paging/KeyedDataSourceTest.kt
index 115fea2..82ad9ac 100644
--- a/paging/common/src/test/java/android/arch/paging/KeyedDataSourceTest.kt
+++ b/paging/common/src/test/java/android/arch/paging/KeyedDataSourceTest.kt
@@ -16,7 +16,6 @@
 
 package android.arch.paging
 
-import org.junit.Assert.assertArrayEquals
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertTrue
@@ -24,6 +23,7 @@
 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
@@ -33,31 +33,20 @@
 
     // ----- STANDARD -----
 
-    @Test
-    fun loadInitial_validateTestDataSource() {
-        val dataSource = ItemDataSource()
-
-        // all
-        assertEquals(ITEMS_BY_NAME_ID, dataSource.loadInitial(ITEMS_BY_NAME_ID.size))
-
-        // 10
-        assertEquals(ITEMS_BY_NAME_ID.subList(0, 10), dataSource.loadInitial(10))
-
-        // too many
-        assertEquals(ITEMS_BY_NAME_ID, dataSource.loadInitial(ITEMS_BY_NAME_ID.size + 10))
-    }
-
     private fun loadInitial(dataSource: ItemDataSource, key: Key?, initialLoadSize: Int,
-            enablePlaceholders: Boolean): PageResult<Key, Item> {
+            enablePlaceholders: Boolean): PageResult<Item> {
         @Suppress("UNCHECKED_CAST")
-        val receiver = mock(PageResult.Receiver::class.java) as PageResult.Receiver<Key, Item>
+        val receiver = mock(PageResult.Receiver::class.java) as PageResult.Receiver<Item>
         @Suppress("UNCHECKED_CAST")
         val captor = ArgumentCaptor.forClass(PageResult::class.java)
-                as ArgumentCaptor<PageResult<Key, Item>>
+                as ArgumentCaptor<PageResult<Item>>
 
-        dataSource.loadInitial(key, initialLoadSize, enablePlaceholders, receiver)
+        val callback = DataSource.InitialLoadCallback(
+                DataSource.LOAD_COUNT_ACCEPTED, /* ignored page size */ 10, dataSource, receiver)
 
-        verify(receiver).onPageResult(captor.capture())
+        dataSource.loadInitial(key, initialLoadSize, enablePlaceholders, callback)
+
+        verify(receiver).onPageResult(anyInt(), captor.capture())
         verifyNoMoreInteractions(receiver)
         assertNotNull(captor.value)
         return captor.value
@@ -69,7 +58,7 @@
         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.items)
+        assertEquals(ITEMS_BY_NAME_ID.subList(45, 55), result.page)
         assertEquals(45, result.trailingNulls)
     }
 
@@ -81,7 +70,7 @@
         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.items)
+        assertEquals(ITEMS_BY_NAME_ID.subList(0, 1), result.page)
         assertEquals(0, result.trailingNulls)
     }
 
@@ -93,8 +82,8 @@
         val key = dataSource.getKey(ITEMS_BY_NAME_ID.last())
         val result = loadInitial(dataSource, key, 20, true)
 
-        assertEquals(89, result.leadingNulls)
-        assertEquals(ITEMS_BY_NAME_ID.subList(89, 100), result.page.items)
+        assertEquals(90, result.leadingNulls)
+        assertEquals(ITEMS_BY_NAME_ID.subList(90, 100), result.page)
         assertEquals(0, result.trailingNulls)
     }
 
@@ -106,7 +95,7 @@
         val result = loadInitial(dataSource, null, 10, true)
 
         assertEquals(0, result.leadingNulls)
-        assertEquals(ITEMS_BY_NAME_ID.subList(0, 10), result.page.items)
+        assertEquals(ITEMS_BY_NAME_ID.subList(0, 10), result.page)
         assertEquals(90, result.trailingNulls)
     }
 
@@ -122,7 +111,7 @@
         // 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.items)
+        assertEquals(ITEMS_BY_NAME_ID.subList(95, 100), result.page)
         assertEquals(0, result.trailingNulls)
     }
 
@@ -137,7 +126,7 @@
         val result = loadInitial(dataSource, key, 10, false)
 
         assertEquals(0, result.leadingNulls)
-        assertEquals(ITEMS_BY_NAME_ID.subList(45, 55), result.page.items)
+        assertEquals(ITEMS_BY_NAME_ID.subList(45, 55), result.page)
         assertEquals(0, result.trailingNulls)
     }
 
@@ -150,7 +139,7 @@
         val result = loadInitial(dataSource, key, 10, true)
 
         assertEquals(0, result.leadingNulls)
-        assertEquals(ITEMS_BY_NAME_ID.subList(45, 55), result.page.items)
+        assertEquals(ITEMS_BY_NAME_ID.subList(45, 55), result.page)
         assertEquals(0, result.trailingNulls)
     }
 
@@ -162,7 +151,7 @@
         val result = loadInitial(dataSource, null, 10, true)
 
         assertEquals(0, result.leadingNulls)
-        assertEquals(ITEMS_BY_NAME_ID.subList(0, 10), result.page.items)
+        assertEquals(ITEMS_BY_NAME_ID.subList(0, 10), result.page)
         assertEquals(0, result.trailingNulls)
     }
 
@@ -177,7 +166,7 @@
         val result = loadInitial(dataSource, key, 10, true)
 
         assertEquals(0, result.leadingNulls)
-        assertTrue(result.page.items.isEmpty())
+        assertTrue(result.page.isEmpty())
         assertEquals(0, result.trailingNulls)
     }
 
@@ -187,7 +176,7 @@
         val result = loadInitial(dataSource, null, 10, true)
 
         assertEquals(0, result.leadingNulls)
-        assertTrue(result.page.items.isEmpty())
+        assertTrue(result.page.isEmpty())
         assertEquals(0, result.trailingNulls)
     }
 
@@ -197,21 +186,18 @@
     fun loadBefore() {
         val dataSource = ItemDataSource()
         @Suppress("UNCHECKED_CAST")
-        val receiver = mock(PageResult.Receiver::class.java)
-                as PageResult.Receiver<Key, Item>
+        val callback = mock(DataSource.LoadCallback::class.java) as DataSource.LoadCallback<Item>
 
-        dataSource.loadBefore(5, ITEMS_BY_NAME_ID[5], 5, receiver)
+        dataSource.loadBefore(5, ITEMS_BY_NAME_ID[5], 5, callback)
 
         @Suppress("UNCHECKED_CAST")
-        val argument = ArgumentCaptor.forClass(PageResult::class.java)
-                as ArgumentCaptor<PageResult<Key, Item>>
-        verify(receiver).postOnPageResult(argument.capture())
-        verifyNoMoreInteractions(receiver)
+        val argument = ArgumentCaptor.forClass(List::class.java) as ArgumentCaptor<List<Item>>
+        verify(callback).onResult(argument.capture())
+        verifyNoMoreInteractions(callback)
 
         val observed = argument.value
 
-        assertEquals(PageResult.PREPEND, observed.type)
-        assertEquals(ITEMS_BY_NAME_ID.subList(0, 5), observed.page.items)
+        assertEquals(ITEMS_BY_NAME_ID.subList(0, 5), observed)
     }
 
     internal data class Key(val name: String, val id: Int)
@@ -219,62 +205,53 @@
     internal data class Item(
             val name: String, val id: Int, val balance: Double, val address: String)
 
-    internal class ItemDataSource(val counted: Boolean = true,
-                                  val items: List<Item> = ITEMS_BY_NAME_ID)
+    internal class ItemDataSource(private val counted: Boolean = true,
+                                  private val items: List<Item> = ITEMS_BY_NAME_ID)
             : KeyedDataSource<Key, Item>() {
+        override fun loadInitial(initialLoadKey: Key?, initialLoadSize: Int,
+                enablePlaceholders: Boolean, callback: InitialLoadCallback<Item>) {
+            val key = initialLoadKey ?: Key("", Integer.MAX_VALUE)
+            val start = Math.max(0, findFirstIndexAfter(key) - initialLoadSize / 2)
+            val endExclusive = Math.min(start + initialLoadSize, items.size)
+
+
+            if (enablePlaceholders && counted) {
+                callback.onResult(items.subList(start, endExclusive), start, items.size)
+            } else {
+                callback.onResult(items.subList(start, endExclusive))
+            }
+        }
+
+        override fun loadAfter(currentEndKey: Key, pageSize: Int, callback: LoadCallback<Item>) {
+            val start = findFirstIndexAfter(currentEndKey)
+            val endExclusive = Math.min(start + pageSize, items.size)
+
+            callback.onResult(items.subList(start, endExclusive))
+        }
+
+        override fun loadBefore(currentBeginKey: Key, pageSize: Int, callback: LoadCallback<Item>) {
+            val firstIndexBefore = findFirstIndexBefore(currentBeginKey)
+            val endExclusive = Math.max(0, firstIndexBefore + 1)
+            val start = Math.max(0, firstIndexBefore - pageSize + 1)
+
+            callback.onResult(items.subList(start, endExclusive))
+        }
 
         override fun getKey(item: Item): Key {
             return Key(item.name, item.id)
         }
 
-        override fun loadInitial(pageSize: Int): List<Item>? {
-            // call loadAfter with a default key
-            return loadAfter(Key("", Integer.MAX_VALUE), pageSize)
-        }
-
-        fun findFirstIndexAfter(key: Key): Int {
+        private fun findFirstIndexAfter(key: Key): Int {
             return items.indices.firstOrNull {
                 KEY_COMPARATOR.compare(key, getKey(items[it])) < 0
             } ?: items.size
         }
 
-        fun findFirstIndexBefore(key: Key): Int {
+        private fun findFirstIndexBefore(key: Key): Int {
             return items.indices.reversed().firstOrNull {
                 KEY_COMPARATOR.compare(key, getKey(items[it])) > 0
             } ?: -1
         }
-
-        override fun countItemsBefore(key: Key): Int {
-            if (!counted) {
-                return DataSource.COUNT_UNDEFINED
-            }
-
-            return findFirstIndexBefore(key) + 1
-        }
-
-        override fun countItemsAfter(key: Key): Int {
-            if (!counted) {
-                return DataSource.COUNT_UNDEFINED
-            }
-
-            return items.size - findFirstIndexAfter(key)
-        }
-
-        override fun loadAfter(key: Key, pageSize: Int): List<Item>? {
-            val start = findFirstIndexAfter(key)
-            val endExclusive = Math.min(start + pageSize, items.size)
-
-            return items.subList(start, endExclusive)
-        }
-
-        override fun loadBefore(key: Key, pageSize: Int): List<Item>? {
-            val firstIndexBefore = findFirstIndexBefore(key)
-            val endExclusive = Math.max(0, firstIndexBefore + 1)
-            val start = Math.max(0, firstIndexBefore - pageSize + 1)
-
-            val list = items.subList(start, endExclusive)
-            return list.reversed()
-        }
     }
 
     companion object {
diff --git a/paging/common/src/test/java/android/arch/paging/PagedStorageTest.kt b/paging/common/src/test/java/android/arch/paging/PagedStorageTest.kt
index 92b6c87..8fb54e0 100644
--- a/paging/common/src/test/java/android/arch/paging/PagedStorageTest.kt
+++ b/paging/common/src/test/java/android/arch/paging/PagedStorageTest.kt
@@ -29,8 +29,8 @@
 
 @RunWith(JUnit4::class)
 class PagedStorageTest {
-    private fun createPage(vararg strings: String): Page<Int, String> {
-        return Page(strings.asList())
+    private fun createPage(vararg strings: String): List<String> {
+        return strings.asList()
     }
 
     @Test
@@ -216,7 +216,7 @@
     @Test
     fun insertOne() {
         val callback = mock(PagedStorage.Callback::class.java)
-        val storage = PagedStorage<Int, String>()
+        val storage = PagedStorage<String>()
 
         storage.init(2, createPage("c", "d"), 3, 0, callback)
 
@@ -236,7 +236,7 @@
     @Test
     fun insertThree() {
         val callback = mock(PagedStorage.Callback::class.java)
-        val storage = PagedStorage<Int, String>()
+        val storage = PagedStorage<String>()
 
         storage.init(2, createPage("c", "d"), 3, 0, callback)
 
@@ -273,7 +273,7 @@
     @Test
     fun insertLastFirst() {
         val callback = mock(PagedStorage.Callback::class.java)
-        val storage = PagedStorage<Int, String>()
+        val storage = PagedStorage<String>()
 
         storage.init(6, createPage("g"), 0, 0, callback)
 
@@ -294,7 +294,7 @@
     @Test(expected = IllegalArgumentException::class)
     fun insertFailure_decreaseLast() {
         val callback = mock(PagedStorage.Callback::class.java)
-        val storage = PagedStorage<Int, String>()
+        val storage = PagedStorage<String>()
 
         storage.init(2, createPage("c", "d"), 0, 0, callback)
 
@@ -305,7 +305,7 @@
     @Test(expected = IllegalArgumentException::class)
     fun insertFailure_increase() {
         val callback = mock(PagedStorage.Callback::class.java)
-        val storage = PagedStorage<Int, String>()
+        val storage = PagedStorage<String>()
 
         storage.init(0, createPage("a", "b"), 3, 0, callback)
 
@@ -316,7 +316,7 @@
     @Test
     fun allocatePlaceholders_simple() {
         val callback = mock(PagedStorage.Callback::class.java)
-        val storage = PagedStorage<Int, String>()
+        val storage = PagedStorage<String>()
 
         storage.init(2, createPage("c"), 2, 0, callback)
 
@@ -334,7 +334,7 @@
     @Test
     fun allocatePlaceholders_adoptPageSize() {
         val callback = mock(PagedStorage.Callback::class.java)
-        val storage = PagedStorage<Int, String>()
+        val storage = PagedStorage<String>()
 
         storage.init(4, createPage("e"), 0, 0, callback)
 
@@ -352,7 +352,7 @@
     @Test(expected = IllegalArgumentException::class)
     fun allocatePlaceholders_cannotShrinkPageSize() {
         val callback = mock(PagedStorage.Callback::class.java)
-        val storage = PagedStorage<Int, String>()
+        val storage = PagedStorage<String>()
 
         storage.init(4, createPage("e", "f"), 0, 0, callback)
 
@@ -365,7 +365,7 @@
     @Test(expected = IllegalArgumentException::class)
     fun allocatePlaceholders_cannotAdoptPageSize() {
         val callback = mock(PagedStorage.Callback::class.java)
-        val storage = PagedStorage<Int, String>()
+        val storage = PagedStorage<String>()
 
         storage.init(2, createPage("c", "d"), 2, 0, callback)
 
@@ -377,7 +377,7 @@
     @Test
     fun get_placeholdersMulti() {
         val callback = mock(PagedStorage.Callback::class.java)
-        val storage = PagedStorage<Int, String>()
+        val storage = PagedStorage<String>()
 
         storage.init(2, createPage("c", "d"), 3, 0, callback)
 
@@ -392,7 +392,7 @@
     @Test
     fun hasPage() {
         val callback = mock(PagedStorage.Callback::class.java)
-        val storage = PagedStorage<Int, String>()
+        val storage = PagedStorage<String>()
 
         storage.init(4, createPage("e"), 0, 0, callback)
 
diff --git a/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt b/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt
new file mode 100644
index 0000000..34e0a57
--- /dev/null
+++ b/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt
@@ -0,0 +1,50 @@
+/*
+ * 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
+
+@RunWith(JUnit4::class)
+class PositionalDataSourceTest {
+    @Test
+    fun computeFirstLoadPositionZero() {
+        assertEquals(0, PositionalDataSource.computeFirstLoadPosition(0, 30, 10, 100))
+    }
+
+    @Test
+    fun computeFirstLoadPositionRequestedPositionIncluded() {
+        assertEquals(10, PositionalDataSource.computeFirstLoadPosition(10, 10, 10, 100))
+    }
+
+    @Test
+    fun computeFirstLoadPositionRound() {
+        assertEquals(10, PositionalDataSource.computeFirstLoadPosition(13, 30, 10, 100))
+    }
+
+    @Test
+    fun computeFirstLoadPositionEndAdjusted() {
+        assertEquals(70, PositionalDataSource.computeFirstLoadPosition(99, 30, 10, 100))
+    }
+
+    @Test
+    fun computeFirstLoadPositionEndAdjustedAndAligned() {
+        assertEquals(70, PositionalDataSource.computeFirstLoadPosition(99, 35, 10, 100))
+    }
+}
diff --git a/paging/common/src/test/java/android/arch/paging/TiledDataSourceTest.kt b/paging/common/src/test/java/android/arch/paging/TiledDataSourceTest.kt
index 2b16fb2..fe8b9e8 100644
--- a/paging/common/src/test/java/android/arch/paging/TiledDataSourceTest.kt
+++ b/paging/common/src/test/java/android/arch/paging/TiledDataSourceTest.kt
@@ -1,3 +1,19 @@
+/*
+ * 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
@@ -5,41 +21,66 @@
 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 {
-    @Test
-    fun loadInitialEmpty() {
+
+    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<Int, String>
-        val dataSource = EmptyDataSource()
-        dataSource.loadRangeInitial(0, 0, 1, 0, receiver)
+        val receiver = mock(PageResult.Receiver::class.java) as PageResult.Receiver<String>
+
+        val callback = DataSource.InitialLoadCallback(
+                DataSource.LOAD_COUNT_REQUIRED_TILED, pageSize, this, receiver)
+
+        this.loadInitial(startPosition, count, pageSize, callback)
 
         @Suppress("UNCHECKED_CAST")
         val argument = ArgumentCaptor.forClass(PageResult::class.java)
-                as ArgumentCaptor<PageResult<Int, String>>
-        verify(receiver).onPageResult(argument.capture())
+                as ArgumentCaptor<PageResult<String>>
+        verify(receiver).onPageResult(eq(PageResult.INIT), argument.capture())
         verifyNoMoreInteractions(receiver)
 
         val observed = argument.value
 
-        assertEquals(PageResult.INIT, observed.type)
-        assertEquals(Collections.EMPTY_LIST, observed.page.items)
+        return observed.page
     }
 
-    class EmptyDataSource : TiledDataSource<String>() {
-        override fun countItems(): Int {
-            return 0
+    @Test
+    fun loadInitialEmpty() {
+        class EmptyDataSource : TiledDataSource<String>() {
+            override fun countItems(): Int {
+                return 0
+            }
+
+            override fun loadRange(startPosition: Int, count: Int): List<String> {
+                return emptyList()
+            }
         }
 
-        override fun loadRange(startPosition: Int, count: Int): List<String> {
-            @Suppress("UNCHECKED_CAST")
-            return Collections.EMPTY_LIST as List<String>
-        }
+        assertEquals(Collections.EMPTY_LIST, EmptyDataSource().loadInitial(0, 1, 5))
     }
-}
\ No newline at end of file
+
+    @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
index 6524008..3e2a60d 100644
--- a/paging/common/src/test/java/android/arch/paging/TiledPagedListTest.kt
+++ b/paging/common/src/test/java/android/arch/paging/TiledPagedListTest.kt
@@ -17,14 +17,14 @@
 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.Assert.fail
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
-import org.mockito.Mockito.any
-import org.mockito.Mockito.eq
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
@@ -43,12 +43,12 @@
         }
     }
 
-    private fun verifyLoadedPages(list: List<Item>, vararg loadedPages: Int, expected: List<Item> = ITEMS) {
+    private fun verifyLoadedPages(list: List<Item>, vararg loadedPages: Int) {
         val loadedPageList = loadedPages.asList()
-        assertEquals(expected.size, list.size)
+        assertEquals(ITEMS.size, list.size)
         for (i in list.indices) {
             if (loadedPageList.contains(i / PAGE_SIZE)) {
-                assertSame("Index $i", expected[i], list[i])
+                assertSame("Index $i", ITEMS[i], list[i])
             } else {
                 assertNull("Index $i", list[i])
             }
@@ -70,21 +70,6 @@
     }
 
     @Test
-    fun computeFirstLoadPosition_zero() {
-        assertEquals(0, TiledPagedList.computeFirstLoadPosition(0, 30, 10, 100))
-    }
-
-    @Test
-    fun computeFirstLoadPosition_requestedPositionIncluded() {
-        assertEquals(0, TiledPagedList.computeFirstLoadPosition(10, 10, 10, 100))
-    }
-
-    @Test
-    fun computeFirstLoadPosition_endAdjusted() {
-        assertEquals(70, TiledPagedList.computeFirstLoadPosition(99, 30, 10, 100))
-    }
-
-    @Test
     fun initialLoad_onePage() {
         val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1)
         verifyLoadedPages(pagedList, 0, 1)
@@ -121,6 +106,56 @@
     }
 
     @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)
@@ -251,16 +286,15 @@
     @Test
     fun placeholdersDisabled() {
         // disable placeholders with config, so we create a contiguous version of the pagedlist
-        val pagedList = PagedList.Builder<Int, Item>()
-                .setDataSource(ListDataSource(ITEMS))
+        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)
-                .setConfig(PagedList.Config.Builder()
-                        .setPageSize(PAGE_SIZE)
-                        .setPrefetchDistance(PAGE_SIZE)
-                        .setInitialLoadSizeHint(PAGE_SIZE)
-                        .setEnablePlaceholders(false)
-                        .build())
                 .setInitialKey(20)
                 .build()
 
@@ -305,8 +339,8 @@
 
         // callbacks posted, since creation often happens on BG thread
         drain()
-        verify(boundaryCallback).onItemAtFrontLoaded(any(), eq(ITEMS[0]), eq(2))
-        verify(boundaryCallback).onItemAtEndLoaded(any(), eq(ITEMS[1]), eq(2))
+        verify(boundaryCallback).onItemAtFrontLoaded(ITEMS[0])
+        verify(boundaryCallback).onItemAtEndLoaded(ITEMS[1])
         verifyNoMoreInteractions(boundaryCallback)
     }
 
@@ -332,8 +366,8 @@
 
         drain()
         // first/last items loaded now, so callbacks dispatched
-        verify(boundaryCallback).onItemAtFrontLoaded(any(), eq(ITEMS.first()), eq(45))
-        verify(boundaryCallback).onItemAtEndLoaded(any(), eq(ITEMS.last()), eq(45))
+        verify(boundaryCallback).onItemAtFrontLoaded(ITEMS.first())
+        verify(boundaryCallback).onItemAtEndLoaded(ITEMS.last())
         verifyNoMoreInteractions(boundaryCallback)
     }
 
@@ -360,11 +394,79 @@
         drain()
 
         // items accessed, so now posted callbacks are run
-        verify(boundaryCallback).onItemAtFrontLoaded(any(), eq(ITEMS.first()), eq(45))
-        verify(boundaryCallback).onItemAtEndLoaded(any(), eq(ITEMS.last()), eq(45))
+        verify(boundaryCallback).onItemAtFrontLoaded(ITEMS.first())
+        verify(boundaryCallback).onItemAtEndLoaded(ITEMS.last())
         verifyNoMoreInteractions(boundaryCallback)
     }
 
+    private fun performInitialLoad(
+            callbackInvoker: (callback: DataSource.InitialLoadCallback<String>) -> Unit) {
+        val dataSource = object: PositionalDataSource<String>() {
+            override fun loadInitial(requestedStartPosition: Int, requestedLoadSize: Int,
+                    pageSize: Int, callback: InitialLoadCallback<String>) {
+                callbackInvoker(callback)
+            }
+            override fun loadRange(startPosition: Int, count: Int, callback: LoadCallback<String>) {
+                fail("loadRange not expected")
+            }
+        }
+        TiledPagedList(
+                dataSource, mMainThread, mBackgroundThread, null,
+                PagedList.Config.Builder()
+                        .setPageSize(PAGE_SIZE)
+                        .build(),
+                0)
+    }
+
+    @Test
+    fun initialLoadCallbackSuccess() = performInitialLoad {
+        // InitialLoadCallback correct usage
+        it.onResult(listOf("a", "b"), 0, 2)
+    }
+
+    @Test
+    fun initialLoadCallbackEmptySuccess() = performInitialLoad {
+        // InitialLoadCallback correct usage - empty special case
+        it.onResult(emptyList())
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun initialLoadCallbackNotPageSizeMultiple() = performInitialLoad {
+        // Positional InitialLoadCallback 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 initialLoadCallbackMissingPlaceholders() = performInitialLoad {
+        // Positional InitialLoadCallback can't accept list-only call
+        it.onResult(listOf("a", "b"))
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun initialLoadCallbackListTooBig() = performInitialLoad {
+        // InitialLoadCallback can't accept pos + list > totalCount
+        it.onResult(listOf("a", "b", "c"), 0, 2)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun initialLoadCallbackPositionTooLarge() = performInitialLoad {
+        // InitialLoadCallback can't accept pos + list > totalCount
+        it.onResult(listOf("a", "b"), 1, 2)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun initialLoadCallbackPositionNegative() = performInitialLoad {
+        // InitialLoadCallback can't accept negative position
+        it.onResult(listOf("a", "b", "c"), -1, 2)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun initialLoadCallbackEmptyCannotHavePlaceholders() = performInitialLoad {
+        // Positional InitialLoadCallback can't accept empty result unless data set is empty
+        it.onResult(emptyList(), 0, 2)
+    }
+
     private fun drain() {
         var executed: Boolean
         do {
diff --git a/paging/integration-tests/testapp/build.gradle b/paging/integration-tests/testapp/build.gradle
index 47f45d1..fb19ba0 100644
--- a/paging/integration-tests/testapp/build.gradle
+++ b/paging/integration-tests/testapp/build.gradle
@@ -27,6 +27,14 @@
         versionCode 1
         versionName "1.0"
     }
+
+    signingConfigs {
+        debug {
+            // Use a local debug keystore to avoid build server issues.
+            storeFile project.rootProject.init.debugKeystore
+        }
+    }
+
     testOptions {
         unitTests.returnDefaultValues = true
     }
@@ -37,17 +45,17 @@
 }
 
 dependencies {
-    compile project(':arch:runtime')
-    compile project(':arch:common')
-    compile project(':paging:common')
-    compile project(':lifecycle:extensions')
-    compile project(':lifecycle:runtime')
-    compile project(':lifecycle:common')
-    compile project(':paging:runtime')
-    compile 'com.android.support:multidex:1.0.1'
+    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 'com.android.support:multidex:1.0.1'
 
-    compile libs.support.recyclerview, libs.support_exclude_config
-    compile libs.support.app_compat, libs.support_exclude_config
+    implementation libs.support.recyclerview, libs.support_exclude_config
+    implementation libs.support.app_compat, libs.support_exclude_config
 }
 
 createAndroidCheckstyle(project)
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
index 4690533..bbbfabb 100644
--- 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
@@ -16,9 +16,10 @@
 
 package android.arch.paging.integration.testapp;
 
-import android.arch.paging.BoundedDataSource;
+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;
@@ -26,7 +27,7 @@
 /**
  * Sample data source with artificial data.
  */
-class ItemDataSource extends BoundedDataSource<Item> {
+class ItemDataSource extends PositionalDataSource<Item> {
     private static final int COUNT = 500;
 
     @ColorInt
@@ -39,18 +40,7 @@
     private static int sGenerationId;
     private final int mGenerationId = sGenerationId++;
 
-    @Override
-    public int countItems() {
-        return COUNT;
-    }
-
-    @Override
-    public List<Item> loadRange(int startPosition, int loadCount) {
-        if (isInvalid()) {
-            // abort!
-            return null;
-        }
-
+    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];
@@ -63,11 +53,38 @@
         for (int i = startPosition; i != end; i++) {
             items.add(new Item(i, "item " + i, bgColor));
         }
-
-        if (isInvalid()) {
-            // abort!
-            return null;
-        }
         return items;
     }
+
+    // TODO: open up this API in PositionalDataSource?
+    private static int computeFirstLoadPosition(int position, int firstLoadSize,
+            int pageSize, int size) {
+        int roundedPageStart = Math.round(position / pageSize) * pageSize;
+
+        // minimum start position is 0
+        roundedPageStart = Math.max(0, roundedPageStart);
+
+        // maximum start pos is that which will encompass end of list
+        int maximumLoadPage = ((size - firstLoadSize + pageSize - 1) / pageSize) * pageSize;
+        roundedPageStart = Math.min(maximumLoadPage, roundedPageStart);
+
+        return roundedPageStart;
+    }
+
+    @Override
+    public void loadInitial(int requestedStartPosition, int requestedLoadSize,
+            int pageSize, @NonNull InitialLoadCallback<Item> callback) {
+        requestedStartPosition = computeFirstLoadPosition(
+                requestedStartPosition, requestedLoadSize, pageSize, COUNT);
+
+        requestedLoadSize = Math.min(COUNT - requestedStartPosition, requestedLoadSize);
+        List<Item> data = loadRangeInternal(requestedStartPosition, requestedLoadSize);
+        callback.onResult(data, requestedStartPosition, COUNT);
+    }
+
+    @Override
+    public void loadRange(int startPosition, int count, @NonNull LoadCallback<Item> callback) {
+        List<Item> data = loadRangeInternal(startPosition, count);
+        callback.onResult(data);
+    }
 }
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
index 974eab9..be14bc1 100644
--- 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
@@ -27,10 +27,24 @@
  */
 @SuppressWarnings("WeakerAccess")
 public class PagedListItemViewModel extends ViewModel {
-    private LiveData<PagedList<Item>> mLivePagedList;
     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) {
@@ -40,22 +54,6 @@
     }
 
     LiveData<PagedList<Item>> getLivePagedList() {
-        if (mLivePagedList == null) {
-            mLivePagedList = new LivePagedListBuilder<Integer, Item>()
-                    .setPagingConfig(20)
-                    .setDataSourceFactory(new DataSource.Factory<Integer, Item>() {
-                        @Override
-                        public DataSource<Integer, Item> create() {
-                            ItemDataSource newDataSource = new ItemDataSource();
-                            synchronized (mDataSourceLock) {
-                                mDataSource = newDataSource;
-                                return mDataSource;
-                            }
-                        }
-                    })
-                    .build();
-        }
-
         return mLivePagedList;
     }
 }
diff --git a/paging/runtime/build.gradle b/paging/runtime/build.gradle
index 6613ee9..187acb9 100644
--- a/paging/runtime/build.gradle
+++ b/paging/runtime/build.gradle
@@ -30,12 +30,12 @@
 }
 
 dependencies {
-    compile project(":arch:runtime")
-    compile project(":paging:common")
-    compile project(":lifecycle:runtime")
-    compile project(':lifecycle:extensions')
+    api project(":arch:runtime")
+    api project(":paging:common")
+    api project(":lifecycle:runtime")
+    api project(':lifecycle:extensions')
 
-    compile libs.support.recyclerview, libs.support_exclude_config
+    api libs.support.recyclerview, libs.support_exclude_config
 
     androidTestImplementation libs.junit
     androidTestImplementation libs.mockito_core,     { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
@@ -49,16 +49,6 @@
 createAndroidCheckstyle(project)
 createKotlinCheckstyle(project)
 
-android.libraryVariants.all { variant ->
-    def name = variant.buildType.name
-    def suffix = name.capitalize()
-    project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        destinationDir new File(project.buildDir, "libJar")
-    }
-}
-
 supportLibrary {
     name = "Android Lifecycle Extensions"
     publish = true
diff --git a/paging/runtime/src/androidTest/java/android/arch/paging/PagedListAdapterHelperTest.kt b/paging/runtime/src/androidTest/java/android/arch/paging/PagedListAdapterHelperTest.kt
index 735a61f..d3a910d 100644
--- a/paging/runtime/src/androidTest/java/android/arch/paging/PagedListAdapterHelperTest.kt
+++ b/paging/runtime/src/androidTest/java/android/arch/paging/PagedListAdapterHelperTest.kt
@@ -42,8 +42,8 @@
     private val mPageLoadingThread = TestExecutor()
 
 
-    private fun <T> createHelper(
-            listUpdateCallback: ListUpdateCallback, diffCallback: DiffCallback<T>): PagedListAdapterHelper<T> {
+    private fun <T> createHelper(listUpdateCallback: ListUpdateCallback,
+                                 diffCallback: DiffCallback<T>): PagedListAdapterHelper<T> {
         return PagedListAdapterHelper(listUpdateCallback,
                 ListAdapterConfig.Builder<T>()
                         .setDiffCallback(diffCallback)
@@ -54,12 +54,10 @@
 
     private fun <V> createPagedListFromListAndPos(
             config: PagedList.Config, data: List<V>, initialKey: Int): PagedList<V> {
-        return PagedList.Builder<Int, V>()
+        return PagedList.Builder<Int, V>(ListDataSource(data), config)
                 .setInitialKey(initialKey)
-                .setConfig(config)
                 .setMainThreadExecutor(mMainThread)
                 .setBackgroundThreadExecutor(mPageLoadingThread)
-                .setDataSource(ListDataSource(data))
                 .build()
     }
 
diff --git a/paging/runtime/src/androidTest/java/android/arch/paging/PagedStorageDiffHelperTest.kt b/paging/runtime/src/androidTest/java/android/arch/paging/PagedStorageDiffHelperTest.kt
index 64501f7..b64b27b 100644
--- a/paging/runtime/src/androidTest/java/android/arch/paging/PagedStorageDiffHelperTest.kt
+++ b/paging/runtime/src/androidTest/java/android/arch/paging/PagedStorageDiffHelperTest.kt
@@ -35,17 +35,17 @@
     @Test
     fun sameListNoUpdates() {
         validateTwoListDiff(
-                PagedStorage(5, createPage("a", "b", "c"), 5),
-                PagedStorage(5, createPage("a", "b", "c"), 5)) {
+                PagedStorage(5, listOf("a", "b", "c"), 5),
+                PagedStorage(5, listOf("a", "b", "c"), 5)) {
             verifyZeroInteractions(it)
         }
     }
 
     @Test
     fun sameListNoUpdatesPlaceholder() {
-        val storageNoPlaceholder = PagedStorage(0, createPage("a", "b", "c"), 10)
+        val storageNoPlaceholder = PagedStorage(0, listOf("a", "b", "c"), 10)
 
-        val storageWithPlaceholder = PagedStorage(0, createPage("a", "b", "c"), 10)
+        val storageWithPlaceholder = PagedStorage(0, listOf("a", "b", "c"), 10)
         storageWithPlaceholder.allocatePlaceholders(3, 0, 3,
                 /* ignored */ mock(PagedStorage.Callback::class.java))
 
@@ -64,8 +64,8 @@
     @Test
     fun appendFill() {
         validateTwoListDiff(
-                PagedStorage(5, createPage("a", "b"), 5),
-                PagedStorage(5, createPage("a", "b", "c"), 4)) {
+                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)
@@ -76,8 +76,8 @@
     @Test
     fun prependFill() {
         validateTwoListDiff(
-                PagedStorage(5, createPage("b", "c"), 5),
-                PagedStorage(4, createPage("a", "b", "c"), 5)) {
+                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);
@@ -88,8 +88,8 @@
     @Test
     fun change() {
         validateTwoListDiff(
-                PagedStorage(5, createPage("a1", "b1", "c1"), 5),
-                PagedStorage(5, createPage("a2", "b1", "c2"), 5)) {
+                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)
@@ -108,12 +108,8 @@
             }
         }
 
-        private fun createPage(vararg items: String): Page<Int, String> {
-            return Page(items.toList())
-        }
-
-        private fun validateTwoListDiff(oldList: PagedStorage<*, String>,
-                                        newList: PagedStorage<*, String>,
+        private fun validateTwoListDiff(oldList: PagedStorage<String>,
+                                        newList: PagedStorage<String>,
                                         validator: (callback: ListUpdateCallback) -> Unit) {
             val diffResult = PagedStorageDiffHelper.computeDiff(
                     oldList, newList, DIFF_CALLBACK)
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 c2e5ec7..48d51bc 100644
--- a/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.kt
+++ b/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.kt
@@ -17,13 +17,13 @@
 package android.arch.paging
 
 class StringPagedList constructor(leadingNulls: Int, trailingNulls: Int, vararg items: String)
-        : PagedList<String>(PagedStorage<Int, String>(), TestExecutor(), TestExecutor(), null,
+        : PagedList<String>(PagedStorage<String>(), TestExecutor(), TestExecutor(), null,
                 PagedList.Config.Builder().setPageSize(1).build()), PagedStorage.Callback {
     init {
         @Suppress("UNCHECKED_CAST")
-        val keyedStorage = mStorage as PagedStorage<Int, String>
+        val keyedStorage = mStorage as PagedStorage<String>
         keyedStorage.init(leadingNulls,
-                Page<Int, String>(null, items.toList(), null),
+                items.toList(),
                 trailingNulls,
                 0,
                 this)
diff --git a/paging/runtime/src/androidTest/java/android/support/v7/recyclerview/extensions/ListAdapterHelperTest.kt b/paging/runtime/src/androidTest/java/android/support/v7/recyclerview/extensions/ListAdapterHelperTest.kt
index 4c06f25..433f4c1 100644
--- a/paging/runtime/src/androidTest/java/android/support/v7/recyclerview/extensions/ListAdapterHelperTest.kt
+++ b/paging/runtime/src/androidTest/java/android/support/v7/recyclerview/extensions/ListAdapterHelperTest.kt
@@ -34,8 +34,8 @@
     private val mMainThread = TestExecutor()
     private val mBackgroundThread = TestExecutor()
 
-    private fun <T> createHelper(
-            listUpdateCallback: ListUpdateCallback, diffCallback: DiffCallback<T>): ListAdapterHelper<T> {
+    private fun <T> createHelper(listUpdateCallback: ListUpdateCallback,
+                                 diffCallback: DiffCallback<T>): ListAdapterHelper<T> {
         return ListAdapterHelper(listUpdateCallback,
                 ListAdapterConfig.Builder<T>()
                         .setDiffCallback(diffCallback)
diff --git a/paging/runtime/src/main/java/android/arch/paging/LivePagedListBuilder.java b/paging/runtime/src/main/java/android/arch/paging/LivePagedListBuilder.java
index ee1810b..b0fddba 100644
--- a/paging/runtime/src/main/java/android/arch/paging/LivePagedListBuilder.java
+++ b/paging/runtime/src/main/java/android/arch/paging/LivePagedListBuilder.java
@@ -25,42 +25,76 @@
 
 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 class LivePagedListBuilder<Key, Value> {
     private Key mInitialLoadKey;
     private PagedList.Config mConfig;
     private DataSource.Factory<Key, Value> mDataSourceFactory;
     private PagedList.BoundaryCallback mBoundaryCallback;
-    private Executor mMainThreadExecutor;
     private Executor mBackgroundThreadExecutor;
 
-    @SuppressWarnings("WeakerAccess")
+    /**
+     * 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;
     }
 
-    @SuppressWarnings("WeakerAccess")
-    @NonNull
-    public LivePagedListBuilder<Key, Value> setPagingConfig(@NonNull PagedList.Config config) {
-        mConfig = config;
-        return this;
-    }
-
-    @SuppressWarnings("WeakerAccess")
-    @NonNull
-    public LivePagedListBuilder<Key, Value> setPagingConfig(int pageSize) {
-        mConfig = new PagedList.Config.Builder().setPageSize(pageSize).build();
-        return this;
-    }
-
-    @NonNull
-    public LivePagedListBuilder<Key, Value> setDataSourceFactory(
-            @NonNull DataSource.Factory<Key, Value> dataSourceFactory) {
-        mDataSourceFactory = dataSourceFactory;
-        return this;
-    }
-
+    /**
+     * Sets a {@link PagedList.BoundaryCallback} on each PagedList created.
+     * <p>
+     * This can be used to
+     *
+     * @param boundaryCallback The boundary callback for listening to PagedList load state.
+     * @return this
+     */
     @SuppressWarnings("unused")
     @NonNull
     public LivePagedListBuilder<Key, Value> setBoundaryCallback(
@@ -69,14 +103,15 @@
         return this;
     }
 
-    @SuppressWarnings("unused")
-    @NonNull
-    public LivePagedListBuilder<Key, Value> setMainThreadExecutor(
-            @NonNull Executor mainThreadExecutor) {
-        mMainThreadExecutor = mainThreadExecutor;
-        return this;
-    }
-
+    /**
+     * Sets executor which will be used for background loading of pages.
+     * <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(
@@ -85,6 +120,14 @@
         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) {
@@ -93,20 +136,17 @@
         if (mDataSourceFactory == null) {
             throw new IllegalArgumentException("DataSource.Factory must be provided");
         }
-        if (mMainThreadExecutor == null) {
-            mMainThreadExecutor = ArchTaskExecutor.getMainThreadExecutor();
-        }
         if (mBackgroundThreadExecutor == null) {
             mBackgroundThreadExecutor = ArchTaskExecutor.getIOThreadExecutor();
         }
 
         return create(mInitialLoadKey, mConfig, mBoundaryCallback, mDataSourceFactory,
-                mMainThreadExecutor, mBackgroundThreadExecutor);
+                ArchTaskExecutor.getMainThreadExecutor(), mBackgroundThreadExecutor);
     }
 
     @AnyThread
     @NonNull
-    public static <Key, Value> LiveData<PagedList<Value>> create(
+    private static <Key, Value> LiveData<PagedList<Value>> create(
             @Nullable final Key initialLoadKey,
             @NonNull final PagedList.Config config,
             @Nullable final PagedList.BoundaryCallback boundaryCallback,
@@ -143,12 +183,10 @@
                     mDataSource = dataSourceFactory.create();
                     mDataSource.addInvalidatedCallback(mCallback);
 
-                    mList = new PagedList.Builder<Key, Value>()
-                            .setDataSource(mDataSource)
+                    mList = new PagedList.Builder<>(mDataSource, config)
                             .setMainThreadExecutor(mainThreadExecutor)
                             .setBackgroundThreadExecutor(backgroundThreadExecutor)
                             .setBoundaryCallback(boundaryCallback)
-                            .setConfig(config)
                             .setInitialKey(initializeKey)
                             .build();
                 } while (mList.isDetached());
diff --git a/paging/runtime/src/main/java/android/arch/paging/LivePagedListProvider.java b/paging/runtime/src/main/java/android/arch/paging/LivePagedListProvider.java
index e0a03cb..44b71a8 100644
--- a/paging/runtime/src/main/java/android/arch/paging/LivePagedListProvider.java
+++ b/paging/runtime/src/main/java/android/arch/paging/LivePagedListProvider.java
@@ -22,45 +22,30 @@
 import android.support.annotation.Nullable;
 import android.support.annotation.WorkerThread;
 
+// NOTE: Room 1.0 depends on this class, so it should not be removed
+// until Room switches to using DataSource.Factory directly
 /**
  * Provides a {@code LiveData<PagedList>}, given a means to construct a DataSource.
  * <p>
  * Return type for data-loading system of an application or library to produce a
  * {@code LiveData<PagedList>}, while leaving the details of the paging mechanism up to the
  * consumer.
- * <p>
- * If you're using Room, it can generate a LivePagedListProvider from a query:
- * <pre>
- * {@literal @}Dao
- * interface UserDao {
- *     {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
- *     public abstract LivePagedListProvider&lt;Integer, User> usersByLastName();
- * }</pre>
- * In the above sample, {@code Integer} is used because it is the {@code Key} type of
- * {@link TiledDataSource}. Currently, Room can only generate a {@code LIMIT}/{@code OFFSET},
- * position based loader that uses TiledDataSource under the hood, and specifying {@code Integer}
- * here lets you pass an initial loading position as an integer.
- * <p>
- * In the future, Room plans to offer other key types to support paging content with a
- * {@link KeyedDataSource}.
  *
  * @param <Key> Type of input valued used to load data from the DataSource. Must be integer if
- *             you're using TiledDataSource.
+ *             you're using PositionalDataSource.
  * @param <Value> Data type produced by the DataSource, and held by the PagedLists.
  *
  * @see PagedListAdapter
  * @see DataSource
  * @see PagedList
  *
- * @deprecated To construct a {@code LiveData<PagedList>}, use {@link LivePagedListBuilder}, which
- * provides the same construction capability with more customization, and better defaults. The role
+ * @deprecated use {@link LivePagedListBuilder} to construct a {@code LiveData<PagedList>}. It
+ * provides the same construction capability with more customization, and simpler defaults. The role
  * of DataSource construction has been separated out to {@link DataSource.Factory} to access or
  * provide a self-invalidating sequence of DataSources. If you were acquiring this from Room, you
- * can switch to having your Dao return a {@link DataSource.Factory} instead, and create a LiveData
- * of PagedList with a {@link LivePagedListBuilder}.
+ * can switch to having your Dao return a {@link DataSource.Factory} instead, and create a
+ * {@code LiveData<PagedList>} with a {@link LivePagedListBuilder}.
  */
-// NOTE: Room 1.0 depends on this class, so it should not be removed
-// until Room switches to using DataSource.Factory directly
 @Deprecated
 public abstract class LivePagedListProvider<Key, Value> implements DataSource.Factory<Key, Value> {
 
@@ -93,10 +78,8 @@
     @AnyThread
     @NonNull
     public LiveData<PagedList<Value>> create(@Nullable Key initialLoadKey, int pageSize) {
-        return new LivePagedListBuilder<Key, Value>()
+        return new LivePagedListBuilder<>(this, pageSize)
                 .setInitialLoadKey(initialLoadKey)
-                .setPagingConfig(pageSize)
-                .setDataSourceFactory(this)
                 .build();
     }
 
@@ -116,10 +99,8 @@
     @NonNull
     public LiveData<PagedList<Value>> create(@Nullable Key initialLoadKey,
             @NonNull PagedList.Config config) {
-        return new LivePagedListBuilder<Key, Value>()
+        return new LivePagedListBuilder<>(this, config)
                 .setInitialLoadKey(initialLoadKey)
-                .setPagingConfig(config)
-                .setDataSourceFactory(this)
                 .build();
     }
 }
diff --git a/paging/runtime/src/main/java/android/arch/paging/PagedListAdapter.java b/paging/runtime/src/main/java/android/arch/paging/PagedListAdapter.java
index 89b9c2e..a8158c2 100644
--- a/paging/runtime/src/main/java/android/arch/paging/PagedListAdapter.java
+++ b/paging/runtime/src/main/java/android/arch/paging/PagedListAdapter.java
@@ -44,18 +44,14 @@
  * {@literal @}Dao
  * interface UserDao {
  *     {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
- *     public abstract LivePagedListProvider&lt;Integer, User> usersByLastName();
+ *     public abstract DataSource.Factory&lt;Integer, User> usersByLastName();
  * }
  *
  * class MyViewModel extends ViewModel {
  *     public final LiveData&lt;PagedList&lt;User>> usersList;
  *     public MyViewModel(UserDao userDao) {
- *         usersList = userDao.usersByLastName().create(
- *                 /* initial load position {@literal *}/ 0,
- *                 new PagedList.Config.Builder()
- *                         .setPageSize(50)
- *                         .setPrefetchDistance(50)
- *                         .build());
+ *         usersList = LivePagedListBuilder&lt;>(
+ *                 userDao.usersByLastName(), /* page size {@literal *}/ 20).build();
  *     }
  * }
  *
diff --git a/paging/runtime/src/main/java/android/arch/paging/PagedListAdapterHelper.java b/paging/runtime/src/main/java/android/arch/paging/PagedListAdapterHelper.java
index 51a6e37..7a0b81a 100644
--- a/paging/runtime/src/main/java/android/arch/paging/PagedListAdapterHelper.java
+++ b/paging/runtime/src/main/java/android/arch/paging/PagedListAdapterHelper.java
@@ -48,18 +48,14 @@
  * {@literal @}Dao
  * interface UserDao {
  *     {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
- *     public abstract LivePagedListProvider&lt;Integer, User> usersByLastName();
+ *     public abstract DataSource.Factory&lt;Integer, User> usersByLastName();
  * }
  *
  * class MyViewModel extends ViewModel {
  *     public final LiveData&lt;PagedList&lt;User>> usersList;
  *     public MyViewModel(UserDao userDao) {
- *         usersList = userDao.usersByLastName().create(
- *                 /* initial load position {@literal *}/ 0,
- *                 new PagedList.Config.Builder()
- *                         .setPageSize(50)
- *                         .setPrefetchDistance(50)
- *                         .build());
+ *         usersList = LivePagedListBuilder&lt;>(
+ *                 userDao.usersByLastName(), /* page size {@literal *}/ 20).build();
  *     }
  * }
  *
diff --git a/paging/runtime/src/main/java/android/arch/paging/PagedStorageDiffHelper.java b/paging/runtime/src/main/java/android/arch/paging/PagedStorageDiffHelper.java
index 6fc7039..d991b72 100644
--- a/paging/runtime/src/main/java/android/arch/paging/PagedStorageDiffHelper.java
+++ b/paging/runtime/src/main/java/android/arch/paging/PagedStorageDiffHelper.java
@@ -26,8 +26,8 @@
     }
 
     static <T> DiffUtil.DiffResult computeDiff(
-            final PagedStorage<?, T> oldList,
-            final PagedStorage<?, T> newList,
+            final PagedStorage<T> oldList,
+            final PagedStorage<T> newList,
             final DiffCallback<T> diffCallback) {
         final int oldOffset = oldList.computeLeadingNulls();
         final int newOffset = newList.computeLeadingNulls();
@@ -131,8 +131,8 @@
      * immediately after dispatching this diff.
      */
     static <T> void dispatchDiff(ListUpdateCallback callback,
-            final PagedStorage<?, T> oldList,
-            final PagedStorage<?, T> newList,
+            final PagedStorage<T> oldList,
+            final PagedStorage<T> newList,
             final DiffUtil.DiffResult diffResult) {
 
         final int trailingOld = oldList.computeTrailingNulls();
diff --git a/persistence/db-framework/build.gradle b/persistence/db-framework/build.gradle
index d1e66d6..8607bf6 100644
--- a/persistence/db-framework/build.gradle
+++ b/persistence/db-framework/build.gradle
@@ -26,27 +26,14 @@
     defaultConfig {
         minSdkVersion flatfoot.min_sdk
     }
-
-    testOptions {
-        unitTests.returnDefaultValues = true
-    }
 }
 
 dependencies {
-    compile libs.support.annotations
-    compile project(":persistence:db")
+    api libs.support.annotations
+    api project(":persistence:db")
 }
-createAndroidCheckstyle(project)
 
-android.libraryVariants.all { variant ->
-    def name = variant.buildType.name
-    def suffix = name.capitalize()
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        destinationDir new File(project.buildDir, "libJar")
-    }
-}
+createAndroidCheckstyle(project)
 
 supportLibrary {
     name = "Android Support SQLite - Framework Implementation"
diff --git a/persistence/db/build.gradle b/persistence/db/build.gradle
index b6c6572..085676d 100644
--- a/persistence/db/build.gradle
+++ b/persistence/db/build.gradle
@@ -26,17 +26,15 @@
     defaultConfig {
         minSdkVersion flatfoot.min_sdk
     }
-
-    testOptions {
-        unitTests.returnDefaultValues = true
-    }
 }
 
 dependencies {
-    compile libs.support.annotations
+    api libs.support.annotations
 }
+
 createAndroidCheckstyle(project)
 
+// Used by testCompile in room-compiler
 android.libraryVariants.all { variant ->
     def name = variant.buildType.name
     def suffix = name.capitalize()
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
index ad7621f..76051c8 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/RoomProcessor.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/RoomProcessor.kt
@@ -34,7 +34,6 @@
 /**
  * The annotation processor for Room.
  */
-@SupportedSourceVersion(SourceVersion.RELEASE_7)
 class RoomProcessor : BasicAnnotationProcessor() {
     override fun initSteps(): MutableIterable<ProcessingStep>? {
         val context = Context(processingEnv)
@@ -45,6 +44,10 @@
         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> {
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
index 85e89d1..d165b55 100644
--- 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
@@ -85,7 +85,8 @@
                 syntaxErrors)
     }
 
-    override fun visitCommon_table_expression(ctx: SQLiteParser.Common_table_expressionContext): Void? {
+    override fun visitCommon_table_expression(
+            ctx: SQLiteParser.Common_table_expressionContext): Void? {
         val tableName = ctx.table_name()?.text
         if (tableName != null) {
             withClauseNames.add(unescapeIdentifier(tableName))
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
index 535e116..f4cd5f3 100644
--- 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
@@ -333,7 +333,8 @@
                                      relationElement: VariableElement)
             : android.arch.persistence.room.vo.Relation? {
 
-        val asTypeElement = MoreTypes.asTypeElement(MoreElements.asVariable(relationElement).asType())
+        val asTypeElement = MoreTypes.asTypeElement(
+                MoreElements.asVariable(relationElement).asType())
 
         if (detectReferenceRecursion(asTypeElement)) {
             return null
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
index 4abf13d..631f511 100644
--- 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
@@ -1808,7 +1808,9 @@
                 @Embedded
                 MyEntity myEntity;
                 """){ _, _ ->
-        }.failsToCompile().withErrorContaining(ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format("foo.bar.MyEntity -> foo.bar.MyEntity"))
+        }.failsToCompile().withErrorContaining(
+                ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+                        "foo.bar.MyEntity -> foo.bar.MyEntity"))
     }
 
     @Test
@@ -1825,7 +1827,9 @@
                     MyEntity myEntity;
                 }
                 """){ _, _ ->
-        }.failsToCompile().withErrorContaining(ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format("foo.bar.MyEntity -> foo.bar.MyEntity.A -> foo.bar.MyEntity"))
+        }.failsToCompile().withErrorContaining(
+                ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+                        "foo.bar.MyEntity -> foo.bar.MyEntity.A -> foo.bar.MyEntity"))
     }
 
     @Test
@@ -1840,7 +1844,9 @@
                     MyEntity myEntity;
                 }
                 """){ _, _ ->
-        }.failsToCompile().withErrorContaining(ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format("foo.bar.MyEntity -> foo.bar.MyEntity.A -> foo.bar.MyEntity"))
+        }.failsToCompile().withErrorContaining(
+                ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+                        "foo.bar.MyEntity -> foo.bar.MyEntity.A -> foo.bar.MyEntity"))
     }
 
     @Test
@@ -1856,7 +1862,9 @@
                     MyEntity myEntity;
                 }
                 """) { _, _ ->
-        }.failsToCompile().withErrorContaining(ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format("foo.bar.MyEntity -> foo.bar.MyEntity.A -> foo.bar.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\"")
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
index 4f14fdb..6fdb300 100644
--- 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
@@ -715,7 +715,9 @@
                 @Embedded
                 MyPojo myPojo;
                 """){ _, _ ->
-        }.failsToCompile().withErrorContaining(ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format("foo.bar.MyPojo -> foo.bar.MyPojo"))
+        }.failsToCompile().withErrorContaining(
+                ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+                        "foo.bar.MyPojo -> foo.bar.MyPojo"))
     }
 
     @Test
@@ -735,7 +737,9 @@
                     MyPojo myPojo;
                 }
                 """){ _, _ ->
-        }.failsToCompile().withErrorContaining(ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format("foo.bar.MyPojo -> foo.bar.MyPojo.MyEntity -> foo.bar.MyPojo"))
+        }.failsToCompile().withErrorContaining(
+                ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+                        "foo.bar.MyPojo -> foo.bar.MyPojo.MyEntity -> foo.bar.MyPojo"))
     }
 
     @Test
@@ -751,7 +755,9 @@
                     MyPojo myPojo;
                 }
                 """){ _, _ ->
-        }.failsToCompile().withErrorContaining(ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format("foo.bar.MyPojo -> foo.bar.MyPojo.A -> foo.bar.MyPojo"))
+        }.failsToCompile().withErrorContaining(
+                ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+                        "foo.bar.MyPojo -> foo.bar.MyPojo.A -> foo.bar.MyPojo"))
     }
 
     @Test
@@ -765,7 +771,9 @@
                     MyPojo myPojo;
                 }
                 """){ _, _ ->
-        }.failsToCompile().withErrorContaining(ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format("foo.bar.MyPojo -> foo.bar.MyPojo.A -> foo.bar.MyPojo"))
+        }.failsToCompile().withErrorContaining(
+                ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+                        "foo.bar.MyPojo -> foo.bar.MyPojo.A -> foo.bar.MyPojo"))
     }
 
     @Test
@@ -783,7 +791,9 @@
                     MyPojo myPojo;
                 }
                 """){ _, _ ->
-        }.failsToCompile().withErrorContaining(ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format("foo.bar.MyPojo -> foo.bar.MyPojo.A -> foo.bar.MyPojo.B -> foo.bar.MyPojo"))
+        }.failsToCompile().withErrorContaining(
+                ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+                        "foo.bar.MyPojo -> foo.bar.MyPojo.A -> foo.bar.MyPojo.B -> foo.bar.MyPojo"))
     }
 
     @Test
@@ -801,7 +811,9 @@
                     A a;
                 }
                 """){ _, _ ->
-        }.failsToCompile().withErrorContaining(ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format("foo.bar.MyPojo.A -> foo.bar.MyPojo.B -> foo.bar.MyPojo.A"))
+        }.failsToCompile().withErrorContaining(
+                ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+                        "foo.bar.MyPojo.A -> foo.bar.MyPojo.B -> foo.bar.MyPojo.A"))
     }
 
     @Test
@@ -825,7 +837,9 @@
                     MyPojo myPojo;
                 }
                 """){ _, _ ->
-        }.failsToCompile().withErrorContaining(ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format("foo.bar.MyPojo -> foo.bar.MyPojo.C -> foo.bar.MyPojo"))
+        }.failsToCompile().withErrorContaining(
+                ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+                        "foo.bar.MyPojo -> foo.bar.MyPojo.C -> foo.bar.MyPojo"))
     }
 
     @Test
@@ -849,7 +863,9 @@
                 static class C {
                 }
                 """){ _, _ ->
-        }.failsToCompile().withErrorContaining(ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format("foo.bar.MyPojo -> foo.bar.MyPojo.A -> foo.bar.MyPojo"))
+        }.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)
diff --git a/room/integration-tests/kotlintestapp/build.gradle b/room/integration-tests/kotlintestapp/build.gradle
index f4df9cf..f1f5139 100644
--- a/room/integration-tests/kotlintestapp/build.gradle
+++ b/room/integration-tests/kotlintestapp/build.gradle
@@ -36,6 +36,14 @@
             }
         }
     }
+
+    signingConfigs {
+        debug {
+            // Use a local debug keystore to avoid build server issues.
+            storeFile project.rootProject.init.debugKeystore
+        }
+    }
+
     testOptions {
         unitTests.returnDefaultValues = true
     }
@@ -59,11 +67,11 @@
     kapt project(":room:compiler")
     kaptAndroidTest project(":room:compiler")
 
-    androidTestCompile(libs.test_runner) {
+    androidTestImplementation(libs.test_runner) {
         exclude module: 'support-annotations'
         exclude module: 'hamcrest-core'
     }
-    androidTestCompile(libs.espresso_core, {
+    androidTestImplementation(libs.espresso_core, {
         exclude group: 'com.android.support', module: 'support-annotations'
         exclude module: "hamcrest-core"
     })
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
index 5c6620d..ec8fd4a 100644
--- 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
@@ -8,9 +8,11 @@
     // 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)
+    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)
+    fun intListToString(ints: List<Int>?): String? =
+            if (ints == null) null else StringUtil.joinIntoString(ints)
 }
diff --git a/room/integration-tests/testapp/build.gradle b/room/integration-tests/testapp/build.gradle
index 6fefc29..b063595 100644
--- a/room/integration-tests/testapp/build.gradle
+++ b/room/integration-tests/testapp/build.gradle
@@ -34,6 +34,14 @@
             }
         }
     }
+
+    signingConfigs {
+        debug {
+            // Use a local debug keystore to avoid build server issues.
+            storeFile project.rootProject.init.debugKeystore
+        }
+    }
+
     testOptions {
         unitTests.returnDefaultValues = true
     }
@@ -47,42 +55,42 @@
 }
 
 dependencies {
-    compile project(":room:common")
-    compile project(":persistence:db")
-    compile project(":persistence:db-framework")
-    compile project(':room:runtime')
-    compile project(':arch:runtime')
-    compile project(':arch:common')
-    compile project(':paging:common')
-    compile project(':lifecycle:extensions')
-    compile project(':lifecycle:runtime')
-    compile project(':lifecycle:common')
-    compile project(':room:rxjava2')
-    compile project(':paging:runtime')
+    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')
 
-    compile libs.support.recyclerview, libs.support_exclude_config
-    compile libs.support.app_compat, libs.support_exclude_config
+    implementation libs.support.recyclerview, libs.support_exclude_config
+    implementation libs.support.app_compat, libs.support_exclude_config
     annotationProcessor project(":room:compiler")
     androidTestAnnotationProcessor project(":room:compiler")
 
     // IJ's gradle integration just cannot figure this out ...
-    androidTestCompile project(':lifecycle:extensions')
-    androidTestCompile project(':lifecycle:common')
-    androidTestCompile project(':lifecycle:runtime')
-    androidTestCompile project(':room:testing')
-    androidTestCompile project(':room:rxjava2')
-    androidTestCompile project(':arch:core-testing')
-    androidTestCompile libs.rx_java
-    androidTestCompile(libs.test_runner) {
+    androidTestImplementation project(':lifecycle:extensions')
+    androidTestImplementation project(':lifecycle:common')
+    androidTestImplementation project(':lifecycle:runtime')
+    androidTestImplementation project(':room:testing')
+    androidTestImplementation project(':room:rxjava2')
+    androidTestImplementation project(':arch:core-testing')
+    androidTestImplementation libs.rx_java
+    androidTestImplementation(libs.test_runner) {
         exclude module: 'support-annotations'
     }
-    androidTestCompile(libs.espresso_core, {
+    androidTestImplementation(libs.espresso_core, {
         exclude group: 'com.android.support', module: 'support-annotations'
     })
     androidTestImplementation libs.mockito_core,     { exclude group: 'net.bytebuddy' } // DexMaker has it's own MockMaker
     androidTestImplementation libs.dexmaker_mockito, { exclude group: 'net.bytebuddy' } // DexMaker has it's own MockMaker
 
-    testCompile libs.junit
+    testImplementation libs.junit
 }
 
 createAndroidCheckstyle(project)
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
index cb2bb03..0b184a9 100644
--- 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
@@ -190,8 +190,12 @@
     @Query("SELECT * FROM user where mAge > :age")
     public abstract LivePagedListProvider<Integer, User> loadPagedByAge_legacy(int age);
 
+    // TODO: switch to PositionalDataSource once Room supports it
     @Query("SELECT * FROM user ORDER BY mAge DESC")
-    public abstract TiledDataSource<User> loadUsersByAgeDesc();
+    public abstract DataSource.Factory<Integer, User> loadUsersByAgeDesc();
+
+    @Query("SELECT * FROM user ORDER BY mAge DESC")
+    public abstract TiledDataSource<User> loadUsersByAgeDesc_legacy();
 
     @Query("DELETE FROM User WHERE mId IN (:ids) AND mAge == :age")
     public abstract int deleteByAgeAndIds(int age, List<Integer> ids);
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
index c546531..b54abe8 100644
--- 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
@@ -63,12 +63,12 @@
         validateUsersAsPagedList(new LivePagedListFactory() {
             @Override
             public LiveData<PagedList<User>> create() {
-                return new LivePagedListBuilder<Integer, User>()
-                        .setPagingConfig(new PagedList.Config.Builder()
+                return new LivePagedListBuilder<>(
+                        mUserDao.loadPagedByAge(3),
+                        new PagedList.Config.Builder()
                                 .setPageSize(10)
                                 .setPrefetchDistance(1)
                                 .setInitialLoadSizeHint(10).build())
-                        .setDataSourceFactory(mUserDao.loadPagedByAge(3))
                         .build();
             }
         });
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
index 8226759..f0285a0 100644
--- 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
@@ -45,8 +45,17 @@
         mUserDao.deleteEverything();
     }
 
+    // TODO: delete this and factory abstraction when LivePagedListProvider is removed
+    @Test
+    public void limitOffsetDataSource_legacyTiledDataSource() {
+        // Simple verification that loading a TiledDataSource still works.
+        LimitOffsetDataSource<User> dataSource =
+                (LimitOffsetDataSource<User>) mUserDao.loadUsersByAgeDesc_legacy();
+        assertThat(dataSource.countItems(), is(0));
+    }
+
     private LimitOffsetDataSource<User> loadUsersByAgeDesc() {
-        return (LimitOffsetDataSource<User>) mUserDao.loadUsersByAgeDesc();
+        return (LimitOffsetDataSource<User>) mUserDao.loadUsersByAgeDesc().create();
     }
 
     @Test
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
index 291cfd6..f076cf1 100644
--- 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
@@ -203,10 +203,8 @@
 
     @Test
     public void pagedList() {
-        LiveData<PagedList<Entity1>> pagedList = new LivePagedListBuilder<Integer, Entity1>()
-                .setDataSourceFactory(mDao.pagedList())
-                .setPagingConfig(10)
-                .build();
+        LiveData<PagedList<Entity1>> pagedList =
+                new LivePagedListBuilder<>(mDao.pagedList(), 10).build();
         observeForever(pagedList);
         drain();
         assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 0 : 0));
@@ -228,6 +226,7 @@
         mDao.insert(new Entity1(2, "bar"));
         drain();
         resetTransactionCount();
+        @SuppressWarnings("deprecation")
         TiledDataSource<Entity1> dataSource = mDao.dataSource();
         dataSource.loadRange(0, 10);
         assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 1 : 0));
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
index 89d16b7..d8cd8d4 100644
--- 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
@@ -83,13 +83,12 @@
 
     private static <K> LiveData<PagedList<Customer>> getLivePagedList(
             K initialLoadKey, DataSource.Factory<K, Customer> dataSourceFactory) {
-        return new LivePagedListBuilder<K, Customer>()
+        PagedList.Config config = new PagedList.Config.Builder()
+                .setPageSize(10)
+                .setEnablePlaceholders(false)
+                .build();
+        return new LivePagedListBuilder<>(dataSourceFactory, config)
                 .setInitialLoadKey(initialLoadKey)
-                .setPagingConfig(new PagedList.Config.Builder()
-                        .setPageSize(10)
-                        .setEnablePlaceholders(false)
-                        .build())
-                .setDataSourceFactory(dataSourceFactory)
                 .build();
     }
 
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
index cdd464e..0c79aee 100644
--- 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
@@ -16,7 +16,6 @@
 
 package android.arch.persistence.room.integration.testapp;
 
-import android.arch.lifecycle.LifecycleRegistry;
 import android.arch.lifecycle.LiveData;
 import android.arch.lifecycle.Observer;
 import android.arch.lifecycle.ViewModelProviders;
@@ -75,7 +74,7 @@
                 mAdapter.setList(items);
             }
         });
-        final Button button = findViewById(R.id.button);
+        final Button button = findViewById(R.id.addButton);
         button.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
@@ -114,11 +113,4 @@
     protected boolean useKeyedQuery() {
         return false;
     }
-
-    private LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
-
-    @Override
-    public LifecycleRegistry getLifecycle() {
-        return mLifecycleRegistry;
-    }
 }
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
index a38d6ae..2db543b 100644
--- 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
@@ -21,6 +21,7 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
@@ -60,7 +61,6 @@
     @Override
     public boolean isInvalid() {
         mDb.getInvalidationTracker().refreshVersionsSync();
-
         return super.isInvalid();
     }
 
@@ -76,30 +76,48 @@
     }
 
     @Override
-    public int countItemsBefore(@NonNull String customerName) {
-        return mCustomerDao.customerNameCountBefore(customerName);
+    public void loadInitial(@Nullable String customerName, int initialLoadSize,
+            boolean enablePlaceholders, @NonNull InitialLoadCallback<Customer> callback) {
+        List<Customer> list;
+        if (customerName != null) {
+            // initial keyed load - load before 'customerName',
+            // and load after last item in before list
+            int pageSize = initialLoadSize / 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(initialLoadSize);
+        }
+
+        if (enablePlaceholders && !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 int countItemsAfter(@NonNull String customerName) {
-        return mCustomerDao.customerNameCountAfter(customerName);
+    public void loadAfter(@NonNull String currentEndKey, int pageSize,
+            @NonNull LoadCallback<Customer> callback) {
+        callback.onResult(mCustomerDao.customerNameLoadAfter(currentEndKey, pageSize));
     }
 
-    @Nullable
     @Override
-    public List<Customer> loadInitial(int pageSize) {
-        return mCustomerDao.customerNameInitial(pageSize);
-    }
-
-    @Nullable
-    @Override
-    public List<Customer> loadBefore(@NonNull String customerName, int pageSize) {
-        return mCustomerDao.customerNameLoadBefore(customerName, pageSize);
-    }
-
-    @Nullable
-    @Override
-    public List<Customer> loadAfter(@Nullable String customerName, int pageSize) {
-        return mCustomerDao.customerNameLoadAfter(customerName, pageSize);
+    public void loadBefore(@NonNull String currentBeginKey, int pageSize,
+            @NonNull LoadCallback<Customer> callback) {
+        List<Customer> list = mCustomerDao.customerNameLoadBefore(currentBeginKey, pageSize);
+        Collections.reverse(list);
+        callback.onResult(list);
     }
 }
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 71dae1b..34e4491 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
@@ -35,7 +35,7 @@
         android:paddingTop="@dimen/activity_vertical_margin"
     />
     <Button
-        android:id="@+id/button"
+        android:id="@+id/addButton"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_alignParentBottom="true"
diff --git a/room/runtime/build.gradle b/room/runtime/build.gradle
index 83c2d74..0570fe8 100644
--- a/room/runtime/build.gradle
+++ b/room/runtime/build.gradle
@@ -30,10 +30,6 @@
     buildTypes.all {
         consumerProguardFiles 'proguard-rules.pro'
     }
-
-    testOptions {
-        unitTests.returnDefaultValues = true
-    }
 }
 
 dependencies {
@@ -41,15 +37,15 @@
     api project(":persistence:db-framework")
     api project(":persistence:db")
     api project(":arch:runtime")
-    provided project(":paging:common")
-    provided project(":lifecycle:runtime")
-    provided project(":lifecycle:extensions")
-    compile libs.support.core_utils, libs.support_exclude_config
+    compileOnly project(":paging:common")
+    compileOnly project(":lifecycle:runtime")
+    compileOnly project(":lifecycle:extensions")
+    api libs.support.core_utils, libs.support_exclude_config
 
-    testCompile project(":arch:core-testing")
-    testCompile libs.junit
-    testCompile libs.mockito_core
-    testCompile libs.support.annotations
+    testImplementation project(":arch:core-testing")
+    testImplementation libs.junit
+    testImplementation libs.mockito_core
+    testImplementation libs.support.annotations
 
     androidTestImplementation libs.junit
     androidTestImplementation libs.test_runner,      { exclude module: 'support-annotations' }
@@ -58,6 +54,7 @@
 
 createAndroidCheckstyle(project)
 
+// Used by testCompile in room-compiler
 android.libraryVariants.all { variant ->
     def name = variant.buildType.name
     def suffix = name.capitalize()
diff --git a/room/rxjava2/build.gradle b/room/rxjava2/build.gradle
index 3600b47..efdfc48 100644
--- a/room/rxjava2/build.gradle
+++ b/room/rxjava2/build.gradle
@@ -26,35 +26,21 @@
     defaultConfig {
         minSdkVersion flatfoot.min_sdk
     }
-
-    testOptions {
-        unitTests.returnDefaultValues = true
-    }
 }
 
 dependencies {
-    compile project(":room:common")
-    compile project(":room:runtime")
-    compile project(":arch:runtime")
-    compile libs.support.core_utils, libs.support_exclude_config
-    compile libs.rx_java
-    testCompile libs.junit
-    testCompile libs.mockito_core
-    testCompile project(":arch:core-testing")
+    api project(":room:common")
+    api project(":room:runtime")
+    api project(":arch:runtime")
+    api libs.support.core_utils, libs.support_exclude_config
+    api libs.rx_java
+    testImplementation libs.junit
+    testImplementation libs.mockito_core
+    testImplementation project(":arch:core-testing")
 }
 
 createAndroidCheckstyle(project)
 
-android.libraryVariants.all { variant ->
-    def name = variant.buildType.name
-    def suffix = name.capitalize()
-    project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        destinationDir new File(project.buildDir, "libJar")
-    }
-}
-
 supportLibrary {
     name = "Android Room RXJava2"
     publish = true
diff --git a/room/testing/build.gradle b/room/testing/build.gradle
index cc506a0..876300a 100644
--- a/room/testing/build.gradle
+++ b/room/testing/build.gradle
@@ -26,35 +26,21 @@
     defaultConfig {
         minSdkVersion flatfoot.min_sdk
     }
-
-    testOptions {
-        unitTests.returnDefaultValues = true
-    }
 }
 
 dependencies {
-    compile project(":room:common")
-    compile project(":room:runtime")
-    compile project(":persistence:db")
-    compile project(":persistence:db-framework")
-    compile project(":room:migration")
-    compile project(":arch:runtime")
-    compile libs.support.core_utils, libs.support_exclude_config
-    compile libs.junit
+    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 libs.support.core_utils, libs.support_exclude_config
+    api libs.junit
 }
 
 createAndroidCheckstyle(project)
 
-android.libraryVariants.all { variant ->
-    def name = variant.buildType.name
-    def suffix = name.capitalize()
-    project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        destinationDir new File(project.buildDir, "libJar")
-    }
-}
-
 supportLibrary {
     name = "Android Room Testing"
     publish = true
diff --git a/samples/SupportDesignDemos/src/main/res/layout/design_text_input.xml b/samples/SupportDesignDemos/src/main/res/layout/design_text_input.xml
index 6dbbb92..fc9ff00 100644
--- a/samples/SupportDesignDemos/src/main/res/layout/design_text_input.xml
+++ b/samples/SupportDesignDemos/src/main/res/layout/design_text_input.xml
@@ -96,6 +96,22 @@
         </android.support.design.widget.TextInputLayout>
 
         <android.support.design.widget.TextInputLayout
+            android:id="@+id/input_auto_complete"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="8dp"
+            app:counterEnabled="true"
+            app:counterMaxLength="30">
+
+            <AutoCompleteTextView
+                android:id="@+id/edit_auto_complete"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:hint="@string/form_auto_complete" />
+
+        </android.support.design.widget.TextInputLayout>
+
+        <android.support.design.widget.TextInputLayout
             android:id="@+id/input_password"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
diff --git a/samples/SupportDesignDemos/src/main/res/values/strings.xml b/samples/SupportDesignDemos/src/main/res/values/strings.xml
index b7ad381..e1818af 100644
--- a/samples/SupportDesignDemos/src/main/res/values/strings.xml
+++ b/samples/SupportDesignDemos/src/main/res/values/strings.xml
@@ -70,6 +70,7 @@
     <string name="form_username">Username</string>
     <string name="form_email">Email address</string>
     <string name="form_description">Description</string>
+    <string name="form_auto_complete">Auto complete</string>
     <string name="form_password">Password field</string>
     <string name="show_error">Show error</string>
     <string name="toggle_enabled">Toggle enabled</string>
diff --git a/samples/SupportPreferenceDemos/src/main/res/xml/preferences.xml b/samples/SupportPreferenceDemos/src/main/res/xml/preferences.xml
index f469af2..4d1dca7 100644
--- a/samples/SupportPreferenceDemos/src/main/res/xml/preferences.xml
+++ b/samples/SupportPreferenceDemos/src/main/res/xml/preferences.xml
@@ -31,6 +31,12 @@
         android:title="@string/title_stylish_preference"
         android:summary="@string/summary_stylish_preference" />
 
+    <Preference
+        android:key="preference_with_icon"
+        android:title="Preference with icon"
+        android:summary="This preference has an icon"
+        android:icon="@android:drawable/ic_menu_camera" />
+
     <PreferenceCategory
         android:title="@string/inline_preferences">
 
@@ -39,6 +45,11 @@
             android:title="@string/title_checkbox_preference"
             android:summary="@string/summary_checkbox_preference" />
 
+        <SwitchPreference
+            android:key="switch_preference"
+            android:title="Switch preference"
+            android:summary="This is a switch" />
+
         <DropDownPreference
             android:key="dropdown_preference"
             android:title="@string/title_dropdown_preference"
diff --git a/transition/src/main/java/android/support/transition/package.html b/transition/src/main/java/android/support/transition/package.html
index b09005f..d8394a5 100644
--- a/transition/src/main/java/android/support/transition/package.html
+++ b/transition/src/main/java/android/support/transition/package.html
@@ -1,6 +1,6 @@
 <body>
 
-Support android.transition classes to provide transition API back to android API level 14.
+Support android.transition classes to provide transition API back to Android API level 14.
 This library contains {@link android.support.transition.Transition},
 {@link android.support.transition.TransitionManager}, and other related classes
 back-ported from their platform versions introduced Android API level 19.
diff --git a/v13/java/android/support/v13/app/package.html b/v13/java/android/support/v13/app/package.html
deleted file mode 100755
index 3557ecb..0000000
--- a/v13/java/android/support/v13/app/package.html
+++ /dev/null
@@ -1,5 +0,0 @@
-<body>
-
-Support classes to access some of the android.app package features introduced after API level 13 in a backwards compatible fashion.
-
-</body>
diff --git a/v14/preference/res/layout-v21/preference_material.xml b/v14/preference/res/layout-v21/preference_material.xml
index 197e429..c3000f9 100644
--- a/v14/preference/res/layout-v21/preference_material.xml
+++ b/v14/preference/res/layout-v21/preference_material.xml
@@ -65,6 +65,7 @@
             android:layout_below="@android:id/title"
             android:layout_alignStart="@android:id/title"
             android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+            android:textAlignment="viewStart"
             android:textColor="?android:attr/textColorSecondary"
             android:maxLines="10" />
 
diff --git a/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatAutoCompleteTextView.java b/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatAutoCompleteTextView.java
index 5b0a2f8..e41bec7 100644
--- a/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatAutoCompleteTextView.java
+++ b/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatAutoCompleteTextView.java
@@ -29,6 +29,8 @@
 import android.support.v7.appcompat.R;
 import android.support.v7.content.res.AppCompatResources;
 import android.util.AttributeSet;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
 import android.widget.AutoCompleteTextView;
 
 /**
@@ -177,4 +179,10 @@
             mTextHelper.onSetTextAppearance(context, resId);
         }
     }
+
+    @Override
+    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+        return AppCompatHintHelper.onCreateInputConnection(super.onCreateInputConnection(outAttrs),
+                outAttrs, this);
+    }
 }
diff --git a/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatCheckedTextView.java b/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatCheckedTextView.java
index 921f0a2..dca409c 100644
--- a/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatCheckedTextView.java
+++ b/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatCheckedTextView.java
@@ -20,6 +20,8 @@
 import android.support.annotation.DrawableRes;
 import android.support.v7.content.res.AppCompatResources;
 import android.util.AttributeSet;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
 import android.widget.CheckedTextView;
 
 /**
@@ -79,4 +81,10 @@
             mTextHelper.applyCompoundDrawablesTints();
         }
     }
+
+    @Override
+    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+        return AppCompatHintHelper.onCreateInputConnection(super.onCreateInputConnection(outAttrs),
+                outAttrs, this);
+    }
 }
diff --git a/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatEditText.java b/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatEditText.java
index 406e364..6831fcb 100644
--- a/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatEditText.java
+++ b/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatEditText.java
@@ -28,6 +28,8 @@
 import android.support.v4.view.TintableBackgroundView;
 import android.support.v7.appcompat.R;
 import android.util.AttributeSet;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
 import android.widget.EditText;
 
 /**
@@ -159,4 +161,10 @@
             mTextHelper.onSetTextAppearance(context, resId);
         }
     }
+
+    @Override
+    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+        return AppCompatHintHelper.onCreateInputConnection(super.onCreateInputConnection(outAttrs),
+                outAttrs, this);
+    }
 }
diff --git a/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatHintHelper.java b/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatHintHelper.java
new file mode 100644
index 0000000..0d30fb7
--- /dev/null
+++ b/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatHintHelper.java
@@ -0,0 +1,43 @@
+/*
+ * 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.support.v7.widget;
+
+import android.view.View;
+import android.view.ViewParent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+class AppCompatHintHelper {
+
+    static InputConnection onCreateInputConnection(InputConnection ic, EditorInfo outAttrs,
+            View view) {
+        if (ic != null && outAttrs.hintText == null) {
+            // If we don't have a hint and the parent implements WithHint, use its hint for the
+            // EditorInfo. This allows us to display a hint in 'extract mode'.
+            ViewParent parent = view.getParent();
+            while (parent instanceof View) {
+                if (parent instanceof WithHint) {
+                    outAttrs.hintText = ((WithHint) parent).getHint();
+                    break;
+                }
+                parent = parent.getParent();
+            }
+        }
+        return ic;
+    }
+
+}
diff --git a/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatMultiAutoCompleteTextView.java b/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatMultiAutoCompleteTextView.java
index 8060d7d..b71b08a 100644
--- a/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatMultiAutoCompleteTextView.java
+++ b/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatMultiAutoCompleteTextView.java
@@ -29,6 +29,8 @@
 import android.support.v7.appcompat.R;
 import android.support.v7.content.res.AppCompatResources;
 import android.util.AttributeSet;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
 import android.widget.MultiAutoCompleteTextView;
 
 /**
@@ -177,4 +179,10 @@
             mTextHelper.onSetTextAppearance(context, resId);
         }
     }
+
+    @Override
+    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+        return AppCompatHintHelper.onCreateInputConnection(super.onCreateInputConnection(outAttrs),
+                outAttrs, this);
+    }
 }
diff --git a/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatTextView.java b/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatTextView.java
index cfa6a2a..d813277 100644
--- a/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatTextView.java
+++ b/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatTextView.java
@@ -31,6 +31,8 @@
 import android.support.v4.widget.TextViewCompat;
 import android.support.v7.appcompat.R;
 import android.util.AttributeSet;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
 import android.widget.TextView;
 
 /**
@@ -361,4 +363,10 @@
         }
         return new int[0];
     }
+
+    @Override
+    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+        return AppCompatHintHelper.onCreateInputConnection(super.onCreateInputConnection(outAttrs),
+                outAttrs, this);
+    }
 }
diff --git a/v7/appcompat/src/main/java/android/support/v7/widget/Toolbar.java b/v7/appcompat/src/main/java/android/support/v7/widget/Toolbar.java
index 45e2583..f383e90 100644
--- a/v7/appcompat/src/main/java/android/support/v7/widget/Toolbar.java
+++ b/v7/appcompat/src/main/java/android/support/v7/widget/Toolbar.java
@@ -56,6 +56,7 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewParent;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.TextView;
@@ -2366,12 +2367,20 @@
         @Override
         public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) {
             ensureCollapseButtonView();
-            if (mCollapseButtonView.getParent() != Toolbar.this) {
+            ViewParent collapseButtonParent = mCollapseButtonView.getParent();
+            if (collapseButtonParent != Toolbar.this) {
+                if (collapseButtonParent instanceof ViewGroup) {
+                    ((ViewGroup) collapseButtonParent).removeView(mCollapseButtonView);
+                }
                 addView(mCollapseButtonView);
             }
             mExpandedActionView = item.getActionView();
             mCurrentExpandedItem = item;
-            if (mExpandedActionView.getParent() != Toolbar.this) {
+            ViewParent expandedActionParent = mExpandedActionView.getParent();
+            if (expandedActionParent != Toolbar.this) {
+                if (expandedActionParent instanceof ViewGroup) {
+                    ((ViewGroup) expandedActionParent).removeView(mExpandedActionView);
+                }
                 final LayoutParams lp = generateDefaultLayoutParams();
                 lp.gravity = GravityCompat.START | (mButtonGravity & Gravity.VERTICAL_GRAVITY_MASK);
                 lp.mViewType = LayoutParams.EXPANDED;
diff --git a/v7/appcompat/src/main/java/android/support/v7/widget/WithHint.java b/v7/appcompat/src/main/java/android/support/v7/widget/WithHint.java
new file mode 100644
index 0000000..d14f483
--- /dev/null
+++ b/v7/appcompat/src/main/java/android/support/v7/widget/WithHint.java
@@ -0,0 +1,36 @@
+/*
+ * 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.support.v7.widget;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public interface WithHint {
+    /**
+     * Returns the hint which is displayed in the floating label, if enabled.
+     *
+     * @return the hint, or null if there isn't one set, or the hint is not enabled.
+     */
+    @Nullable
+    CharSequence getHint();
+}
diff --git a/v7/mediarouter/src/android/support/v7/media/package.html b/v7/mediarouter/src/android/support/v7/media/package.html
index 0866a42..be2aaf2 100644
--- a/v7/mediarouter/src/android/support/v7/media/package.html
+++ b/v7/mediarouter/src/android/support/v7/media/package.html
@@ -4,7 +4,6 @@
 
 <p>Contains APIs that control the routing of media channels and streams from the current device
   to external speakers and destination devices.</p>
-<p>Compatible with API level 7 and higher.</p>
 
 </body>
 </html>
diff --git a/v7/preference/Android.mk b/v7/preference/Android.mk
index 63f9ff8..5a3f57a 100644
--- a/v7/preference/Android.mk
+++ b/v7/preference/Android.mk
@@ -40,4 +40,5 @@
 LOCAL_JAR_EXCLUDE_FILES := none
 LOCAL_JAVA_LANGUAGE_VERSION := 1.7
 LOCAL_AAPT_FLAGS := --add-javadoc-annotation doconly
+LOCAL_EXPORT_PROGUARD_FLAG_FILES := proguard-rules.pro
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/v7/preference/src/main/java/android/support/v7/preference/CollapsiblePreferenceGroupController.java b/v7/preference/src/main/java/android/support/v7/preference/CollapsiblePreferenceGroupController.java
index e15ca18..b63ff75 100644
--- a/v7/preference/src/main/java/android/support/v7/preference/CollapsiblePreferenceGroupController.java
+++ b/v7/preference/src/main/java/android/support/v7/preference/CollapsiblePreferenceGroupController.java
@@ -166,7 +166,7 @@
             CharSequence summary = null;
             for (int i = collapsedIndex; i < flattenedPreferenceList.size(); i++) {
                 final Preference preference = flattenedPreferenceList.get(i);
-                if (preference instanceof PreferenceGroup) {
+                if (preference instanceof PreferenceGroup || !preference.isVisible()) {
                     continue;
                 }
                 final CharSequence title = preference.getTitle();
diff --git a/v7/preference/tests/src/android/support/v7/preference/PreferenceGroupInitialExpandedChildrenCountTest.java b/v7/preference/tests/src/android/support/v7/preference/PreferenceGroupInitialExpandedChildrenCountTest.java
index 4f53b9a..499e2c1 100644
--- a/v7/preference/tests/src/android/support/v7/preference/PreferenceGroupInitialExpandedChildrenCountTest.java
+++ b/v7/preference/tests/src/android/support/v7/preference/PreferenceGroupInitialExpandedChildrenCountTest.java
@@ -188,6 +188,26 @@
     }
 
     /**
+     * Verifies that summary for the expand button only lists visible preferences.
+     */
+    @Test
+    @UiThreadTest
+    public void createPreferenceGroupAdapter_expandButtonSummaryShouldListVisiblePreferencesOnly() {
+        mScreen.setInitialExpandedChildrenCount(INITIAL_EXPANDED_COUNT);
+        mPreferenceList.get(INITIAL_EXPANDED_COUNT + 1).setVisible(false);
+        mPreferenceList.get(INITIAL_EXPANDED_COUNT + 4).setVisible(false);
+        PreferenceGroupAdapter preferenceGroupAdapter = new PreferenceGroupAdapter(mScreen);
+        // Preference 5 to Preference 9 are collapsed, only preferences 5, 7, 8 are visible
+        CharSequence summary = mPreferenceList.get(INITIAL_EXPANDED_COUNT).getTitle();
+        summary = mContext.getString(R.string.summary_collapsed_preference_list,
+                summary, mPreferenceList.get(INITIAL_EXPANDED_COUNT + 2).getTitle());
+        summary = mContext.getString(R.string.summary_collapsed_preference_list,
+                summary, mPreferenceList.get(INITIAL_EXPANDED_COUNT + 3).getTitle());
+        final Preference expandButton = preferenceGroupAdapter.getItem(INITIAL_EXPANDED_COUNT);
+        assertEquals(summary, expandButton.getSummary());
+    }
+
+    /**
      * Verifies that clicking the expand button will show all preferences.
      */
     @Test
diff --git a/v7/recyclerview/Android.mk b/v7/recyclerview/Android.mk
index d42e1b4..a62c3cd 100644
--- a/v7/recyclerview/Android.mk
+++ b/v7/recyclerview/Android.mk
@@ -37,4 +37,5 @@
 LOCAL_JAR_EXCLUDE_FILES := none
 LOCAL_JAVA_LANGUAGE_VERSION := 1.7
 LOCAL_AAPT_FLAGS := --add-javadoc-annotation doconly
+LOCAL_EXPORT_PROGUARD_FLAG_FILES := proguard-rules.pro
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/v7/recyclerview/build.gradle b/v7/recyclerview/build.gradle
index 0dfb276..daac4ab 100644
--- a/v7/recyclerview/build.gradle
+++ b/v7/recyclerview/build.gradle
@@ -31,10 +31,6 @@
         main.res.srcDirs 'res', 'res-public'
     }
 
-    testOptions {
-        unitTests.returnDefaultValues = true
-    }
-
     buildTypes.all {
         consumerProguardFiles 'proguard-rules.pro'
     }
diff --git a/v7/recyclerview/src/main/java/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/main/java/android/support/v7/widget/RecyclerView.java
index 4bc17a8..5bd275f 100644
--- a/v7/recyclerview/src/main/java/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/main/java/android/support/v7/widget/RecyclerView.java
@@ -386,8 +386,8 @@
     private List<OnChildAttachStateChangeListener> mOnChildAttachStateListeners;
 
     /**
-     * Set to true when an adapter data set changed notification is received.
-     * In that case, we cannot run any animations since we don't know what happened until layout.
+     * True after an event occurs that signals that the entire data set has changed. In that case,
+     * we cannot run any animations since we don't know what happened until layout.
      *
      * Attached items are invalid until next layout, at which point layout will animate/replace
      * items as necessary, building up content from the (effectively) new adapter from scratch.
@@ -395,11 +395,20 @@
      * Cached items must be discarded when setting this to true, so that the cache may be freely
      * used by prefetching until the next layout occurs.
      *
-     * @see #setDataSetChangedAfterLayout()
+     * @see #processDataSetCompletelyChanged(boolean)
      */
     boolean mDataSetHasChangedAfterLayout = false;
 
     /**
+     * True after the data set has completely changed and
+     * {@link LayoutManager#onItemsChanged(RecyclerView)} should be called during the subsequent
+     * measure/layout.
+     *
+     * @see #processDataSetCompletelyChanged(boolean)
+     */
+    boolean mDispatchItemsChangedEvent = false;
+
+    /**
      * This variable is incremented during a dispatchLayout and/or scroll.
      * Some methods should not be called during these periods (e.g. adapter data change).
      * Doing so will create hard to find bugs so we better check it and throw an exception.
@@ -1044,6 +1053,7 @@
         // bail out if layout is frozen
         setLayoutFrozen(false);
         setAdapterInternal(adapter, true, removeAndRecycleExistingViews);
+        processDataSetCompletelyChanged(true);
         requestLayout();
     }
     /**
@@ -1059,6 +1069,7 @@
         // bail out if layout is frozen
         setLayoutFrozen(false);
         setAdapterInternal(adapter, false, true);
+        processDataSetCompletelyChanged(false);
         requestLayout();
     }
 
@@ -1112,7 +1123,6 @@
         }
         mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
         mState.mStructureChanged = true;
-        setDataSetChangedAfterLayout();
     }
 
     /**
@@ -2509,9 +2519,17 @@
         if (next == null || next == this) {
             return false;
         }
+        // panic, result view is not a child anymore, maybe workaround b/37864393
+        if (findContainingItemView(next) == null) {
+            return false;
+        }
         if (focused == null) {
             return true;
         }
+        // panic, focused view is not a child anymore, maybe workaround b/37864393
+        if (findContainingItemView(focused) == null) {
+            return true;
+        }
 
         mTempRect.set(0, 0, focused.getWidth(), focused.getHeight());
         mTempRect2.set(0, 0, next.getWidth(), next.getHeight());
@@ -3398,7 +3416,9 @@
             // Processing these items have no value since data set changed unexpectedly.
             // Instead, we just reset it.
             mAdapterHelper.reset();
-            mLayout.onItemsChanged(this);
+            if (mDispatchItemsChangedEvent) {
+                mLayout.onItemsChanged(this);
+            }
         }
         // simple animations are a subset of advanced animations (which will cause a
         // pre-layout step)
@@ -3821,6 +3841,7 @@
         mLayout.removeAndRecycleScrapInt(mRecycler);
         mState.mPreviousLayoutItemCount = mState.mItemCount;
         mDataSetHasChangedAfterLayout = false;
+        mDispatchItemsChangedEvent = false;
         mState.mRunSimpleAnimations = false;
 
         mState.mRunPredictiveAnimations = false;
@@ -4288,19 +4309,21 @@
                 viewHolder.getUnmodifiedPayloads());
     }
 
-
     /**
-     * Call this method to signal that *all* adapter content has changed (generally, because of
-     * setAdapter, swapAdapter, or notifyDataSetChanged), and that once layout occurs, all
-     * attached items should be discarded or animated.
+     * Processes the fact that, as far as we can tell, the data set has completely changed.
      *
-     * Attached items are labeled as invalid, and all cached items are discarded.
+     * <ul>
+     *   <li>Once layout occurs, all attached items should be discarded or animated.
+     *   <li>Attached items are labeled as invalid.
+     *   <li>Because items may still be prefetched between a "data set completely changed"
+     *       event and a layout event, all cached items are discarded.
+     * </ul>
      *
-     * It is still possible for items to be prefetched while mDataSetHasChangedAfterLayout == true,
-     * so this method must always discard all cached views so that the only valid items that remain
-     * in the cache, once layout occurs, are valid prefetched items.
+     * @param dispatchItemsChanged Whether to call
+     * {@link LayoutManager#onItemsChanged(RecyclerView)} during measure/layout.
      */
-    void setDataSetChangedAfterLayout() {
+    void processDataSetCompletelyChanged(boolean dispatchItemsChanged) {
+        mDispatchItemsChangedEvent |= dispatchItemsChanged;
         mDataSetHasChangedAfterLayout = true;
         markKnownViewsInvalid();
     }
@@ -5110,7 +5133,7 @@
             assertNotInLayoutOrScroll(null);
             mState.mStructureChanged = true;
 
-            setDataSetChangedAfterLayout();
+            processDataSetCompletelyChanged(true);
             if (!mAdapterHelper.hasPendingUpdates()) {
                 requestLayout();
             }
@@ -9491,9 +9514,11 @@
         }
 
         /**
-         * Called if the RecyclerView this LayoutManager is bound to has a different adapter set.
-         * The LayoutManager may use this opportunity to clear caches and configure state such
-         * that it can relayout appropriately with the new data and potentially new view types.
+         * Called if the RecyclerView this LayoutManager is bound to has a different adapter set via
+         * {@link RecyclerView#setAdapter(Adapter)} or
+         * {@link RecyclerView#swapAdapter(Adapter, boolean)}. The LayoutManager may use this
+         * opportunity to clear caches and configure state such that it can relayout appropriately
+         * with the new data and potentially new view types.
          *
          * <p>The default implementation removes all currently attached views.</p>
          *
@@ -9535,8 +9560,9 @@
         }
 
         /**
-         * Called when {@link Adapter#notifyDataSetChanged()} is triggered instead of giving
-         * detailed information on what has actually changed.
+         * Called in response to a call to {@link Adapter#notifyDataSetChanged()} or
+         * {@link RecyclerView#swapAdapter(Adapter, boolean)} ()} and signals that the the entire
+         * data set has changed.
          *
          * @param recyclerView
          */
@@ -10042,7 +10068,7 @@
             if (vScroll == 0 && hScroll == 0) {
                 return false;
             }
-            mRecyclerView.scrollBy(hScroll, vScroll);
+            mRecyclerView.smoothScrollBy(hScroll, vScroll);
             return true;
         }
 
diff --git a/v7/recyclerview/tests/src/android/support/v7/util/ImeCleanUpTestRule.java b/v7/recyclerview/tests/src/android/support/v7/util/ImeCleanUpTestRule.java
index f4caad3..32e1295 100644
--- a/v7/recyclerview/tests/src/android/support/v7/util/ImeCleanUpTestRule.java
+++ b/v7/recyclerview/tests/src/android/support/v7/util/ImeCleanUpTestRule.java
@@ -16,8 +16,12 @@
 
 package android.support.v7.util;
 
+import android.app.Instrumentation;
 import android.graphics.Rect;
+import android.support.annotation.NonNull;
 import android.support.testutils.PollingCheck;
+import android.support.v7.widget.TestActivity;
+import android.view.KeyEvent;
 import android.view.View;
 
 import org.junit.rules.TestRule;
@@ -25,13 +29,22 @@
 import org.junit.runners.model.Statement;
 
 /**
- * A JUnit rule that ensures that IME is closed after a test is finished (or exception thrown).
- * A test that triggers IME open/close should call setContainerView with the activity's container
- * view in order to trigger this cleanup at the end of the test. Otherwise, no cleanup happens.
+ * A JUnit rule that ensures that IME is closed after a test is finished by determining if the
+ * keyboard is open, and if it is closing it.  If the rules determines the keyboard is open and is
+ * unable to close it within a timeout (see source), an exception will be thrown.
+ *
+ * A test that wants to benefit from this functionality must call
+ * {@link #setup(TestActivity, Instrumentation)} with the {@link TestActivity} under test and the
+ * test's {@link Instrumentation}, or this rule does nothing.
  */
 public class ImeCleanUpTestRule implements TestRule {
 
+    // We consider the keyboard open if its height is at least this percentage of the available
+    // screen height.
+    private static final float KEYBOARD_HEIGHT_TO_SCREEN_RATIO = .15f;
+
     private View mContainerView;
+    private Instrumentation mInstrumentation;
 
     @Override
     public Statement apply(final Statement base, Description description) {
@@ -48,39 +61,41 @@
     }
 
     /**
-     * Sets the container view used to calculate the total screen height and the height available
-     * to the test activity.
+     * Call to enable the functionality of this TestRule.
+     * @param testActivity The {@link TestActivity} under test.
+     * @param instrumentation The test's {@link Instrumentation}.
      */
-    public void setContainerView(View containerView) {
-        mContainerView = containerView;
+    public void setup(@NonNull TestActivity testActivity,
+            @NonNull Instrumentation instrumentation) {
+        mContainerView = testActivity.getContainer();
+        mInstrumentation = instrumentation;
     }
 
     private void closeImeIfOpen() {
-        if (mContainerView == null) {
+        if (mContainerView == null || mInstrumentation == null) {
             return;
         }
-        // Ensuring that IME is closed after starting each test.
+
         final Rect r = new Rect();
         mContainerView.getWindowVisibleDisplayFrame(r);
+
         // This is the entire height of the screen available to both the view and IME
-        final int screenHeight = mContainerView.getRootView().getHeight();
+        final int screenHeight = mContainerView.getHeight();
 
         // r.bottom is the position above IME if it's open or device button.
         // if IME is shown, r.bottom is smaller than screenHeight.
         int imeHeight = screenHeight - r.bottom;
 
-        // Picking a threshold to detect when IME is open
-        if (imeHeight > screenHeight * 0.15) {
-            // Soft keyboard is shown, will wait for it to close after running the test. Note that
-            // we don't press back button here as the IME should close by itself when a test
-            // finishes. If the wait isn't done here, the IME can mess up with the layout of the
-            // next test.
+        if (imeHeight > screenHeight * KEYBOARD_HEIGHT_TO_SCREEN_RATIO) {
+            // Soft keyboard is shown, therefore we click the back button to close it and wait for
+            // it to be closed.
+            mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
             PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
                 @Override
                 public boolean canProceed() {
                     mContainerView.getWindowVisibleDisplayFrame(r);
                     int imeHeight = screenHeight - r.bottom;
-                    return imeHeight < screenHeight * 0.15;
+                    return imeHeight < screenHeight * KEYBOARD_HEIGHT_TO_SCREEN_RATIO;
                 }
             });
         }
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java
index 23eaf52..5d378ff 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java
@@ -184,7 +184,7 @@
         final int spanCount = 3;
         final int itemCount = 100;
 
-        imeCleanUp.setContainerView(getActivity().getContainer());
+        imeCleanUp.setup(getActivity(), getInstrumentation());
         RecyclerView recyclerView = new WrappedRecyclerView(getActivity());
         GridEditTextAdapter editTextAdapter = new GridEditTextAdapter(itemCount) {
             @Override
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
index 91d0dbf..4dd0d8f 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
@@ -82,7 +82,7 @@
         // The condition for this test is setting RV's height to a non-exact height, so that measure
         // is called twice (once with the larger height and another time with smaller height when
         // the keyboard shows up). To ensure this resizing of RV, SOFT_INPUT_ADJUST_RESIZE is set.
-        imeCleanUp.setContainerView(getActivity().getContainer());
+        imeCleanUp.setup(getActivity(), getInstrumentation());
         final LinearLayout container = new LinearLayout(getActivity());
         container.setOrientation(LinearLayout.VERTICAL);
         container.setLayoutParams(
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
index 5946940..3357c2f 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
@@ -477,7 +477,6 @@
             assertTrue("must contain Adapter class", m.contains(MockAdapter.class.getName()));
             assertTrue("must contain LM class", m.contains(LinearLayoutManager.class.getName()));
             assertTrue("must contain ctx class", m.contains(getContext().getClass().getName()));
-
         }
     }
 
@@ -488,20 +487,71 @@
         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
         measure();
         layout();
-        assertSame(focusAdapter.mBottomLeft,
-                focusAdapter.mTopRight.focusSearch(View.FOCUS_FORWARD));
-        assertSame(focusAdapter.mBottomRight,
-                focusAdapter.mBottomLeft.focusSearch(View.FOCUS_FORWARD));
+
+        boolean isIcsOrLower = Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1;
+
+        // On API 15 and lower, focus forward get's translated to focus down.
+        View expected = isIcsOrLower ? focusAdapter.mBottomRight : focusAdapter.mBottomLeft;
+        assertEquals(expected, focusAdapter.mTopRight.focusSearch(View.FOCUS_FORWARD));
+
+        // On API 15 and lower, focus forward get's translated to focus down, which in this case
+        // runs out of the RecyclerView, thus returning null.
+        expected = isIcsOrLower ? null : focusAdapter.mBottomRight;
+        assertSame(expected, focusAdapter.mBottomLeft.focusSearch(View.FOCUS_FORWARD));
+
         // we don't want looping within RecyclerView
         assertNull(focusAdapter.mBottomRight.focusSearch(View.FOCUS_FORWARD));
         assertNull(focusAdapter.mTopLeft.focusSearch(View.FOCUS_BACKWARD));
     }
 
+    @Test
+    public void setAdapter_callsCorrectLmMethods() throws Throwable {
+        MockLayoutManager mockLayoutManager = new MockLayoutManager();
+        MockAdapter mockAdapter = new MockAdapter(1);
+        mRecyclerView.setLayoutManager(mockLayoutManager);
+
+        mRecyclerView.setAdapter(mockAdapter);
+        layout();
+
+        assertEquals(1, mockLayoutManager.mAdapterChangedCount);
+        assertEquals(0, mockLayoutManager.mItemsChangedCount);
+    }
+
+    @Test
+    public void swapAdapter_callsCorrectLmMethods() throws Throwable {
+        MockLayoutManager mockLayoutManager = new MockLayoutManager();
+        MockAdapter mockAdapter = new MockAdapter(1);
+        mRecyclerView.setLayoutManager(mockLayoutManager);
+
+        mRecyclerView.swapAdapter(mockAdapter, true);
+        layout();
+
+        assertEquals(1, mockLayoutManager.mAdapterChangedCount);
+        assertEquals(1, mockLayoutManager.mItemsChangedCount);
+    }
+
+    @Test
+    public void notifyDataSetChanged_callsCorrectLmMethods() throws Throwable {
+        MockLayoutManager mockLayoutManager = new MockLayoutManager();
+        MockAdapter mockAdapter = new MockAdapter(1);
+        mRecyclerView.setLayoutManager(mockLayoutManager);
+        mRecyclerView.setAdapter(mockAdapter);
+        mockLayoutManager.mAdapterChangedCount = 0;
+        mockLayoutManager.mItemsChangedCount = 0;
+
+        mockAdapter.notifyDataSetChanged();
+        layout();
+
+        assertEquals(0, mockLayoutManager.mAdapterChangedCount);
+        assertEquals(1, mockLayoutManager.mItemsChangedCount);
+    }
+
     static class MockLayoutManager extends RecyclerView.LayoutManager {
 
         int mLayoutCount = 0;
 
         int mAdapterChangedCount = 0;
+        int mItemsChangedCount = 0;
 
         RecyclerView.Adapter mPrevAdapter;
 
@@ -519,6 +569,11 @@
         }
 
         @Override
+        public void onItemsChanged(RecyclerView recyclerView) {
+            mItemsChangedCount++;
+        }
+
+        @Override
         public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
             mLayoutCount += 1;
         }
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
index a17dbdd..1c03c0f 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
@@ -253,6 +253,186 @@
     }
 
     @Test
+    public void setAdapter_afterSwapAdapter_callsCorrectLmMethods() throws Throwable {
+        final RecyclerView rv = new RecyclerView(getActivity());
+        final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true);
+        final TestAdapter testAdapter = new TestAdapter(1);
+
+        lm.expectLayouts(1);
+        rv.setLayoutManager(lm);
+        setRecyclerView(rv);
+        setAdapter(testAdapter);
+        lm.waitForLayout(2);
+
+        lm.onAdapterChagnedCallCount = 0;
+        lm.onItemsChangedCallCount = 0;
+
+        lm.expectLayouts(1);
+        mActivityRule.runOnUiThread(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        rv.swapAdapter(testAdapter, true);
+                        rv.setAdapter(testAdapter);
+                    }
+                });
+        lm.waitForLayout(2);
+
+        assertEquals(2, lm.onAdapterChagnedCallCount);
+        assertEquals(1, lm.onItemsChangedCallCount);
+    }
+
+    @Test
+    public void setAdapter_afterNotifyDataSetChanged_callsCorrectLmMethods() throws Throwable {
+        final RecyclerView rv = new RecyclerView(getActivity());
+        final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true);
+        final TestAdapter testAdapter = new TestAdapter(1);
+
+        lm.expectLayouts(1);
+        rv.setLayoutManager(lm);
+        setRecyclerView(rv);
+        setAdapter(testAdapter);
+        lm.waitForLayout(2);
+
+        lm.onAdapterChagnedCallCount = 0;
+        lm.onItemsChangedCallCount = 0;
+
+        lm.expectLayouts(1);
+        mActivityRule.runOnUiThread(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        testAdapter.notifyDataSetChanged();
+                        rv.setAdapter(testAdapter);
+                    }
+                });
+        lm.waitForLayout(2);
+
+        assertEquals(1, lm.onAdapterChagnedCallCount);
+        assertEquals(1, lm.onItemsChangedCallCount);
+    }
+
+    @Test
+    public void notifyDataSetChanged_afterSetAdapter_callsCorrectLmMethods() throws Throwable {
+        final RecyclerView rv = new RecyclerView(getActivity());
+        final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true);
+        final TestAdapter testAdapter = new TestAdapter(1);
+
+        lm.expectLayouts(1);
+        rv.setLayoutManager(lm);
+        setRecyclerView(rv);
+        setAdapter(testAdapter);
+        lm.waitForLayout(2);
+
+        lm.onAdapterChagnedCallCount = 0;
+        lm.onItemsChangedCallCount = 0;
+
+        lm.expectLayouts(1);
+        mActivityRule.runOnUiThread(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        rv.setAdapter(testAdapter);
+                        testAdapter.notifyDataSetChanged();
+                    }
+                });
+        lm.waitForLayout(2);
+
+        assertEquals(1, lm.onAdapterChagnedCallCount);
+        assertEquals(1, lm.onItemsChangedCallCount);
+    }
+
+    @Test
+    public void notifyDataSetChanged_afterSwapAdapter_callsCorrectLmMethods() throws Throwable {
+        final RecyclerView rv = new RecyclerView(getActivity());
+        final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true);
+        final TestAdapter testAdapter = new TestAdapter(1);
+
+        lm.expectLayouts(1);
+        rv.setLayoutManager(lm);
+        setRecyclerView(rv);
+        setAdapter(testAdapter);
+        lm.waitForLayout(2);
+
+        lm.onAdapterChagnedCallCount = 0;
+        lm.onItemsChangedCallCount = 0;
+
+        lm.expectLayouts(1);
+        mActivityRule.runOnUiThread(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        rv.swapAdapter(testAdapter, true);
+                        testAdapter.notifyDataSetChanged();
+                    }
+                });
+        lm.waitForLayout(2);
+
+        assertEquals(1, lm.onAdapterChagnedCallCount);
+        assertEquals(1, lm.onItemsChangedCallCount);
+    }
+
+    @Test
+    public void swapAdapter_afterSetAdapter_callsCorrectLmMethods() throws Throwable {
+        final RecyclerView rv = new RecyclerView(getActivity());
+        final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true);
+        final TestAdapter testAdapter = new TestAdapter(1);
+
+        lm.expectLayouts(1);
+        rv.setLayoutManager(lm);
+        setRecyclerView(rv);
+        setAdapter(testAdapter);
+        lm.waitForLayout(2);
+
+        lm.onAdapterChagnedCallCount = 0;
+        lm.onItemsChangedCallCount = 0;
+
+        lm.expectLayouts(1);
+        mActivityRule.runOnUiThread(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        rv.setAdapter(testAdapter);
+                        rv.swapAdapter(testAdapter, true);
+                    }
+                });
+        lm.waitForLayout(2);
+
+        assertEquals(2, lm.onAdapterChagnedCallCount);
+        assertEquals(1, lm.onItemsChangedCallCount);
+    }
+
+    @Test
+    public void swapAdapter_afterNotifyDataSetChanged_callsCorrectLmMethods() throws Throwable {
+        final RecyclerView rv = new RecyclerView(getActivity());
+        final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true);
+        final TestAdapter testAdapter = new TestAdapter(1);
+
+        lm.expectLayouts(1);
+        rv.setLayoutManager(lm);
+        setRecyclerView(rv);
+        setAdapter(testAdapter);
+        lm.waitForLayout(2);
+
+        lm.onAdapterChagnedCallCount = 0;
+        lm.onItemsChangedCallCount = 0;
+
+        lm.expectLayouts(1);
+        mActivityRule.runOnUiThread(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        testAdapter.notifyDataSetChanged();
+                        rv.swapAdapter(testAdapter, true);
+                    }
+                });
+        lm.waitForLayout(2);
+
+        assertEquals(1, lm.onAdapterChagnedCallCount);
+        assertEquals(1, lm.onItemsChangedCallCount);
+    }
+
+    @Test
     public void setAdapterNotifyItemRangeInsertedCrashTest() throws Throwable {
         final RecyclerView rv = new RecyclerView(getActivity());
         final TestLayoutManager lm = new LayoutAllLayoutManager(true);
@@ -4788,6 +4968,8 @@
 
     public class LayoutAllLayoutManager extends TestLayoutManager {
         private final boolean mAllowNullLayoutLatch;
+        public int onItemsChangedCallCount = 0;
+        public int onAdapterChagnedCallCount = 0;
 
         public LayoutAllLayoutManager() {
             // by default, we don't allow unexpected layouts.
@@ -4797,6 +4979,18 @@
             mAllowNullLayoutLatch = allowNullLayoutLatch;
         }
 
+        @Override
+        public void onItemsChanged(RecyclerView recyclerView) {
+            super.onItemsChanged(recyclerView);
+            onItemsChangedCallCount++;
+        }
+
+        @Override
+        public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
+                RecyclerView.Adapter newAdapter) {
+            super.onAdapterChanged(oldAdapter, newAdapter);
+            onAdapterChagnedCallCount++;
+        }
 
         @Override
         public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
@@ -4850,7 +5044,7 @@
 
         @Override
         public void onNestedPreScroll(@NonNull View target, int dx, int dy,
-                @Nullable int[] consumed, @ViewCompat.NestedScrollType int type) {
+                @NonNull int[] consumed, @ViewCompat.NestedScrollType int type) {
             // Consume everything!
             consumed[0] = dx;
             consumed[1] = dy;
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/TestedFrameLayout.java b/v7/recyclerview/tests/src/android/support/v7/widget/TestedFrameLayout.java
index cd5fa22..2d73fd0 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/TestedFrameLayout.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/TestedFrameLayout.java
@@ -18,7 +18,6 @@
 
 import android.content.Context;
 import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
 import android.support.v4.view.NestedScrollingParent2;
 import android.support.v4.view.ViewCompat;
 import android.util.AttributeSet;
@@ -201,7 +200,7 @@
     }
 
     @Override
-    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed,
+    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
             @ViewCompat.NestedScrollType int type) {
         if (mNestedScrollingDelegate != null) {
             mNestedScrollingDelegate.onNestedPreScroll(target, dx, dy, consumed, type);
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/test/NestedScrollingParent2Adapter.java b/v7/recyclerview/tests/src/android/support/v7/widget/test/NestedScrollingParent2Adapter.java
index da0d86f..356cffa 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/test/NestedScrollingParent2Adapter.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/test/NestedScrollingParent2Adapter.java
@@ -17,7 +17,6 @@
 package android.support.v7.widget.test;
 
 import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
 import android.support.v4.view.NestedScrollingParent2;
 import android.support.v4.view.ViewCompat;
 import android.view.View;
@@ -46,7 +45,7 @@
 
     @Override
     public void onNestedPreScroll(@NonNull View target, int dx, int dy,
-            @Nullable int[] consumed, @ViewCompat.NestedScrollType int type) {
+            @NonNull int[] consumed, @ViewCompat.NestedScrollType int type) {
     }
 
     @Override
diff --git a/wear/api/current.txt b/wear/api/current.txt
index e9b7d86..3f148d3 100644
--- a/wear/api/current.txt
+++ b/wear/api/current.txt
@@ -2,7 +2,7 @@
 
   public final class AmbientMode extends android.app.Fragment {
     ctor public AmbientMode();
-    method public static <T extends android.app.Activity & android.support.wear.ambient.AmbientMode.AmbientCallbackProvider> android.support.wear.ambient.AmbientMode.AmbientController attachAmbientSupport(T);
+    method public static <T extends android.app.Activity> android.support.wear.ambient.AmbientMode.AmbientController attachAmbientSupport(T);
     field public static final java.lang.String EXTRA_BURN_IN_PROTECTION = "com.google.android.wearable.compat.extra.BURN_IN_PROTECTION";
     field public static final java.lang.String EXTRA_LOWBIT_AMBIENT = "com.google.android.wearable.compat.extra.LOWBIT_AMBIENT";
     field public static final java.lang.String FRAGMENT_TAG = "android.support.wearable.ambient.AmbientMode";
diff --git a/wear/src/main/java/android/support/wear/ambient/AmbientMode.java b/wear/src/main/java/android/support/wear/ambient/AmbientMode.java
index 5db9383..0077a5b 100644
--- a/wear/src/main/java/android/support/wear/ambient/AmbientMode.java
+++ b/wear/src/main/java/android/support/wear/ambient/AmbientMode.java
@@ -21,7 +21,9 @@
 import android.content.Context;
 import android.os.Bundle;
 import android.support.annotation.CallSuper;
+import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
+import android.util.Log;
 
 import com.google.android.wearable.compat.WearableActivityController;
 
@@ -48,6 +50,7 @@
  * }</pre>
  */
 public final class AmbientMode extends Fragment {
+    private static final String TAG = "AmbientMode";
 
     /**
      * Property in bundle passed to {@code AmbientCallback#onEnterAmbient(Bundle)} to indicate
@@ -104,9 +107,6 @@
          * running (after onResume, before onPause). All drawing should complete by the conclusion
          * of this method. Note that {@code invalidate()} calls will be executed before resuming
          * lower-power mode.
-         * <p>
-         * <p><em>Derived classes must call through to the super class's implementation of this
-         * method. If they do not, an exception will be thrown.</em>
          *
          * @param ambientDetails bundle containing information about the display being used.
          *                      It includes information about low-bit color and burn-in protection.
@@ -122,9 +122,6 @@
         /**
          * Called when an activity should exit ambient mode. This event is sent while an activity is
          * running (after onResume, before onPause).
-         * <p>
-         * <p><em>Derived classes must call through to the super class's implementation of this
-         * method. If they do not, an exception will be thrown.</em>
          */
         public void onExitAmbient() {}
     }
@@ -133,20 +130,27 @@
             new AmbientDelegate.AmbientCallback() {
                 @Override
                 public void onEnterAmbient(Bundle ambientDetails) {
-                    mSuppliedCallback.onEnterAmbient(ambientDetails);
+                    if (mSuppliedCallback != null) {
+                        mSuppliedCallback.onEnterAmbient(ambientDetails);
+                    }
                 }
 
                 @Override
                 public void onExitAmbient() {
-                    mSuppliedCallback.onExitAmbient();
+                    if (mSuppliedCallback != null) {
+                        mSuppliedCallback.onExitAmbient();
+                    }
                 }
 
                 @Override
                 public void onUpdateAmbient() {
-                    mSuppliedCallback.onUpdateAmbient();
+                    if (mSuppliedCallback != null) {
+                        mSuppliedCallback.onUpdateAmbient();
+                    }
                 }
             };
     private AmbientDelegate mDelegate;
+    @Nullable
     private AmbientCallback mSuppliedCallback;
     private AmbientController mController;
 
@@ -166,8 +170,7 @@
         if (context instanceof AmbientCallbackProvider) {
             mSuppliedCallback = ((AmbientCallbackProvider) context).getAmbientCallback();
         } else {
-            throw new IllegalArgumentException(
-                    "fragment should attach to an activity that implements AmbientCallback");
+            Log.w(TAG, "No callback provided - enabling only smart resume");
         }
     }
 
@@ -176,7 +179,9 @@
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         mDelegate.onCreate();
-        mDelegate.setAmbientEnabled();
+        if (mSuppliedCallback != null) {
+            mDelegate.setAmbientEnabled();
+        }
     }
 
     @Override
@@ -215,15 +220,19 @@
     }
 
     /**
-     * Attach ambient support to the given activity.
+     * Attach ambient support to the given activity. Calling this method with an Activity
+     * implementing the {@link AmbientCallbackProvider} interface will provide you with an
+     * opportunity to react to ambient events such as {@code onEnterAmbient}. Alternatively,
+     * you can call this method with an Activity which does not implement
+     * the {@link AmbientCallbackProvider} interface and that will only enable the auto-resume
+     * functionality. This is equivalent to providing (@code null} from
+     * the {@link AmbientCallbackProvider}.
      *
-     * @param activity the activity to attach ambient support to. This activity has to also
-     *                implement {@link AmbientCallbackProvider}
+     * @param activity the activity to attach ambient support to.
      * @return the associated {@link AmbientController} which can be used to query the state of
      * ambient mode.
      */
-    public static <T extends Activity & AmbientCallbackProvider> AmbientController
-            attachAmbientSupport(T activity) {
+    public static <T extends Activity> AmbientController attachAmbientSupport(T activity) {
         FragmentManager fragmentManager = activity.getFragmentManager();
         AmbientMode ambientFragment = (AmbientMode) fragmentManager.findFragmentByTag(FRAGMENT_TAG);
         if (ambientFragment == null) {
diff --git a/wear/tests/AndroidManifest.xml b/wear/tests/AndroidManifest.xml
index ce78477..67ccd91 100644
--- a/wear/tests/AndroidManifest.xml
+++ b/wear/tests/AndroidManifest.xml
@@ -55,6 +55,13 @@
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
+
+        <activity android:name="android.support.wear.ambient.AmbientModeResumeTestActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
         <!-- Test app is iOS compatible. -->
         <meta-data
             android:name="com.google.android.wearable.standalone"
diff --git a/wear/tests/src/android/support/wear/ambient/AmbientModeResumeTest.java b/wear/tests/src/android/support/wear/ambient/AmbientModeResumeTest.java
new file mode 100644
index 0000000..32de769
--- /dev/null
+++ b/wear/tests/src/android/support/wear/ambient/AmbientModeResumeTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.support.wear.ambient;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.wear.widget.util.WakeLockRule;
+
+import com.google.android.wearable.compat.WearableActivityController;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class AmbientModeResumeTest {
+    @Rule
+    public final WakeLockRule mWakeLock = new WakeLockRule();
+
+    @Rule
+    public final ActivityTestRule<AmbientModeResumeTestActivity> mActivityRule =
+            new ActivityTestRule<>(AmbientModeResumeTestActivity.class);
+
+    @Test
+    public void testActivityDefaults() throws Throwable {
+        assertTrue(WearableActivityController.getLastInstance().isAutoResumeEnabled());
+        assertFalse(WearableActivityController.getLastInstance().isAmbientEnabled());
+    }
+}
diff --git a/wear/tests/src/android/support/wear/ambient/AmbientModeResumeTestActivity.java b/wear/tests/src/android/support/wear/ambient/AmbientModeResumeTestActivity.java
new file mode 100644
index 0000000..0ca3c15
--- /dev/null
+++ b/wear/tests/src/android/support/wear/ambient/AmbientModeResumeTestActivity.java
@@ -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 android.support.wear.ambient;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class AmbientModeResumeTestActivity extends Activity {
+    AmbientMode.AmbientController mAmbientController;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mAmbientController = AmbientMode.attachAmbientSupport(this);
+    }
+}
diff --git a/wear/tests/src/com/google/android/wearable/compat/WearableActivityController.java b/wear/tests/src/com/google/android/wearable/compat/WearableActivityController.java
index 7823f23..0a76af0 100644
--- a/wear/tests/src/com/google/android/wearable/compat/WearableActivityController.java
+++ b/wear/tests/src/com/google/android/wearable/compat/WearableActivityController.java
@@ -33,7 +33,7 @@
 
     private AmbientCallback mCallback;
     private boolean mAmbientEnabled = false;
-    private boolean mAutoResumeEnabled = false;
+    private boolean mAutoResumeEnabled = true;
     private boolean mAmbient = false;
 
     public WearableActivityController(String tag, Activity activity, AmbientCallback callback) {