Merge "Allow FragmentTransactions in onConfigurationChanged" into oc-mr1-jetpack-dev
diff --git a/.gitignore b/.gitignore
index 162af55..7e44717 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,4 +16,8 @@
**/out
buildSrc/build
lifecycle/common/build
-jacoco.exec
\ No newline at end of file
+jacoco.exec
+
+# When Gradle configuration fails with the --profile option enabled it creates this folder.
+/reports
+/app-toolkit/reports
diff --git a/app-toolkit/core-testing/api/current.txt b/app-toolkit/core-testing/api/current.txt
new file mode 100644
index 0000000..f1d206c
--- /dev/null
+++ b/app-toolkit/core-testing/api/current.txt
@@ -0,0 +1,15 @@
+package android.arch.core.executor.testing {
+
+ public class CountingTaskExecutorRule extends org.junit.rules.TestWatcher {
+ ctor public CountingTaskExecutorRule();
+ method public void drainTasks(int, java.util.concurrent.TimeUnit) throws java.lang.InterruptedException, java.util.concurrent.TimeoutException;
+ method public boolean isIdle();
+ method protected void onIdle();
+ }
+
+ public class InstantTaskExecutorRule extends org.junit.rules.TestWatcher {
+ ctor public InstantTaskExecutorRule();
+ }
+
+}
+
diff --git a/app-toolkit/init.gradle b/app-toolkit/init.gradle
index 393b2f9..654f65c 100644
--- a/app-toolkit/init.gradle
+++ b/app-toolkit/init.gradle
@@ -15,7 +15,6 @@
*/
import android.support.DacOptions
-import org.gradle.internal.os.OperatingSystem
apply from: "${ext.supportRootFolder}/buildSrc/init.gradle"
init.setSdkInLocalPropertiesFile()
diff --git a/app-toolkit/runtime/api/current.txt b/app-toolkit/runtime/api/current.txt
new file mode 100644
index 0000000..af5b253
--- /dev/null
+++ b/app-toolkit/runtime/api/current.txt
@@ -0,0 +1,8 @@
+package android.arch.core.util {
+
+ public abstract interface Function<I, O> {
+ method public abstract O apply(I);
+ }
+
+}
+
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index 718110f..4d663af 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -11,6 +11,14 @@
dependencies {
classpath build_libs.kotlin.gradle_plugin
}
+
+ configurations.classpath.resolutionStrategy {
+ eachDependency { details ->
+ if (details.requested.group == 'org.jetbrains.kotlin') {
+ details.useVersion build_versions.kotlin
+ }
+ }
+ }
}
def runningInBuildServer = System.env.DIST_DIR != null && System.env.OUT_DIR != null
if (runningInBuildServer) {
@@ -24,10 +32,11 @@
repos.addMavenRepositories(repositories)
-apply plugin: 'groovy'
apply plugin: 'java'
apply plugin: 'kotlin'
+apply from: "kotlin-dsl-dependency.gradle.kts"
+
compileGroovy {
dependsOn tasks.getByPath('compileKotlin')
classpath += files(compileKotlin.destinationDir)
@@ -38,4 +47,5 @@
compile build_libs.error_prone
compile build_libs.jarjar_gradle
compile gradleApi()
+ testCompile "junit:junit:4.12"
}
diff --git a/buildSrc/build_dependencies.gradle b/buildSrc/build_dependencies.gradle
index 313c4e3..f693884 100644
--- a/buildSrc/build_dependencies.gradle
+++ b/buildSrc/build_dependencies.gradle
@@ -14,6 +14,13 @@
* limitations under the License.
*/
+def build_versions = [:]
+
+build_versions.kotlin = '1.2.0'
+
+rootProject.ext['build_versions'] = build_versions
+
+
def build_libs = [:]
def androidPluginVersionOverride = System.getenv("GRADLE_PLUGIN_VERSION")
@@ -31,7 +38,9 @@
build_libs.jacoco = 'org.jacoco:org.jacoco.core:0.7.8'
build_libs.jacoco_ant = 'org.jacoco:org.jacoco.ant:0.7.8'
build_libs.jetifier = 'androidx.tools.jetifier:gradle-plugin:0.1'
-build_libs.kotlin = [gradle_plugin: "org.jetbrains.kotlin:kotlin-gradle-plugin:1.2.0"]
+build_libs.kotlin = [
+ gradle_plugin: "org.jetbrains.kotlin:kotlin-gradle-plugin:${build_versions.kotlin}"
+]
// jdiff dependencies
build_libs.jdiff = 'com.android:jdiff:1.1.0'
build_libs.xml_parser_apis = 'xerces:xmlParserAPIs:2.6.2'
diff --git a/buildSrc/init.gradle b/buildSrc/init.gradle
index 08faa27..30841b6 100644
--- a/buildSrc/init.gradle
+++ b/buildSrc/init.gradle
@@ -30,7 +30,7 @@
}
def init = new Properties()
ext.init = init
-rootProject.ext.versionChecker = new GMavenVersionChecker(rootProject)
+rootProject.ext.versionChecker = new GMavenVersionChecker(rootProject.logger)
ext.runningInBuildServer = System.env.DIST_DIR != null && System.env.OUT_DIR != null
apply from: "${supportRoot}/buildSrc/dependencies.gradle"
@@ -63,27 +63,15 @@
}
def setSdkInLocalPropertiesFile() {
- ext.buildToolsVersion = '27.0.1'
- final String fullSdkPath = getFullSdkPath();
- if (file(fullSdkPath).exists()) {
- project.ext.currentSdk = 26
- project.ext.androidJar =
- files("${fullSdkPath}/platforms/android-${project.currentSdk}/android.jar")
- project.ext.androidSrcJar =
- file("${fullSdkPath}/platforms/android-${project.currentSdk}/android-stubs-src.jar")
- project.ext.androidApiTxt = null
+ final File fullSdkPath = file(getFullSdkPath())
+ if (fullSdkPath.exists()) {
+ project.ext.fullSdkPath = fullSdkPath
File props = file("local.properties")
- props.write "sdk.dir=${fullSdkPath}"
+ props.write "sdk.dir=${fullSdkPath.getAbsolutePath()}"
ext.usingFullSdk = true
} else {
- gradle.ext.currentSdk = 'current'
- project.ext.androidJar = files("${repos.prebuiltsRoot}/sdk/current/android.jar")
- project.ext.androidSrcJar = null
- project.ext.androidApiTxt = file("${repos.prebuiltsRoot}/sdk/api/26.txt")
- System.setProperty('android.dir', "${supportRootFolder}/../../")
- File props = file("local.properties")
- props.write "android.dir=../../"
- ext.usingFullSdk = false
+ throw Exception("You are using non ub-supportlib-* checkout. You need to check out "
+ + "ub-supportlib-* to work on support library. See go/supportlib for details.")
}
}
@@ -164,8 +152,6 @@
}
def configureSubProjects() {
- // lint every library
- def lintTask = project.tasks.create("lint")
subprojects {
repos.addMavenRepositories(repositories)
@@ -180,13 +166,10 @@
return
}
- project.ext.currentSdk = rootProject.ext.currentSdk
-
project.plugins.whenPluginAdded { plugin ->
def isAndroidLibrary = "com.android.build.gradle.LibraryPlugin"
.equals(plugin.class.name)
def isAndroidApp = "com.android.build.gradle.AppPlugin".equals(plugin.class.name)
- def isJavaLibrary = "org.gradle.api.plugins.JavaPlugin".equals(plugin.class.name)
if (isAndroidLibrary || isAndroidApp) {
// Enable code coverage for debug builds only if we are not running inside the IDE,
@@ -216,29 +199,6 @@
v.assemble.dependsOn jarifyTask
}
}
-
- // Enforce NewApi lint check as fatal.
- project.android.lintOptions.fatal 'NewApi'
- lintTask.dependsOn {project.lint}
- }
-
- if (isAndroidLibrary || isJavaLibrary) {
- // Add library to the aggregate dependency report.
- task allDeps(type: DependencyReportTask) {}
-
- project.afterEvaluate {
- Upload uploadTask = (Upload) project.tasks.uploadArchives;
- uploadTask.repositories.mavenDeployer {
- repository(url: uri("$rootProject.ext.supportRepoOut"))
- setUniqueVersion(true)
- }
-
- // Before the upload, make sure the repo is ready.
- uploadTask.dependsOn rootProject.tasks.prepareRepo
-
- // Make the mainupload depend on this one.
- mainUpload.dependsOn uploadTask
- }
}
}
diff --git a/v13/tests/java/android/support/v13/app/FragmentCompatTestActivity.java b/buildSrc/kotlin-dsl-dependency.gradle.kts
similarity index 75%
rename from v13/tests/java/android/support/v13/app/FragmentCompatTestActivity.java
rename to buildSrc/kotlin-dsl-dependency.gradle.kts
index b3ab763..64085ab 100644
--- a/v13/tests/java/android/support/v13/app/FragmentCompatTestActivity.java
+++ b/buildSrc/kotlin-dsl-dependency.gradle.kts
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,9 +14,6 @@
* limitations under the License.
*/
-package android.support.v13.app;
-
-import android.app.Activity;
-
-public class FragmentCompatTestActivity extends Activity {
-}
+dependencies {
+ "compileOnly"(gradleKotlinDsl())
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/android/support/gmaven/GMavenVersionChecker.groovy b/buildSrc/src/main/groovy/android/support/gmaven/GMavenVersionChecker.groovy
deleted file mode 100644
index 0a71fbc..0000000
--- a/buildSrc/src/main/groovy/android/support/gmaven/GMavenVersionChecker.groovy
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR 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.gmaven
-
-import android.support.Version
-import com.android.annotations.Nullable
-import groovy.util.slurpersupport.NodeChild
-import org.gradle.api.GradleException
-import org.gradle.api.Project
-import org.gradle.api.logging.Logger
-
-/**
- * Queries maven.google.com to get the version numbers for each artifact.
- * Due to the structure of maven.google.com, a new query is necessary for each group.
- */
-@SuppressWarnings("GroovyUnusedDeclaration")
-class GMavenVersionChecker {
- // wait 2 seconds before retrying if fetch fails
- private static final int RETRY_DELAY = 2000 // ms
- // number of times we'll try to reach maven.google.com before failing
- private static final int DEFAULT_RETRY_LIMIT = 20
- private static final String BASE = "https://dl.google.com/dl/android/maven2/"
- private static final String GROUP_FILE = "group-index.xml"
-
- // cache versions by group to avoid re-querying for each artifact
- private final Map<String, GroupVersionData> versionCache = new HashMap<>()
- // the logger from the project
- private final Logger logger
-
- /**
- * Creates a new instance using the given project's logger
- *
- * @param project This should be the root project. No reason to create multiple instances of
- * this
- */
- GMavenVersionChecker(Project project) {
- this.logger = project.logger
- }
-
- /**
- * Creates the URL which has the XML file that describes the available versions for each
- * artifact in that group
- *
- * @param group Maven group name
- * @return The URL of the XML file
- */
- private static String buildGroupUrl(String group) {
- return BASE + group.replace(".", "/") + "/" + GROUP_FILE
- }
-
- /**
- * Returns the version data for each artifact in a given group.
- * <p>
- * If data is not cached, this will make a web request to get it.
- *
- * @param group The group to query
- * @return A data class which has the versions for each artifact
- */
- private GroupVersionData getVersionData(String group) {
- def versionData = versionCache.get(group)
- if (versionData == null) {
- versionData = fetchGroup(group)
- if (versionData != null) {
- versionCache.put(versionData.name, versionData)
- }
- }
- return versionData
- }
-
- /**
- * Fetches the group version information from maven.google.com
- *
- * @param group The group name to fetch
- * @param retryCount Number of times we'll retry before failing
- * @return GroupVersionData that has the data or null if it is a new item.
- */
- @Nullable
- private GroupVersionData fetchGroup(String group, int retryCount) {
- def url = buildGroupUrl(group)
- for (int run = 0; run < retryCount; run++) {
- logger.info "fetching maven XML from $url"
- try {
- def parsedXml = new XmlSlurper(false, false).parse(url)
- return new GroupVersionData(parsedXml)
- } catch (FileNotFoundException ignored) {
- logger.info "could not find version data for $group, seems like a new file"
- return null
- } catch (IOException ioException) {
- logger.warning "failed to fetch the maven info, retrying in 2 seconds. " +
- "Run $run of $retryCount"
- Thread.sleep(RETRY_DELAY)
- }
- }
- throw new GradleException("Could not access maven.google.com")
- }
-
- /**
- * Fetches the group version information from maven.google.com
- *
- * @param group The group name to fetch
- * @return GroupVersionData that has the data or null if it is a new item.
- */
- @Nullable
- private GroupVersionData fetchGroup(String group) {
- return fetchGroup(group, DEFAULT_RETRY_LIMIT)
- }
-
- /**
- * Return the available versions on maven.google.com for a given artifact
- *
- * @param group The group id of the artifact
- * @param artifactName The name of the artifact
- * @return The set of versions that are available on maven.google.com. Null if artifact is not
- * available.
- */
- @Nullable
- Set<Version> getVersions(String group, String artifactName) {
- def groupData = getVersionData(group)
- return groupData?.artifacts?.get(artifactName)?.versions
- }
-
- /**
- * Checks whether the given artifact is already on maven.google.com.
- *
- * @param group The project group on maven
- * @param artifactName The artifact name on maven
- * @param version The version on maven
- * @return true if the artifact is already on maven.google.com
- */
- boolean isReleased(String group, String artifactName, String version) {
- return getVersions(group, artifactName)?.contains(new Version(version))
- }
-
- /**
- * Data class that holds the artifacts of a single maven group
- */
- private static class GroupVersionData {
- /**
- * The group name
- */
- String name
- /**
- * Map of artifact versions keyed by artifact name
- */
- Map<String, ArtifactVersionData> artifacts = new HashMap<>()
-
- /**
- * Constructs an instance from the given node.
- *
- * @param xml The information node fetched from {@code GROUP_FILE}
- */
- GroupVersionData(NodeChild xml) {
- /**
- * sample input:
- * <android.arch.core>
- * <runtime versions="1.0.0-alpha4,1.0.0-alpha5,1.0.0-alpha6,1.0.0-alpha7"/>
- * <common versions="1.0.0-alpha4,1.0.0-alpha5,1.0.0-alpha6,1.0.0-alpha7"/>
- * </android.arch.core>
- */
- this.name = xml.name()
- xml.childNodes().each {
- def versions = it.attributes['versions'].split(",").collect { version ->
- new Version(version)
- }.toSet()
- artifacts[it.name()] = new ArtifactVersionData(it.name(), versions)
- }
- }
- }
-
- /**
- * Data class that holds the version information about a single artifact
- */
- private static class ArtifactVersionData {
- /**
- * name of the artifact
- */
- final String name
- /**
- * set of version codes that are already on maven.google.com
- */
- final Set<Version> versions
-
- ArtifactVersionData(String name, Set<Version> versions) {
- this.name = name
- this.versions = versions
- }
- }
-}
\ No newline at end of file
diff --git a/buildSrc/src/main/java/android/support/LibraryVersions.java b/buildSrc/src/main/java/android/support/LibraryVersions.java
index baf5007..7b7a37e 100644
--- a/buildSrc/src/main/java/android/support/LibraryVersions.java
+++ b/buildSrc/src/main/java/android/support/LibraryVersions.java
@@ -28,7 +28,7 @@
/**
* Version code for flatfoot 1.0 projects (room, lifecycles)
*/
- private static final Version FLATFOOT_1_0_BATCH = new Version("1.0.0");
+ private static final Version FLATFOOT_1_0_BATCH = new Version("1.1.0-SNAPSHOT");
/**
* Version code for Room
diff --git a/buildSrc/src/main/java/android/support/Version.java b/buildSrc/src/main/java/android/support/Version.java
deleted file mode 100644
index 36c7728..0000000
--- a/buildSrc/src/main/java/android/support/Version.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support;
-
-import java.io.File;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Utility class which represents a version
- */
-public class Version implements Comparable<Version> {
- private static final Pattern VERSION_FILE_REGEX = Pattern.compile("^(\\d+\\.\\d+\\.\\d+).txt$");
- private static final Pattern VERSION_REGEX = Pattern
- .compile("^(\\d+)\\.(\\d+)\\.(\\d+)(-.+)?$");
-
- private final int mMajor;
- private final int mMinor;
- private final int mPatch;
- private final String mExtra;
-
- public Version(String versionString) {
- this(checkedMatcher(versionString));
- }
-
- private static Matcher checkedMatcher(String versionString) {
- Matcher matcher = VERSION_REGEX.matcher(versionString);
- if (!matcher.matches()) {
- throw new IllegalArgumentException("Can not parse version: " + versionString);
- }
- return matcher;
- }
-
- private Version(Matcher matcher) {
- mMajor = Integer.parseInt(matcher.group(1));
- mMinor = Integer.parseInt(matcher.group(2));
- mPatch = Integer.parseInt(matcher.group(3));
- mExtra = matcher.groupCount() == 4 ? matcher.group(4) : null;
- }
-
- @Override
- public int compareTo(Version version) {
- if (mMajor != version.mMajor) {
- return mMajor - version.mMajor;
- }
- if (mMinor != version.mMinor) {
- return mMinor - version.mMinor;
- }
- if (mPatch != version.mPatch) {
- return mPatch - version.mPatch;
- }
- if (mExtra == null) {
- if (version.mExtra == null) {
- return 0;
- }
- // not having any extra is always a later version
- return 1;
- } else {
- if (version.mExtra == null) {
- // not having any extra is always a later version
- return -1;
- }
- // gradle uses lexicographic ordering
- return mExtra.compareTo(version.mExtra);
- }
- }
-
- public boolean isPatch() {
- return mPatch != 0;
- }
-
- public boolean isSnapshot() {
- return "-SNAPSHOT".equals(mExtra);
- }
-
- public int getMajor() {
- return mMajor;
- }
-
- public int getMinor() {
- return mMinor;
- }
-
- public int getPatch() {
- return mPatch;
- }
-
- public String getExtra() {
- return mExtra;
- }
-
- @Override
- public String toString() {
- return mMajor + "." + mMinor + "." + mPatch + (mExtra != null ? mExtra : "");
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- Version version = (Version) o;
-
- if (mMajor != version.mMajor) return false;
- if (mMinor != version.mMinor) return false;
- if (mPatch != version.mPatch) return false;
- return mExtra != null ? mExtra.equals(version.mExtra) : version.mExtra == null;
- }
-
- @Override
- public int hashCode() {
- int result = mMajor;
- result = 31 * result + mMinor;
- result = 31 * result + mPatch;
- result = 31 * result + (mExtra != null ? mExtra.hashCode() : 0);
- return result;
- }
-
- /**
- * @return Version or null, if a name of the given file doesn't match
- */
- public static Version from(File file) {
- if (!file.isFile()) {
- return null;
- }
- Matcher matcher = VERSION_FILE_REGEX.matcher(file.getName());
- if (!matcher.matches()) {
- return null;
- }
- return new Version(matcher.group(1));
- }
-
- /**
- * @return Version or null, if the given string doesn't match
- */
- public static Version from(String versionString) {
- Matcher matcher = VERSION_REGEX.matcher(versionString);
- if (!matcher.matches()) {
- return null;
- }
- return new Version(matcher);
- }
-}
diff --git a/buildSrc/src/main/java/android/support/VersionFileWriterTask.java b/buildSrc/src/main/java/android/support/VersionFileWriterTask.java
deleted file mode 100644
index aafa023..0000000
--- a/buildSrc/src/main/java/android/support/VersionFileWriterTask.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support;
-
-import com.android.build.gradle.LibraryExtension;
-
-import org.gradle.api.Action;
-import org.gradle.api.DefaultTask;
-import org.gradle.api.Project;
-import org.gradle.api.tasks.Input;
-import org.gradle.api.tasks.OutputFile;
-import org.gradle.api.tasks.TaskAction;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.PrintWriter;
-
-/**
- * Task that allows to write a version to a given output file.
- */
-public class VersionFileWriterTask extends DefaultTask {
- public static final String RESOURCE_DIRECTORY = "generatedResources";
- public static final String VERSION_FILE_PATH =
- RESOURCE_DIRECTORY + "/META-INF/%s_%s.version";
-
- private String mVersion;
- private File mOutputFile;
-
- /**
- * Sets up Android Library project to have a task that generates a version file.
- *
- * @param project an Android Library project.
- */
- public static void setUpAndroidLibrary(Project project) {
- project.afterEvaluate(new Action<Project>() {
- @Override
- public void execute(Project project) {
- LibraryExtension library =
- project.getExtensions().findByType(LibraryExtension.class);
-
- String group = (String) project.getProperties().get("group");
- String artifactId = (String) project.getProperties().get("name");
- String version = (String) project.getProperties().get("version");
-
- // Add a java resource file to the library jar for version tracking purposes.
- File artifactName = new File(project.getBuildDir(),
- String.format(VersionFileWriterTask.VERSION_FILE_PATH,
- group, artifactId));
-
- VersionFileWriterTask writeVersionFile =
- project.getTasks().create("writeVersionFile", VersionFileWriterTask.class);
- writeVersionFile.setVersion(version);
- writeVersionFile.setOutputFile(artifactName);
-
- library.getLibraryVariants().all(
- libraryVariant -> libraryVariant.getProcessJavaResources().dependsOn(
- writeVersionFile));
-
- library.getSourceSets().getByName("main").getResources().srcDir(
- new File(project.getBuildDir(), VersionFileWriterTask.RESOURCE_DIRECTORY)
- );
- }
- });
- }
-
- @Input
- public String getVersion() {
- return mVersion;
- }
-
- public void setVersion(String version) {
- mVersion = version;
- }
-
- @OutputFile
- public File getOutputFile() {
- return mOutputFile;
- }
-
- public void setOutputFile(File outputFile) {
- mOutputFile = outputFile;
- }
-
- /**
- * The main method for actually writing out the file.
- *
- * @throws IOException
- */
- @TaskAction
- public void run() throws IOException {
- PrintWriter writer = new PrintWriter(mOutputFile);
- writer.println(mVersion);
- writer.close();
- }
-}
diff --git a/buildSrc/src/main/kotlin/android/support/DiffAndDocs.kt b/buildSrc/src/main/kotlin/android/support/DiffAndDocs.kt
index fefc3ed..b96260c 100644
--- a/buildSrc/src/main/kotlin/android/support/DiffAndDocs.kt
+++ b/buildSrc/src/main/kotlin/android/support/DiffAndDocs.kt
@@ -30,7 +30,6 @@
import org.gradle.api.artifacts.Configuration
import org.gradle.api.file.FileCollection
import org.gradle.api.plugins.JavaBasePlugin
-import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.bundling.Zip
import org.gradle.api.tasks.compile.JavaCompile
@@ -106,7 +105,7 @@
var lastFile: File? = null
var lastVersion: Version? = null
apiDir.listFiles().forEach { file ->
- Version.from(file)?.let { version ->
+ Version.parseOrNull(file)?.let { version ->
if ((lastFile == null || lastVersion!! < version) && version < refVersion) {
lastFile = file
lastVersion = version
@@ -129,7 +128,7 @@
private fun getApiFile(rootDir: File, refVersion: Version, forceRelease: Boolean = false): File {
val apiDir = File(rootDir, "api")
- if (!refVersion.isSnapshot || forceRelease) {
+ if (!refVersion.isSnapshot() || forceRelease) {
// Release API file is always X.Y.0.txt.
return File(apiDir, "${refVersion.major}.${refVersion.minor}.0.txt")
}
@@ -147,13 +146,13 @@
val rootFolder = project.projectDir
val version = Version(project.version as String)
- if (version.isPatch) {
+ if (version.isPatch()) {
throw GradleException("Public APIs may not be modified in patch releases.")
- } else if (version.isSnapshot && getApiFile(rootFolder,
+ } else if (version.isSnapshot() && getApiFile(rootFolder,
version,
true).exists()) {
throw GradleException("Inconsistent version. Public API file already exists.")
- } else if (!version.isSnapshot && getApiFile(rootFolder, version).exists()
+ } else if (!version.isSnapshot() && getApiFile(rootFolder, version).exists()
&& !project.hasProperty("force")) {
throw GradleException("Public APIs may not be modified in finalized releases.")
}
@@ -166,7 +165,7 @@
setDocletpath(docletpathParam)
destinationDir = project.docsDir()
// Base classpath is Android SDK, sub-projects add their own.
- classpath = project.androidJar()
+ classpath = androidJarFile(project)
apiFile = File(project.docsDir(), "release/${project.name}/current.txt")
generateDocs = false
@@ -247,7 +246,7 @@
*/
private fun createOldApiXml(project: Project, doclavaConfig: Configuration) =
project.tasks.createWithConfig("oldApiXml", ApiXmlConversionTask::class.java) {
- val toApi = project.processProperty("toApi")?.let(Version::from)
+ val toApi = project.processProperty("toApi")?.let { Version.parseOrNull(it) }
val fromApi = project.processProperty("fromApi")
classpath = project.files(doclavaConfig.resolve())
val rootFolder = project.projectDir
@@ -329,7 +328,7 @@
jdiffConfig: Configuration) =
project.tasks.createWithConfig("generateDiffs", JDiffTask::class.java) {
// Base classpath is Android SDK, sub-projects add their own.
- classpath = project.androidJar()
+ classpath = androidJarFile(project)
// JDiff properties.
oldApiXmlFile = oldApiTask.outputApiXmlFile
@@ -358,7 +357,7 @@
from(generateDocs.destinationDir)
baseName = "android-support-docs"
version = project.buildNumber()
-
+ destinationDir = project.distDir()
doLast {
logger.lifecycle("'Wrote API reference to $archivePath")
}
@@ -366,33 +365,18 @@
// Set up platform API files for federation.
private fun createGenerateSdkApiTask(project: Project, doclavaConfig: Configuration): Task =
- if (project.androidApiTxt() != null) {
- project.tasks.createWithConfig("generateSdkApi", Copy::class.java) {
- description = "Copies the API files for the current SDK."
- // Export the API files so this looks like a DoclavaTask.
- from(project.androidApiTxt()!!.absolutePath)
- val apiFile = sdkApiFile(project)
- into(apiFile.parent)
- rename { apiFile.name }
- // Register the fake removed file as an output.
- val removedApiFile = removedSdkApiFile(project)
- outputs.file(removedApiFile)
- doLast { removedApiFile.createNewFile() }
- }
- } else {
- project.tasks.createWithConfig("generateSdkApi", DoclavaTask::class.java) {
- dependsOn(doclavaConfig)
- description = "Generates API files for the current SDK."
- setDocletpath(doclavaConfig.resolve())
- destinationDir = project.docsDir()
- classpath = project.androidJar()
- source(project.zipTree(project.androidSrcJar()))
- apiFile = sdkApiFile(project)
- removedApiFile = removedSdkApiFile(project)
- generateDocs = false
- coreJavadocOptions {
- addStringOption("stubpackages", "android.*")
- }
+ project.tasks.createWithConfig("generateSdkApi", DoclavaTask::class.java) {
+ dependsOn(doclavaConfig)
+ description = "Generates API files for the current SDK."
+ setDocletpath(doclavaConfig.resolve())
+ destinationDir = project.docsDir()
+ classpath = androidJarFile(project)
+ source(project.zipTree(androidSrcJarFile(project)))
+ apiFile = sdkApiFile(project)
+ removedApiFile = removedSdkApiFile(project)
+ generateDocs = false
+ coreJavadocOptions {
+ addStringOption("stubpackages", "android.*")
}
}
@@ -411,7 +395,7 @@
setDocletpath(doclavaConfig.resolve())
val offline = project.processProperty("offlineDocs") != null
destinationDir = File(project.docsDir(), if (offline) "offline" else "online")
- classpath = project.androidJar()
+ classpath = androidJarFile(project)
val hidden = listOf<Int>(105, 106, 107, 111, 112, 113, 115, 116, 121)
doclavaErrors = ((101..122) - hidden).toSet()
doclavaWarnings = emptySet()
@@ -490,7 +474,7 @@
}
// Check whether the development API surface has changed.
- val verifyConfig = if (version.isPatch) CHECK_API_CONFIG_PATCH else CHECK_API_CONFIG_DEVELOP
+ val verifyConfig = if (version.isPatch()) CHECK_API_CONFIG_PATCH else CHECK_API_CONFIG_DEVELOP
val currentApiFile = getApiFile(workingDir, version)
val checkApi = createCheckApiTask(project,
"checkApi",
@@ -602,18 +586,23 @@
config: T.() -> Unit) =
create(name, taskClass) { task -> task.config() }
+private fun androidJarFile(project: Project): FileCollection =
+ project.files(arrayOf(File(project.fullSdkPath(),
+ "platforms/android-${SupportConfig.CURRENT_SDK_VERSION}/android.jar")))
+
+private fun androidSrcJarFile(project: Project): File = File(project.fullSdkPath(),
+ "platforms/android-${SupportConfig.CURRENT_SDK_VERSION}/android-stubs-src.jar")
+
// Nasty part. Get rid of that eventually!
private fun Project.docsDir(): File = properties["docsDir"] as File
-private fun Project.androidJar() = rootProject.properties["androidJar"] as FileCollection
-
-private fun Project.androidSrcJar() = rootProject.properties["androidSrcJar"] as File
+private fun Project.fullSdkPath(): File = rootProject.properties["fullSdkPath"] as File
private fun Project.version() = Version(project.version as String)
private fun Project.buildNumber() = properties["buildNumber"] as String
-private fun Project.androidApiTxt() = properties["androidApiTxt"] as? File
+private fun Project.distDir(): File = rootProject.properties["distDir"] as File
private fun Project.processProperty(name: String) =
if (hasProperty(name)) {
diff --git a/buildSrc/src/main/kotlin/android/support/GroovyInteroperability.kt b/buildSrc/src/main/kotlin/android/support/GroovyInteroperability.kt
deleted file mode 100644
index ec2d2f6..0000000
--- a/buildSrc/src/main/kotlin/android/support/GroovyInteroperability.kt
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * 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
index 1698bf9..c8be47a 100644
--- a/buildSrc/src/main/kotlin/android/support/MavenUploadHelper.kt
+++ b/buildSrc/src/main/kotlin/android/support/MavenUploadHelper.kt
@@ -21,15 +21,17 @@
import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.artifacts.maven.MavenDeployer
import org.gradle.api.tasks.Upload
+import org.gradle.kotlin.dsl.withGroovyBuilder
+import java.io.File
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");
+ throw Exception("You must specify mavenGroup for ${project.name} project")
}
if (extension.mavenVersion == null) {
- throw Exception("You must specify mavenVersion for " + project.name + " project");
+ throw Exception("You must specify mavenVersion for ${project.name} project")
}
project.group = extension.mavenGroup!!
project.version = extension.mavenVersion.toString()
@@ -40,9 +42,22 @@
// Set uploadArchives options.
val uploadTask = project.tasks.getByName("uploadArchives") as Upload
+
+ val repo = project.uri(project.rootProject.property("supportRepoOut") as File)
+ ?: throw Exception("supportRepoOut not set")
+
+ uploadTask.repositories {
+ it.withGroovyBuilder {
+ "mavenDeployer" {
+ "repository"(mapOf("url" to repo))
+ }
+ }
+ }
+
project.afterEvaluate {
if (extension.publish) {
uploadTask.repositories.withType(MavenDeployer::class.java) { mavenDeployer ->
+ mavenDeployer.isUniqueVersion = true
mavenDeployer.getPom().project {
it.withGroovyBuilder {
@@ -68,7 +83,7 @@
"scm" {
"url"("http://source.android.com")
- "connection"("scm:git:https://android.googlesource.com/platform/frameworks/support")
+ "connection"(ANDROID_GIT_URL)
}
"developers" {
@@ -83,9 +98,9 @@
// 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");
+ collectDependenciesForConfiguration(allDeps, project, "api")
+ collectDependenciesForConfiguration(allDeps, project, "implementation")
+ collectDependenciesForConfiguration(allDeps, project, "compile")
mavenDeployer.getPom().whenConfigured {
it.dependencies.forEach { dep ->
@@ -95,10 +110,10 @@
val getGroupIdMethod =
dep::class.java.getDeclaredMethod("getGroupId")
- val groupId : String = getGroupIdMethod.invoke(dep) as String
+ val groupId: String = getGroupIdMethod.invoke(dep) as String
val getArtifactIdMethod =
dep::class.java.getDeclaredMethod("getArtifactId")
- val artifactId : String = getArtifactIdMethod.invoke(dep) as String
+ val artifactId: String = getArtifactIdMethod.invoke(dep) as String
if (isAndroidProject(groupId, artifactId, allDeps)) {
val setTypeMethod = dep::class.java.getDeclaredMethod("setType",
@@ -109,14 +124,23 @@
}
}
}
+
+ // Before the upload, make sure the repo is ready.
+ uploadTask.dependsOn(project.rootProject.tasks.getByName("prepareRepo"))
+
+ // Make the mainUpload depend on this uploadTask one.
+ project.rootProject.tasks.getByName("mainUpload").dependsOn(uploadTask)
} else {
uploadTask.enabled = false
}
}
}
-private fun collectDependenciesForConfiguration(projectDependencies : MutableSet<ProjectDependency>,
- project : Project, name : String) {
+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 {
@@ -125,12 +149,18 @@
}
}
-private fun isAndroidProject(groupId : String, artifactId : String,
- deps : Set<ProjectDependency>) : Boolean {
+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
+}
+
+private const val ANDROID_GIT_URL =
+ "scm:git:https://android.googlesource.com/platform/frameworks/support"
\ 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
index e55f383..c660604 100644
--- a/buildSrc/src/main/kotlin/android/support/SupportAndroidLibraryPlugin.kt
+++ b/buildSrc/src/main/kotlin/android/support/SupportAndroidLibraryPlugin.kt
@@ -19,6 +19,7 @@
import android.support.SupportConfig.INSTRUMENTATION_RUNNER
import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.internal.dsl.LintOptions
+import com.android.build.gradle.tasks.GenerateBuildConfig
import net.ltgt.gradle.errorprone.ErrorProneBasePlugin
import net.ltgt.gradle.errorprone.ErrorProneToolChain
import org.gradle.api.JavaVersion
@@ -72,13 +73,22 @@
library.compileOptions.setSourceCompatibility(javaVersion)
library.compileOptions.setTargetCompatibility(javaVersion)
- }
- VersionFileWriterTask.setUpAndroidLibrary(project)
+ VersionFileWriterTask.setUpAndroidLibrary(project, library)
+ }
project.apply(mapOf("plugin" to "com.android.library"))
project.apply(mapOf("plugin" to ErrorProneBasePlugin::class.java))
+ project.afterEvaluate {
+ project.tasks.all({
+ if (it is GenerateBuildConfig) {
+ // Disable generating BuildConfig.java
+ it.enabled = false
+ }
+ })
+ }
+
project.configurations.all { configuration ->
if (isCoreSupportLibrary && project.name != "support-annotations") {
// While this usually happens naturally due to normal project dependencies, force
@@ -102,16 +112,13 @@
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)
- }
+ library.compileSdkVersion(SupportConfig.CURRENT_SDK_VERSION)
- library.buildToolsVersion = SupportConfig.getBuildTools(project)
+ library.buildToolsVersion = SupportConfig.BUILD_TOOLS_VERSION
// Update the version meta-data in each Manifest.
- library.defaultConfig.addManifestPlaceholders(mapOf("target-sdk-version" to currentSdk))
+ library.defaultConfig.addManifestPlaceholders(
+ mapOf("target-sdk-version" to SupportConfig.CURRENT_SDK_VERSION))
// Set test runner.
library.defaultConfig.testInstrumentationRunner = INSTRUMENTATION_RUNNER
@@ -122,13 +129,13 @@
library.signingConfigs.findByName("debug")?.storeFile =
SupportConfig.getKeystore(project)
- setUpLint(library.lintOptions, SupportConfig.getLintBaseline(project))
-
- if (SupportConfig.isUsingFullSdk(project)) {
- // Library projects don't run lint by default, so set up dependency.
- project.tasks.getByName("uploadArchives").dependsOn("lintRelease")
+ project.afterEvaluate {
+ setUpLint(library.lintOptions, SupportConfig.getLintBaseline(project),
+ (supportLibraryExtension.mavenVersion?.isSnapshot()) ?: true)
}
+ project.tasks.getByName("uploadArchives").dependsOn("lintRelease")
+
SourceJarTaskHelper.setUpAndroidProject(project, library)
val toolChain = ErrorProneToolChain.create(project)
@@ -160,7 +167,7 @@
}
}
-private fun setUpLint(lintOptions: LintOptions, baseline: File) {
+private fun setUpLint(lintOptions: LintOptions, baseline: File, snapshotVersion: Boolean) {
// Always lint check NewApi as fatal.
lintOptions.isAbortOnError = true
lintOptions.isIgnoreWarnings = true
@@ -178,12 +185,21 @@
lintOptions.isNoLines = false
lintOptions.isQuiet = true
- lintOptions.error("NewApi")
+ lintOptions.fatal("NewApi")
- // Set baseline file for all legacy lint warnings.
+ if (snapshotVersion) {
+ // Do not run missing translations checks on snapshot versions of the library.
+ lintOptions.disable("MissingTranslation")
+ } else {
+ lintOptions.fatal("MissingTranslation")
+ }
+
if (System.getenv("GRADLE_PLUGIN_VERSION") != null) {
lintOptions.check("NewApi")
- } else if (baseline.exists()) {
+ }
+
+ // Set baseline file for all legacy lint warnings.
+ if (baseline.exists()) {
lintOptions.baseline(baseline)
}
}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/android/support/SupportAndroidTestAppPlugin.kt b/buildSrc/src/main/kotlin/android/support/SupportAndroidTestAppPlugin.kt
index 6d73c99..89e8177 100644
--- a/buildSrc/src/main/kotlin/android/support/SupportAndroidTestAppPlugin.kt
+++ b/buildSrc/src/main/kotlin/android/support/SupportAndroidTestAppPlugin.kt
@@ -42,19 +42,10 @@
val application = project.extensions.findByType(AppExtension::class.java)
?: throw Exception("Failed to find Android extension")
- val currentSdk = project.property("currentSdk")
- when (currentSdk) {
- is Int -> {
- application.compileSdkVersion(currentSdk)
- application.defaultConfig.targetSdkVersion(currentSdk)
- }
- is String -> {
- application.compileSdkVersion(currentSdk)
- application.defaultConfig.targetSdkVersion(currentSdk)
- }
- }
+ application.compileSdkVersion(SupportConfig.CURRENT_SDK_VERSION)
+ application.defaultConfig.targetSdkVersion(SupportConfig.CURRENT_SDK_VERSION)
- application.buildToolsVersion = SupportConfig.getBuildTools(project)
+ application.buildToolsVersion = SupportConfig.BUILD_TOOLS_VERSION
application.defaultConfig.versionCode = 1
application.defaultConfig.versionName = "1.0"
diff --git a/buildSrc/src/main/kotlin/android/support/SupportConfig.kt b/buildSrc/src/main/kotlin/android/support/SupportConfig.kt
index 26c3bf3..94573c6 100644
--- a/buildSrc/src/main/kotlin/android/support/SupportConfig.kt
+++ b/buildSrc/src/main/kotlin/android/support/SupportConfig.kt
@@ -23,10 +23,8 @@
object SupportConfig {
const val DEFAULT_MIN_SDK_VERSION = 14
const val INSTRUMENTATION_RUNNER = "android.support.test.runner.AndroidJUnitRunner"
-
- fun getBuildTools(project: Project): String {
- return project.rootProject.property("buildToolsVersion") as String
- }
+ const val BUILD_TOOLS_VERSION = "27.0.1"
+ const val CURRENT_SDK_VERSION = 27
fun getKeystore(project: Project): File {
val supportRoot = (project.rootProject.property("ext") as ExtraPropertiesExtension)
@@ -34,10 +32,6 @@
return File(supportRoot, "development/keystore/debug.keystore")
}
- fun isUsingFullSdk(project: Project): Boolean {
- return project.rootProject.property("usingFullSdk") as Boolean
- }
-
fun getLintBaseline(project: Project): File {
return File(project.projectDir, "/lint-baseline.xml")
}
diff --git a/buildSrc/src/main/kotlin/android/support/Version.kt b/buildSrc/src/main/kotlin/android/support/Version.kt
new file mode 100644
index 0000000..0a9d945
--- /dev/null
+++ b/buildSrc/src/main/kotlin/android/support/Version.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support
+
+import java.io.File
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+/**
+ * Utility class which represents a version
+ */
+data class Version(
+ val major: Int,
+ val minor: Int,
+ val patch: Int,
+ val extra: String? = null
+) : Comparable<Version> {
+
+ constructor(versionString: String) : this(
+ Integer.parseInt(checkedMatcher(versionString).group(1)),
+ Integer.parseInt(checkedMatcher(versionString).group(2)),
+ Integer.parseInt(checkedMatcher(versionString).group(3)),
+ if (checkedMatcher(versionString).groupCount() == 4) checkedMatcher(
+ versionString).group(4) else null)
+
+ fun isPatch(): Boolean = patch != 0
+
+ fun isSnapshot(): Boolean = "-SNAPSHOT" == extra
+
+ override fun compareTo(other: Version) = compareValuesBy(this, other,
+ { it.major },
+ { it.minor },
+ { it.patch },
+ { it.extra == null }, // False (no extra) sorts above true (has extra)
+ { it.extra } // gradle uses lexicographic ordering
+ )
+
+ override fun toString(): String {
+ return "$major.$minor.$patch${extra ?: ""}"
+ }
+
+ companion object {
+ private val VERSION_FILE_REGEX = Pattern.compile("^(\\d+\\.\\d+\\.\\d+).txt$")
+ private val VERSION_REGEX = Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+)(-.+)?$")
+
+ private fun checkedMatcher(versionString: String): Matcher {
+ val matcher = VERSION_REGEX.matcher(versionString)
+ if (!matcher.matches()) {
+ throw IllegalArgumentException("Can not parse version: " + versionString)
+ }
+ return matcher
+ }
+
+ /**
+ * @return Version or null, if a name of the given file doesn't match
+ */
+ fun parseOrNull(file: File): Version? {
+ if (!file.isFile) return null
+ val matcher = VERSION_FILE_REGEX.matcher(file.name)
+ return if (matcher.matches()) Version(matcher.group(1)) else null
+ }
+
+ /**
+ * @return Version or null, if the given string doesn't match
+ */
+ fun parseOrNull(versionString: String): Version? {
+ val matcher = VERSION_REGEX.matcher(versionString)
+ return if (matcher.matches()) Version(versionString) else null
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/android/support/VersionFileWriterTask.kt b/buildSrc/src/main/kotlin/android/support/VersionFileWriterTask.kt
new file mode 100644
index 0000000..cde11e6
--- /dev/null
+++ b/buildSrc/src/main/kotlin/android/support/VersionFileWriterTask.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support
+
+import com.android.build.gradle.LibraryExtension
+import org.gradle.api.DefaultTask
+import org.gradle.api.Project
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+import java.io.PrintWriter
+
+/**
+ * Task that allows to write a version to a given output file.
+ */
+open class VersionFileWriterTask : DefaultTask() {
+ @get:Input
+ lateinit var version: String
+ @get:OutputFile
+ lateinit var outputFile: File
+
+ /**
+ * The main method for actually writing out the file.
+ */
+ @TaskAction
+ fun run() {
+ val writer = PrintWriter(outputFile)
+ writer.println(version)
+ writer.close()
+ }
+
+ companion object {
+ val RESOURCE_DIRECTORY = "generatedResources"
+ val VERSION_FILE_PATH = RESOURCE_DIRECTORY + "/META-INF/%s_%s.version"
+
+ /**
+ * Sets up Android Library project to have a task that generates a version file.
+ * It must be called after [LibraryExtension] has been resolved.
+ *
+ * @param project an Android Library project.
+ */
+ fun setUpAndroidLibrary(project: Project, library: LibraryExtension) {
+ val group = project.properties["group"] as String
+ val artifactId = project.properties["name"] as String
+ val version = project.properties["version"] as String
+
+ // Add a java resource file to the library jar for version tracking purposes.
+ val artifactName = File(
+ project.buildDir,
+ String.format(VERSION_FILE_PATH, group, artifactId))
+
+ val writeVersionFile = project.tasks.create("writeVersionFile",
+ VersionFileWriterTask::class.java)
+ writeVersionFile.version = version
+ writeVersionFile.outputFile = artifactName
+
+ library.libraryVariants.all {
+ it.processJavaResources.dependsOn(writeVersionFile)
+ }
+
+ library.sourceSets.getByName("main").resources.srcDir(
+ File(project.buildDir, RESOURCE_DIRECTORY)
+ )
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/android/support/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/android/support/dependencies/Dependencies.kt
index 48103d2..e370801 100644
--- a/buildSrc/src/main/kotlin/android/support/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/android/support/dependencies/Dependencies.kt
@@ -57,4 +57,5 @@
const val SUPPORT_V4 = "com.android.support:support-v4:$SUPPORT_VERSION"
// Arch libraries
-const val ARCH_LIFECYCLE_RUNTIME = "android.arch.lifecycle:runtime:1.0.3@aar"
\ No newline at end of file
+const val ARCH_LIFECYCLE_RUNTIME = "android.arch.lifecycle:runtime:1.0.3@aar"
+const val ARCH_LIFECYCLE_EXTENSIONS = "android.arch.lifecycle:extensions:1.0.0@aar"
diff --git a/buildSrc/src/main/kotlin/android/support/docs/GenerateDocsTask.kt b/buildSrc/src/main/kotlin/android/support/docs/GenerateDocsTask.kt
index 2183b88..e889a54 100644
--- a/buildSrc/src/main/kotlin/android/support/docs/GenerateDocsTask.kt
+++ b/buildSrc/src/main/kotlin/android/support/docs/GenerateDocsTask.kt
@@ -48,7 +48,7 @@
fun addSinceFilesFrom(dir: File) {
File(dir, "api").listFiles().forEach { file ->
- Version.from(file)?.let { version ->
+ Version.parseOrNull(file)?.let { version ->
sinces.add(Since(file.absolutePath, version.toString()))
}
}
diff --git a/buildSrc/src/main/kotlin/android/support/gmaven/GMavenVersionChecker.kt b/buildSrc/src/main/kotlin/android/support/gmaven/GMavenVersionChecker.kt
new file mode 100644
index 0000000..7fc4836
--- /dev/null
+++ b/buildSrc/src/main/kotlin/android/support/gmaven/GMavenVersionChecker.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.gmaven
+
+import android.support.Version
+import groovy.util.XmlSlurper
+import groovy.util.slurpersupport.Node
+import groovy.util.slurpersupport.NodeChild
+import org.gradle.api.GradleException
+import org.gradle.api.logging.Logger
+import java.io.FileNotFoundException
+import java.io.IOException
+
+/**
+ * Queries maven.google.com to get the version numbers for each artifact.
+ * Due to the structure of maven.google.com, a new query is necessary for each group.
+ *
+ * @param logger Logger of the root project. No reason to create multiple instances of this.
+ */
+class GMavenVersionChecker(private val logger: Logger) {
+ private val versionCache: MutableMap<String, GroupVersionData> = HashMap()
+
+ /**
+ * Checks whether the given artifact is already on maven.google.com.
+ *
+ * @param group The project group on maven
+ * @param artifactName The artifact name on maven
+ * @param version The version on maven
+ * @return true if the artifact is already on maven.google.com
+ */
+ fun isReleased(group: String, artifactName: String, version: String): Boolean {
+ return getVersions(group, artifactName)?.contains(Version(version)) ?: false
+ }
+
+ /**
+ * Return the available versions on maven.google.com for a given artifact
+ *
+ * @param group The group id of the artifact
+ * @param artifactName The name of the artifact
+ * @return The set of versions that are available on maven.google.com. Null if artifact is not
+ * available.
+ */
+ private fun getVersions(group: String, artifactName: String): Set<Version>? {
+ val groupData = getVersionData(group)
+ return groupData?.artifacts?.get(artifactName)?.versions
+ }
+
+ /**
+ * Returns the version data for each artifact in a given group.
+ * <p>
+ * If data is not cached, this will make a web request to get it.
+ *
+ * @param group The group to query
+ * @return A data class which has the versions for each artifact
+ */
+ private fun getVersionData(group: String): GroupVersionData? {
+ return versionCache.getOrMaybePut(group) {
+ fetchGroup(group, DEFAULT_RETRY_LIMIT)
+ }
+ }
+
+ /**
+ * Fetches the group version information from maven.google.com
+ *
+ * @param group The group name to fetch
+ * @param retryCount Number of times we'll retry before failing
+ * @return GroupVersionData that has the data or null if it is a new item.
+ */
+ private fun fetchGroup(group: String, retryCount: Int): GroupVersionData? {
+ val url = buildGroupUrl(group)
+ for (run in 0..retryCount) {
+ logger.info("fetching maven XML from $url")
+ try {
+ val parsedXml = XmlSlurper(false, false).parse(url) as NodeChild
+ return GroupVersionData.from(parsedXml)
+ } catch (ignored: FileNotFoundException) {
+ logger.info("could not find version data for $group, seems like a new file")
+ return null
+ } catch (ioException: IOException) {
+ logger.warn("failed to fetch the maven info, retrying in 2 seconds. " +
+ "Run $run of $retryCount")
+ Thread.sleep(RETRY_DELAY)
+ }
+ }
+ throw GradleException("Could not access maven.google.com")
+ }
+
+ companion object {
+ /**
+ * Creates the URL which has the XML file that describes the available versions for each
+ * artifact in that group
+ *
+ * @param group Maven group name
+ * @return The URL of the XML file
+ */
+ private fun buildGroupUrl(group: String) =
+ "$BASE${group.replace(".","/")}/$GROUP_FILE"
+ }
+}
+
+private fun <K, V> MutableMap<K, V>.getOrMaybePut(key: K, defaultValue: () -> V?): V? {
+ val value = get(key)
+ return if (value == null) {
+ val answer = defaultValue()
+ if (answer != null) put(key, answer)
+ answer
+ } else {
+ value
+ }
+}
+
+/**
+ * Data class that holds the artifacts of a single maven group.
+ *
+ * @param name Maven group name
+ * @param artifacts Map of artifact versions keyed by artifact name
+ */
+private data class GroupVersionData(
+ val name: String,
+ val artifacts: Map<String, ArtifactVersionData>
+) {
+ companion object {
+ /**
+ * Constructs an instance from the given node.
+ *
+ * @param xml The information node fetched from {@code GROUP_FILE}
+ */
+ fun from(xml: NodeChild): GroupVersionData {
+ /*
+ * sample input:
+ * <android.arch.core>
+ * <runtime versions="1.0.0-alpha4,1.0.0-alpha5,1.0.0-alpha6,1.0.0-alpha7"/>
+ * <common versions="1.0.0-alpha4,1.0.0-alpha5,1.0.0-alpha6,1.0.0-alpha7"/>
+ * </android.arch.core>
+ */
+ val name = xml.name()
+ val artifacts: MutableMap<String, ArtifactVersionData> = HashMap()
+
+ xml.childNodes().forEach {
+ val node = it as Node
+ val versions = (node.attributes()["versions"] as String).split(",").map {
+ Version(it)
+ }.toSet()
+ artifacts.put(it.name(), ArtifactVersionData(it.name(), versions))
+ }
+ return GroupVersionData(name, artifacts)
+ }
+ }
+}
+
+/**
+ * Data class that holds the version information about a single artifact
+ *
+ * @param name Name of the maven artifact
+ * @param versions set of version codes that are already on maven.google.com
+ */
+private data class ArtifactVersionData(val name: String, val versions: Set<Version>)
+
+// wait 2 seconds before retrying if fetch fails
+private const val RETRY_DELAY: Long = 2000 // ms
+
+// number of times we'll try to reach maven.google.com before failing
+private const val DEFAULT_RETRY_LIMIT = 20
+
+private const val BASE = "https://dl.google.com/dl/android/maven2/"
+private const val GROUP_FILE = "group-index.xml"
\ No newline at end of file
diff --git a/buildSrc/src/test/kotlin/android/support/VersionTest.kt b/buildSrc/src/test/kotlin/android/support/VersionTest.kt
new file mode 100644
index 0000000..339acab
--- /dev/null
+++ b/buildSrc/src/test/kotlin/android/support/VersionTest.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class VersionTest {
+ @Test
+ fun testComparisons() {
+ assert(true > false)
+
+ val version2600 = Version("26.0.0")
+ val version2610 = Version("26.1.0")
+ val version2611 = Version("26.1.1")
+ val version2620 = Version("26.2.0")
+ val version2621 = Version("26.2.1")
+ val version2700 = Version("27.0.0")
+ val version2700SNAPSHOT = Version("27.0.0-SNAPSHOT")
+ val version2700TNAPSHOT = Version("27.0.0-TNAPSHOT")
+
+ assertEquals(version2600, version2600)
+
+ assert(version2600 < version2700)
+
+ assert(version2600 < version2700)
+
+ assert(version2610 < version2611)
+ assert(version2610 < version2620)
+ assert(version2610 < version2621)
+ assert(version2610 < version2700)
+
+ assert(version2611 < version2620)
+ assert(version2611 < version2621)
+ assert(version2611 < version2700)
+
+ assert(version2700 > version2600)
+ assert(version2700 > version2700SNAPSHOT)
+ assert(version2700SNAPSHOT < version2700)
+
+ assert(version2700TNAPSHOT > version2700SNAPSHOT)
+ assert(version2700SNAPSHOT < version2700TNAPSHOT)
+ }
+}
\ No newline at end of file
diff --git a/car/OWNERS b/car/OWNERS
index d226975..eac51b8 100644
--- a/car/OWNERS
+++ b/car/OWNERS
@@ -1 +1,3 @@
-ajchen@google.com
\ No newline at end of file
+ajchen@google.com
+deanh@google.com
+yaoyx@google.com
diff --git a/car/res/drawable/car_borderless_button_text_color.xml b/car/res/drawable/car_borderless_button_text_color.xml
index ff27db5..27f79f0 100644
--- a/car/res/drawable/car_borderless_button_text_color.xml
+++ b/car/res/drawable/car_borderless_button_text_color.xml
@@ -17,5 +17,5 @@
<!-- Default text colors for car buttons when enabled/disabled. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/car_grey_700" android:state_enabled="false"/>
- <item android:color="?android:attr/colorPrimary"/>
+ <item android:color="?android:attr/colorButtonNormal"/>
</selector>
diff --git a/car/res/drawable/car_button_background.xml b/car/res/drawable/car_button_background.xml
index 1a8995c..58aa739 100644
--- a/car/res/drawable/car_button_background.xml
+++ b/car/res/drawable/car_button_background.xml
@@ -25,7 +25,7 @@
<item>
<shape android:shape="rectangle">
<corners android:radius="@dimen/car_button_radius"/>
- <solid android:color="?android:attr/colorPrimary"/>
+ <solid android:color="?android:attr/colorButtonNormal"/>
</shape>
</item>
</selector>
diff --git a/car/res/layout/car_paged_list_item_content.xml b/car/res/layout/car_paged_list_item_content.xml
index fd6a8a4..943c489 100644
--- a/car/res/layout/car_paged_list_item_content.xml
+++ b/car/res/layout/car_paged_list_item_content.xml
@@ -24,8 +24,7 @@
<ImageView
android:id="@+id/primary_icon"
android:layout_width="@dimen/car_single_line_list_item_height"
- android:layout_height="@dimen/car_single_line_list_item_height"
- android:layout_centerVertical="true"/>
+ android:layout_height="@dimen/car_single_line_list_item_height"/>
<!-- Text. -->
<TextView
@@ -55,10 +54,7 @@
<!-- End icon with divider. -->
<View
android:id="@+id/supplemental_icon_divider"
- android:layout_width="@dimen/car_vertical_line_divider_width"
- android:layout_height="@dimen/car_vertical_line_divider_height"
- android:layout_marginStart="@dimen/car_padding_4"
- android:background="@color/car_list_divider"/>
+ style="@style/CarListVerticalDivider"/>
<ImageView
android:id="@+id/supplemental_icon"
android:layout_width="@dimen/car_primary_icon_size"
@@ -66,13 +62,22 @@
android:layout_marginStart="@dimen/car_padding_4"
android:scaleType="fitCenter"/>
+ <!-- Switch with divider. -->
+ <View
+ android:id="@+id/switch_divider"
+ style="@style/CarListVerticalDivider"/>
+ <Switch
+ android:id="@+id/switch_widget"
+ android:layout_width="@dimen/car_primary_icon_size"
+ android:layout_height="@dimen/car_primary_icon_size"
+ android:layout_marginStart="@dimen/car_padding_4"
+ style="@android:style/Widget.Material.CompoundButton.Switch"
+ />
+
<!-- Up to 2 action buttons with dividers. -->
<View
android:id="@+id/action2_divider"
- android:layout_width="@dimen/car_vertical_line_divider_width"
- android:layout_height="@dimen/car_vertical_line_divider_height"
- android:layout_marginStart="@dimen/car_padding_4"
- android:background="@color/car_list_divider"/>
+ style="@style/CarListVerticalDivider"/>
<Button
android:id="@+id/action2"
android:layout_width="wrap_content"
@@ -83,13 +88,10 @@
android:maxLines="1"
android:background="@color/car_card"
android:foreground="@drawable/car_card_ripple_background"
- style="@style/CarButton.Borderless"/>
+ style="?android:attr/borderlessButtonStyle"/>
<View
android:id="@+id/action1_divider"
- android:layout_width="@dimen/car_vertical_line_divider_width"
- android:layout_height="@dimen/car_vertical_line_divider_height"
- android:layout_marginStart="@dimen/car_padding_4"
- android:background="@color/car_list_divider"/>
+ style="@style/CarListVerticalDivider"/>
<Button
android:id="@+id/action1"
android:layout_width="wrap_content"
@@ -100,6 +102,6 @@
android:maxLines="1"
android:background="@color/car_card"
android:foreground="@drawable/car_card_ripple_background"
- style="@style/CarButton.Borderless"/>
+ style="?android:attr/borderlessButtonStyle"/>
</LinearLayout>
</RelativeLayout>
diff --git a/car/res/values/colors.xml b/car/res/values/colors.xml
index 8f82c01..88af182 100644
--- a/car/res/values/colors.xml
+++ b/car/res/values/colors.xml
@@ -99,8 +99,8 @@
<color name="car_body4_dark">@android:color/black</color>
<color name="car_body4">@color/car_body4_dark</color>
- <color name="car_action1_light">@color/car_grey_50</color>
- <color name="car_action1_dark">@color/car_grey_900</color>
+ <color name="car_action1_light">@color/car_grey_900</color>
+ <color name="car_action1_dark">@color/car_grey_50</color>
<color name="car_action1">@color/car_action1_dark</color>
<!-- The tinting colors to create a light- and dark-colored icon respectively. -->
@@ -164,4 +164,8 @@
<color name="car_highlight_light">@color/car_teal_700</color>
<color name="car_highlight_dark">@color/car_teal_200</color>
<color name="car_highlight">@color/car_highlight_light</color>
+
+ <color name="car_accent_light">@color/car_teal_700</color>
+ <color name="car_accent_dark">@color/car_teal_200</color>
+ <color name="car_accent">@color/car_accent_light</color>
</resources>
diff --git a/car/res/values/styles.xml b/car/res/values/styles.xml
index df194cc..78d9603 100644
--- a/car/res/values/styles.xml
+++ b/car/res/values/styles.xml
@@ -124,42 +124,46 @@
</style>
<!-- The styles for the regular and borderless buttons -->
- <style name="CarButton" parent="Widget.AppCompat.Button">
+ <style name="Widget.Car.Button" parent="Widget.AppCompat.Button">
<item name="android:layout_height">@dimen/car_button_height</item>
<item name="android:minWidth">@dimen/car_button_min_width</item>
<item name="android:paddingStart">@dimen/car_button_horizontal_padding</item>
<item name="android:paddingEnd">@dimen/car_button_horizontal_padding</item>
- <item name="android:textStyle">normal</item>
<item name="android:textSize">@dimen/car_action1_size</item>
- <item name="android:textColor">@drawable/car_button_text_color</item>
- <item name="android:textAllCaps">true</item>
<item name="android:background">@drawable/car_button_background</item>
+ <item name="android:textColor">@drawable/car_button_text_color</item>
</style>
- <style name="CarButton.Borderless" parent="Widget.AppCompat.Button.Borderless">
+ <style name="Widget.Car.Button.Borderless.Colored"
+ parent="Widget.AppCompat.Button.Borderless.Colored">
<item name="android:layout_height">@dimen/car_button_height</item>
<item name="android:paddingStart">@dimen/car_borderless_button_horizontal_padding</item>
<item name="android:paddingEnd">@dimen/car_borderless_button_horizontal_padding</item>
- <item name="android:textStyle">normal</item>
<item name="android:textSize">@dimen/car_action1_size</item>
<item name="android:textColor">@drawable/car_borderless_button_text_color</item>
- <item name="android:textAllCaps">true</item>
</style>
<!-- Style for the progress bars -->
- <style name="CarProgressBar.Horizontal"
+ <style name="Widget.Car.ProgressBar.Horizontal"
parent="Widget.AppCompat.ProgressBar.Horizontal">
<item name="android:minHeight">@dimen/car_progress_bar_height</item>
<item name="android:maxHeight">@dimen/car_progress_bar_height</item>
</style>
+ <style name="Widget.Car.EditText" parent="Widget.AppCompat.EditText">
+ <item name="android:textColor">?attr/editTextColor</item>
+ <item name="android:textAppearance">@style/CarBody1</item>
+ </style>
+
<!-- Styles for TextInputLayout hints -->
<style name="CarHintTextAppearance" parent="CarBody2">
</style>
- <!-- Styles for Car Dialogs -->
- <style name="CarDialog" parent="Theme.AppCompat.Light.Dialog.Alert">
- <item name="android:background">@color/car_card</item>
- <item name="android:listDividerAlertDialog">@drawable/car_list_divider</item>
+ <style name="CarListVerticalDivider">
+ <item name="android:layout_width">@dimen/car_vertical_line_divider_width</item>
+ <item name="android:layout_height">@dimen/car_vertical_line_divider_height</item>
+ <item name="android:layout_marginStart">@dimen/car_padding_4</item>
+ <item name="android:background">@color/car_list_divider</item>
</style>
+
</resources>
diff --git a/car/res/values/themes.xml b/car/res/values/themes.xml
index 4244a22..ebb57d8 100644
--- a/car/res/values/themes.xml
+++ b/car/res/values/themes.xml
@@ -25,4 +25,28 @@
<item name="contentInsetStart">@dimen/car_keyline_1</item>
<item name="contentInsetEnd">@dimen/car_keyline_1</item>
</style>
+
+ <!-- Styles for Car Dialogs -->
+ <style name="Theme.Car.Light.Dialog.Alert" parent="Theme.AppCompat.Light.Dialog.Alert">
+ <item name="android:background">@color/car_card</item>
+ <item name="android:listDividerAlertDialog">@drawable/car_list_divider</item>
+ </style>
+
+ <!-- Base style for the Car -->
+ <style name="Theme.Car.NoActionBar" parent="Theme.AppCompat.NoActionBar">
+ <item name="android:colorAccent">@color/car_accent</item>
+ <item name="android:colorButtonNormal">@color/car_accent</item>
+ <item name="android:buttonStyle">@style/Widget.Car.Button</item>
+ <item name="android:borderlessButtonStyle">@style/Widget.Car.Button.Borderless.Colored
+ </item>
+ <item name="android:alertDialogTheme">@style/Theme.Car.Light.Dialog.Alert</item>
+ <item name="android:progressBarStyleHorizontal">
+ @style/Widget.Car.ProgressBar.Horizontal
+ </item>
+ <item name="android:textColorHint">@color/car_body2</item>
+ <item name="android:editTextStyle">@style/Widget.Car.EditText</item>
+ <item name="android:editTextColor">@color/car_body1</item>
+ <item name="android:colorControlNormal">@color/car_body2</item>
+ <item name="android:colorControlHighlight">?android:attr/colorAccent</item>
+ </style>
</resources>
diff --git a/car/src/main/java/androidx/car/widget/ListItem.java b/car/src/main/java/androidx/car/widget/ListItem.java
index d292d6b..d345833 100644
--- a/car/src/main/java/androidx/car/widget/ListItem.java
+++ b/car/src/main/java/androidx/car/widget/ListItem.java
@@ -26,6 +26,7 @@
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.View;
+import android.widget.CompoundButton;
import android.widget.RelativeLayout;
import java.lang.annotation.Retention;
@@ -58,6 +59,7 @@
* <li>Supplemental Icon
* <li>One Action Button
* <li>Two Action Buttons
+ * <li>Switch</li>
* </ul>
* </ul>
*
@@ -87,6 +89,7 @@
vh.getPrimaryIcon(),
vh.getTitle(), vh.getBody(),
vh.getSupplementalIcon(), vh.getSupplementalIconDivider(),
+ vh.getSwitch(), vh.getSwitchDivider(),
vh.getAction1(), vh.getAction1Divider(), vh.getAction2(), vh.getAction2Divider()};
for (View v : subviews) {
v.setVisibility(View.GONE);
@@ -94,6 +97,20 @@
}
/**
+ * Returns whether the divider that comes after the ListItem should be hidden or not.
+ *
+ * <p>Note: For this to work, one must invoke
+ * {@code PagedListView.setDividerVisibilityManager(adapter} for {@link ListItemAdapter} and
+ * have dividers enabled on {@link PagedListView}.
+ *
+ * @return {@code true} if divider coming after the item should be hidden, {@code false}
+ * otherwise.
+ */
+ boolean shouldHideDivider() {
+ return mBuilder.mHideDivider;
+ }
+
+ /**
* Functional interface to provide a way to interact with views in
* {@link ListItemAdapter.ViewHolder}. {@code ViewBinder}s added to a
* {@code ListItem} will be called when {@code ListItem} {@code bind}s to
@@ -127,13 +144,15 @@
@Retention(SOURCE)
@IntDef({SUPPLEMENTAL_ACTION_NO_ACTION, SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON,
- SUPPLEMENTAL_ACTION_ONE_ACTION, SUPPLEMENTAL_ACTION_TWO_ACTIONS})
+ SUPPLEMENTAL_ACTION_ONE_ACTION, SUPPLEMENTAL_ACTION_TWO_ACTIONS,
+ SUPPLEMENTAL_ACTION_SWITCH})
private @interface SupplementalActionType {}
private static final int SUPPLEMENTAL_ACTION_NO_ACTION = 0;
private static final int SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON = 1;
private static final int SUPPLEMENTAL_ACTION_ONE_ACTION = 2;
private static final int SUPPLEMENTAL_ACTION_TWO_ACTIONS = 3;
+ private static final int SUPPLEMENTAL_ACTION_SWITCH = 4;
private final Context mContext;
private final List<ViewBinder> mBinders = new ArrayList<>();
@@ -149,12 +168,18 @@
private String mTitle;
private String mBody;
private boolean mIsBodyPrimary;
+ // tag for indicating whether to hide the divider
+ private boolean mHideDivider;
@SupplementalActionType private int mSupplementalActionType = SUPPLEMENTAL_ACTION_NO_ACTION;
private int mSupplementalIconResId;
private View.OnClickListener mSupplementalIconOnClickListener;
private boolean mShowSupplementalIconDivider;
+ private boolean mSwitchChecked;
+ private boolean mShowSwitchDivider;
+ private CompoundButton.OnCheckedChangeListener mSwitchOnCheckedChangeListener;
+
private String mAction1Text;
private View.OnClickListener mAction1OnClickListener;
private boolean mShowAction1Divider;
@@ -282,12 +307,15 @@
R.dimen.car_keyline_1));
if (!TextUtils.isEmpty(mBody)) {
- // Set top margin.
+ // Set icon top margin so that the icon remains in the same position it
+ // would've been in for non-long-text item, namely so that the center
+ // line of icon matches that of line item.
layoutParams.removeRule(RelativeLayout.CENTER_VERTICAL);
- layoutParams.topMargin = mContext.getResources().getDimensionPixelSize(
- R.dimen.car_padding_4);
+ int itemHeight = mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_double_line_list_item_height);
+ layoutParams.topMargin = (itemHeight - iconSize) / 2;
} else {
- // Centered vertically.
+ // If the icon can be centered vertically, leave the work for framework.
layoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
layoutParams.topMargin = 0;
}
@@ -481,12 +509,32 @@
case SUPPLEMENTAL_ACTION_NO_ACTION:
// Do nothing
break;
+ case SUPPLEMENTAL_ACTION_SWITCH:
+ mBinders.add(vh -> {
+ vh.getSwitch().setVisibility(View.VISIBLE);
+ vh.getSwitch().setChecked(mSwitchChecked);
+ vh.getSwitch().setOnCheckedChangeListener(mSwitchOnCheckedChangeListener);
+ if (mShowSwitchDivider) {
+ vh.getSwitchDivider().setVisibility(View.VISIBLE);
+ }
+ });
+ break;
default:
throw new IllegalArgumentException("Unrecognized supplemental action type.");
}
}
/**
+ * Instructs the Builder to always hide the item divider coming after this ListItem.
+ *
+ * @return This Builder object to allow for chaining calls to set methods.
+ */
+ public Builder withDividerHidden() {
+ mHideDivider = true;
+ return this;
+ }
+
+ /**
* Sets {@link View.OnClickListener} of {@code ListItem}.
*
* @return This Builder object to allow for chaining calls to set methods.
@@ -652,6 +700,7 @@
*
* @param action1Text button text to display - this button will be closer to item end.
* @param action2Text button text to display.
+ * @return This Builder object to allow for chaining calls to set methods.
*/
public Builder withActions(String action1Text, boolean showAction1Divider,
View.OnClickListener action1OnClickListener,
@@ -675,6 +724,24 @@
}
/**
+ * Sets {@code Supplemental Action} to be represented by a {@link android.widget.Switch}.
+ *
+ * @param checked initial value for switched.
+ * @param showDivider whether to display a vertical bar between switch and text.
+ * @param listener callback to be invoked when the checked state is changed.
+ * @return This Builder object to allow for chaining calls to set methods.
+ */
+ public Builder withSwitch(boolean checked, boolean showDivider,
+ CompoundButton.OnCheckedChangeListener listener) {
+ mSupplementalActionType = SUPPLEMENTAL_ACTION_SWITCH;
+
+ mSwitchChecked = checked;
+ mShowSwitchDivider = showDivider;
+ mSwitchOnCheckedChangeListener = listener;
+ return this;
+ }
+
+ /**
* Adds {@link ViewBinder} to interact with sub-views in
* {@link ListItemAdapter.ViewHolder}. These ViewBinders will always bind after
* other {@link Builder} methods have bond.
diff --git a/car/src/main/java/androidx/car/widget/ListItemAdapter.java b/car/src/main/java/androidx/car/widget/ListItemAdapter.java
index c9b6177..a338a51 100644
--- a/car/src/main/java/androidx/car/widget/ListItemAdapter.java
+++ b/car/src/main/java/androidx/car/widget/ListItemAdapter.java
@@ -26,6 +26,7 @@
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.RelativeLayout;
+import android.widget.Switch;
import android.widget.TextView;
import androidx.car.R;
@@ -34,10 +35,16 @@
/**
* Adapter for {@link PagedListView} to display {@link ListItem}.
*
- * Implements {@link PagedListView.ItemCap} - defaults to unlimited item count.
+ * <ul>
+ * <li> Implements {@link PagedListView.ItemCap} - defaults to unlimited item count.
+ * <li> Implements {@link PagedListView.DividerVisibilityManager} - to control dividers after
+ * individual {@link ListItem}.
+ * </ul>
+ *
*/
public class ListItemAdapter extends
- RecyclerView.Adapter<ListItemAdapter.ViewHolder> implements PagedListView.ItemCap {
+ RecyclerView.Adapter<ListItemAdapter.ViewHolder> implements PagedListView.ItemCap,
+ PagedListView.DividerVisibilityManager {
/**
* Constant class for background style of items.
@@ -143,6 +150,15 @@
mMaxItems = maxItems;
}
+ @Override
+ public boolean shouldHideDivider(int position) {
+ // By default we should show the divider i.e. return false.
+
+ // Check if position is within range, and then check the item flag.
+ return position >= 0 && position < getItemCount()
+ && mItemProvider.get(position).shouldHideDivider();
+ }
+
/**
* Holds views of an item in PagedListView.
*
@@ -166,6 +182,9 @@
private Button mAction2;
private View mAction2Divider;
+ private Switch mSwitch;
+ private View mSwitchDivider;
+
public ViewHolder(View itemView) {
super(itemView);
@@ -179,6 +198,9 @@
mSupplementalIcon = itemView.findViewById(R.id.supplemental_icon);
mSupplementalIconDivider = itemView.findViewById(R.id.supplemental_icon_divider);
+ mSwitch = itemView.findViewById(R.id.switch_widget);
+ mSwitchDivider = itemView.findViewById(R.id.switch_divider);
+
mAction1 = itemView.findViewById(R.id.action1);
mAction1Divider = itemView.findViewById(R.id.action1_divider);
mAction2 = itemView.findViewById(R.id.action2);
@@ -209,6 +231,14 @@
return mSupplementalIconDivider;
}
+ public View getSwitchDivider() {
+ return mSwitchDivider;
+ }
+
+ public Switch getSwitch() {
+ return mSwitch;
+ }
+
public Button getAction1() {
return mAction1;
}
diff --git a/car/src/main/java/androidx/car/widget/PagedListView.java b/car/src/main/java/androidx/car/widget/PagedListView.java
index d90d670..63924d8 100644
--- a/car/src/main/java/androidx/car/widget/PagedListView.java
+++ b/car/src/main/java/androidx/car/widget/PagedListView.java
@@ -153,6 +153,23 @@
}
/**
+ * Interface for controlling visibility of item dividers for individual items based on the
+ * item's position.
+ *
+ * <p> NOTE: interface takes effect only when dividers are enabled.
+ */
+ public interface DividerVisibilityManager {
+ /**
+ * Given an item position, returns whether the divider coming after that item should be
+ * hidden.
+ *
+ * @param position item position inside the adapter.
+ * @return true if divider is to be hidden, false if divider should be shown.
+ */
+ boolean shouldHideDivider(int position);
+ }
+
+ /**
* The possible values for @{link #setGutter}. The default value is actually
* {@link Gutter#BOTH}.
*/
@@ -409,9 +426,26 @@
@NonNull RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter) {
mAdapter = adapter;
mRecyclerView.setAdapter(adapter);
+
updateMaxItems();
}
+ /**
+ * Sets {@link DividerVisibilityManager} on all {@code DividerDecoration} item decorations.
+ *
+ * @param dvm {@code DividerVisibilityManager} to be set.
+ */
+ public void setDividerVisibilityManager(DividerVisibilityManager dvm) {
+ int decorCount = mRecyclerView.getItemDecorationCount();
+ for (int i = 0; i < decorCount; i++) {
+ RecyclerView.ItemDecoration decor = mRecyclerView.getItemDecorationAt(i);
+ if (decor instanceof DividerDecoration) {
+ ((DividerDecoration) decor).setVisibilityManager(dvm);
+ }
+ }
+ mRecyclerView.invalidateItemDecorations();
+ }
+
@Nullable
@SuppressWarnings("unchecked")
public RecyclerView.Adapter<? extends RecyclerView.ViewHolder> getAdapter() {
@@ -960,6 +994,7 @@
private final int mDividerStartMargin;
@IdRes private final int mDividerStartId;
@IdRes private final int mDividerEndId;
+ private DividerVisibilityManager mVisibilityManager;
/**
* @param dividerStartMargin The start offset of the dividing line. This offset will be
@@ -989,11 +1024,23 @@
mPaint.setColor(mContext.getResources().getColor(R.color.car_list_divider));
}
+ /** Sets {@link DividerVisibilityManager} on the DividerDecoration.*/
+ public void setVisibilityManager(DividerVisibilityManager dvm) {
+ mVisibilityManager = dvm;
+ }
+
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
// Draw a divider line between each item. No need to draw the line for the last item.
for (int i = 0, childCount = parent.getChildCount(); i < childCount - 1; i++) {
View container = parent.getChildAt(i);
+
+ // if divider should be hidden for this item, proceeds without drawing it
+ int itemPosition = parent.getChildAdapterPosition(container);
+ if (hideDividerForAdapterPosition(itemPosition)) {
+ continue;
+ }
+
View nextContainer = parent.getChildAt(i + 1);
int spacing = nextContainer.getTop() - container.getBottom();
@@ -1034,14 +1081,21 @@
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
- // Skip top offset for first item and bottom offset for last.
- int position = parent.getChildAdapterPosition(view);
- if (position > 0) {
+ int pos = parent.getChildAdapterPosition(view);
+
+ // Skip top offset when there is no divider above.
+ if (pos > 0 && !hideDividerForAdapterPosition(pos - 1)) {
outRect.top = mDividerHeight / 2;
}
- if (position < state.getItemCount() - 1) {
+
+ // Skip bottom offset when there is no divider below.
+ if (pos < state.getItemCount() - 1 && !hideDividerForAdapterPosition(pos)) {
outRect.bottom = mDividerHeight / 2;
}
}
+
+ private boolean hideDividerForAdapterPosition(int position) {
+ return mVisibilityManager != null && mVisibilityManager.shouldHideDivider(position);
+ }
}
}
diff --git a/car/tests/AndroidManifest.xml b/car/tests/AndroidManifest.xml
index 8e5422d..f50977a 100644
--- a/car/tests/AndroidManifest.xml
+++ b/car/tests/AndroidManifest.xml
@@ -22,5 +22,6 @@
<activity android:name="androidx.car.widget.ColumnCardViewTestActivity"/>
<activity android:name="androidx.car.widget.PagedListViewSavedStateActivity"/>
<activity android:name="androidx.car.widget.PagedListViewTestActivity"/>
+ <activity android:name="androidx.car.widget.DividerVisibilityManagerTestActivity"/>
</application>
</manifest>
diff --git a/car/tests/res/layout/activity_paged_list_view_with_dividers.xml b/car/tests/res/layout/activity_paged_list_view_with_dividers.xml
new file mode 100644
index 0000000..31191c4
--- /dev/null
+++ b/car/tests/res/layout/activity_paged_list_view_with_dividers.xml
@@ -0,0 +1,29 @@
+<?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.
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.car.widget.PagedListView
+ android:id="@+id/paged_list_view_with_dividers"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:showPagedListViewDivider="true"
+ app:offsetScrollBar="true"/>
+</FrameLayout>
diff --git a/car/tests/res/values/dimens.xml b/car/tests/res/values/dimens.xml
new file mode 100644
index 0000000..58f1191
--- /dev/null
+++ b/car/tests/res/values/dimens.xml
@@ -0,0 +1,19 @@
+<?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>
+ <!-- Set to something large for testing. -->
+ <dimen name="car_list_divider_height">10dp</dimen>
+</resources>
diff --git a/car/tests/src/androidx/car/widget/DividerVisibilityManagerTest.java b/car/tests/src/androidx/car/widget/DividerVisibilityManagerTest.java
new file mode 100644
index 0000000..3555313
--- /dev/null
+++ b/car/tests/src/androidx/car/widget/DividerVisibilityManagerTest.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.widget;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import android.content.pm.PackageManager;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import androidx.car.test.R;
+
+/** Unit tests for implementations of {@link PagedListView.DividerVisibilityManager}. */
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public final class DividerVisibilityManagerTest {
+
+ /**
+ * Used by {@link TestAdapter} to calculate ViewHolder height so N items appear in one page of
+ * {@link PagedListView}. If you need to test behavior under multiple pages, set number of items
+ * to ITEMS_PER_PAGE * desired_pages.
+ * Actual value does not matter.
+ */
+ private static final int ITEMS_PER_PAGE = 10;
+
+ @Rule
+ public ActivityTestRule<DividerVisibilityManagerTestActivity> mActivityRule =
+ new ActivityTestRule<>(DividerVisibilityManagerTestActivity.class);
+
+ private DividerVisibilityManagerTestActivity mActivity;
+ private PagedListView mPagedListView;
+
+ @Before
+ public void setUp() {
+ mActivity = mActivityRule.getActivity();
+ mPagedListView = mActivity.findViewById(R.id.paged_list_view_with_dividers);
+ }
+
+ /** Returns {@code true} if the testing device has the automotive feature flag. */
+ private boolean isAutoDevice() {
+ PackageManager packageManager = mActivityRule.getActivity().getPackageManager();
+ return packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+ }
+
+ /** Sets up {@link #mPagedListView} with the given number of items. */
+ private void setUpPagedListView(int itemCount) {
+ try {
+ mActivityRule.runOnUiThread(() -> {
+ mPagedListView.setMaxPages(PagedListView.ItemCap.UNLIMITED);
+ mPagedListView.setAdapter(
+ new TestAdapter(itemCount, mPagedListView.getMeasuredHeight()));
+ });
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
+ throw new RuntimeException(throwable);
+ }
+ }
+
+ @Test
+ public void setCustomDividerVisibilityManager() throws Throwable {
+ if (!isAutoDevice()) {
+ return;
+ }
+
+ final int itemCount = 8;
+ setUpPagedListView(itemCount /* itemCount */);
+ RecyclerView.LayoutManager layoutManager =
+ mPagedListView.getRecyclerView().getLayoutManager();
+
+ // Fetch divider height.
+ final int dividerHeight = InstrumentationRegistry.getContext().getResources()
+ .getDimensionPixelSize(R.dimen.car_list_divider_height);
+
+
+ // Initially, dividers are present between each two items.
+ final View[] views = new View[itemCount];
+ mActivityRule.runOnUiThread(() -> {
+ for (int i = 0; i < layoutManager.getChildCount(); i++) {
+ views[i] = layoutManager.getChildAt(i);
+ }
+ });
+ for (int i = 0; i < itemCount - 1; i++) {
+ assertThat(views[i + 1].getTop() - views[i].getBottom(),
+ is(equalTo(2 * (dividerHeight / 2))));
+ }
+
+
+ // Set DividerVisibilityManager on PagedListView.
+ final PagedListView.DividerVisibilityManager dvm = new TestDividerVisibilityManager();
+ mActivityRule.runOnUiThread(() -> {
+ mPagedListView.setDividerVisibilityManager(dvm);
+ });
+
+ mActivityRule.runOnUiThread(() -> {
+ for (int i = 0; i < layoutManager.getChildCount(); i++) {
+ views[i] = layoutManager.getChildAt(i);
+ }
+ });
+
+ for (int i = 0; i < itemCount - 1; i++) {
+ int distance = views[i + 1].getTop() - views[i].getBottom();
+ if (dvm.shouldHideDivider(i)) {
+ assertEquals(distance, 0);
+ } else {
+ assertEquals(distance, 2 * (dividerHeight / 2));
+ }
+ }
+ }
+
+ @Test
+ public void testListItemAdapterAsVisibilityManager() {
+ // Create and populate ListItemAdapter.
+ ListItemProvider provider = new ListItemProvider.ListProvider(Arrays.asList(
+ new ListItem.Builder(mActivity)
+ .withDividerHidden()
+ .build(),
+ new ListItem.Builder(mActivity)
+ .build(),
+ new ListItem.Builder(mActivity)
+ .withDividerHidden()
+ .build(),
+ new ListItem.Builder(mActivity)
+ .withDividerHidden()
+ .build()));
+
+ ListItemAdapter itemAdapter = new ListItemAdapter(mActivity, provider);
+ assertTrue(itemAdapter.shouldHideDivider(0));
+ assertFalse(itemAdapter.shouldHideDivider(1));
+ assertTrue(itemAdapter.shouldHideDivider(2));
+ assertTrue(itemAdapter.shouldHideDivider(3));
+ }
+
+ private class TestDividerVisibilityManager implements PagedListView.DividerVisibilityManager {
+ @Override
+ public boolean shouldHideDivider(int position) {
+ // Hide divider after items at even positions, show after items at odd positions.
+ return position % 2 == 0;
+ }
+ }
+
+ private static String itemText(int index) {
+ return "Data " + index;
+ }
+
+ /** A base adapter that will handle inflating the test view and binding data to it. */
+ private class TestAdapter extends RecyclerView.Adapter<TestViewHolder> {
+ private List<String> mData;
+ private int mParentHeight;
+
+ TestAdapter(int itemCount, int parentHeight) {
+ mData = new ArrayList<>();
+ for (int i = 0; i < itemCount; i++) {
+ mData.add(itemText(i));
+ }
+ mParentHeight = parentHeight;
+ }
+
+ @Override
+ public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ return new TestViewHolder(inflater, parent);
+ }
+
+ @Override
+ public void onBindViewHolder(TestViewHolder holder, int position) {
+ // Calculate height for an item so one page fits ITEMS_PER_PAGE items.
+ int height = (int) Math.floor(mParentHeight / ITEMS_PER_PAGE);
+ holder.itemView.setMinimumHeight(height);
+ holder.bind(mData.get(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return mData.size();
+ }
+ }
+
+ private class TestViewHolder extends RecyclerView.ViewHolder {
+ private TextView mTextView;
+
+ TestViewHolder(LayoutInflater inflater, ViewGroup parent) {
+ super(inflater.inflate(R.layout.paged_list_item_column_card, parent, false));
+ mTextView = itemView.findViewById(R.id.text_view);
+ }
+
+ public void bind(String text) {
+ mTextView.setText(text);
+ }
+ }
+}
diff --git a/v13/tests/java/android/support/v13/view/DragStartHelperTestActivity.java b/car/tests/src/androidx/car/widget/DividerVisibilityManagerTestActivity.java
similarity index 62%
copy from v13/tests/java/android/support/v13/view/DragStartHelperTestActivity.java
copy to car/tests/src/androidx/car/widget/DividerVisibilityManagerTestActivity.java
index 6a3605b..30c1174 100644
--- a/v13/tests/java/android/support/v13/view/DragStartHelperTestActivity.java
+++ b/car/tests/src/androidx/car/widget/DividerVisibilityManagerTestActivity.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2017 The Android Open 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,21 @@
* limitations under the License.
*/
-package android.support.v13.view;
+package androidx.car.widget;
import android.app.Activity;
import android.os.Bundle;
-import android.support.v13.test.R;
-public class DragStartHelperTestActivity extends Activity {
+import androidx.car.test.R;
+
+/**
+ * Simple test activity for {@link PagedListView.DividerVisibilityManager} tests.
+ *
+ */
+public class DividerVisibilityManagerTestActivity extends Activity {
@Override
- protected void onCreate(Bundle savedInstanceState) {
+ public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- setContentView(R.layout.drag_source_activity);
+ setContentView(R.layout.activity_paged_list_view_with_dividers);
}
}
diff --git a/car/tests/src/androidx/car/widget/ListItemTest.java b/car/tests/src/androidx/car/widget/ListItemTest.java
index 8d2096a..fa0771b 100644
--- a/car/tests/src/androidx/car/widget/ListItemTest.java
+++ b/car/tests/src/androidx/car/widget/ListItemTest.java
@@ -23,6 +23,7 @@
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.hamcrest.number.IsCloseTo.closeTo;
@@ -180,6 +181,28 @@
}
@Test
+ public void testSwitchVisibleAndCheckedState() {
+ List<ListItem> items = Arrays.asList(
+ new ListItem.Builder(mActivity)
+ .withSwitch(true, true, null)
+ .build(),
+ new ListItem.Builder(mActivity)
+ .withSwitch(false, true, null)
+ .build());
+ setupPagedListView(items);
+
+ ListItemAdapter.ViewHolder viewHolder = getViewHolderAtPosition(0);
+ assertThat(viewHolder.getSwitch().getVisibility(), is(equalTo(View.VISIBLE)));
+ assertThat(viewHolder.getSwitch().isChecked(), is(equalTo(true)));
+ assertThat(viewHolder.getSwitchDivider().getVisibility(), is(equalTo(View.VISIBLE)));
+
+ viewHolder = getViewHolderAtPosition(1);
+ assertThat(viewHolder.getSwitch().getVisibility(), is(equalTo(View.VISIBLE)));
+ assertThat(viewHolder.getSwitch().isChecked(), is(equalTo(false)));
+ assertThat(viewHolder.getSwitchDivider().getVisibility(), is(equalTo(View.VISIBLE)));
+ }
+
+ @Test
public void testDividersAreOptional() {
List<ListItem> items = Arrays.asList(
new ListItem.Builder(mActivity)
@@ -191,11 +214,12 @@
new ListItem.Builder(mActivity)
.withActions("text", false, v -> { /* Do nothing. */ },
"text", false, v -> { /* Do nothing. */ })
+ .build(),
+ new ListItem.Builder(mActivity)
+ .withSwitch(true, false, null)
.build());
setupPagedListView(items);
- setupPagedListView(items);
-
ListItemAdapter.ViewHolder viewHolder = getViewHolderAtPosition(0);
assertThat(viewHolder.getSupplementalIcon().getVisibility(), is(equalTo(View.VISIBLE)));
assertThat(viewHolder.getSupplementalIconDivider().getVisibility(),
@@ -210,6 +234,30 @@
assertThat(viewHolder.getAction1Divider().getVisibility(), is(equalTo(View.GONE)));
assertThat(viewHolder.getAction2().getVisibility(), is(equalTo(View.VISIBLE)));
assertThat(viewHolder.getAction2Divider().getVisibility(), is(equalTo(View.GONE)));
+
+ viewHolder = getViewHolderAtPosition(3);
+ assertThat(viewHolder.getSwitch().getVisibility(), is(equalTo(View.VISIBLE)));
+ assertThat(viewHolder.getSwitchDivider().getVisibility(), is(equalTo(View.GONE)));
+ }
+
+ @Test
+ public void testCanHideItemDividers() {
+ List<ListItem> items = Arrays.asList(
+ new ListItem.Builder(mActivity)
+ .withDividerHidden()
+ .build(),
+ new ListItem.Builder(mActivity)
+ .build());
+ setupPagedListView(items);
+
+ assertThat(items.get(0).shouldHideDivider(), is(true));
+ assertThat(items.get(1).shouldHideDivider(), is(false));
+
+ PagedListView.DividerVisibilityManager dvm = (PagedListView.DividerVisibilityManager)
+ mPagedListView.getAdapter();
+ assertThat(dvm, is(notNullValue()));
+ assertThat(dvm.shouldHideDivider(0), is(true));
+ assertThat(dvm.shouldHideDivider(1), is(false));
}
@Test
@@ -366,6 +414,49 @@
}
@Test
+ public void testSmallPrimaryIconTopMarginRemainsTheSameRegardlessOfTextLength() {
+ final String longText = InstrumentationRegistry.getContext().getResources().getString(
+ R.string.over_120_chars);
+ List<ListItem> items = Arrays.asList(
+ // Single line item.
+ new ListItem.Builder(mActivity)
+ .withPrimaryActionIcon(android.R.drawable.sym_def_app_icon, false)
+ .withTitle("one line text")
+ .build(),
+ // Double line item with one line text.
+ new ListItem.Builder(mActivity)
+ .withPrimaryActionIcon(android.R.drawable.sym_def_app_icon, false)
+ .withTitle("one line text")
+ .withBody("one line text")
+ .build(),
+ // Double line item with long text.
+ new ListItem.Builder(mActivity)
+ .withPrimaryActionIcon(android.R.drawable.sym_def_app_icon, false)
+ .withTitle("one line text")
+ .withBody(longText)
+ .build(),
+ // Body text only - long text.
+ new ListItem.Builder(mActivity)
+ .withPrimaryActionIcon(android.R.drawable.sym_def_app_icon, false)
+ .withBody(longText)
+ .build(),
+ // Body text only - one line text.
+ new ListItem.Builder(mActivity)
+ .withPrimaryActionIcon(android.R.drawable.sym_def_app_icon, false)
+ .withBody("one line text")
+ .build());
+ setupPagedListView(items);
+
+ for (int i = 1; i < items.size(); i++) {
+ onView(withId(R.id.recycler_view)).perform(scrollToPosition(i));
+ // Implementation uses integer division so it may be off by 1 vs centered vertically.
+ assertThat((double) getViewHolderAtPosition(i - 1).getPrimaryIcon().getTop(),
+ is(closeTo(
+ (double) getViewHolderAtPosition(i).getPrimaryIcon().getTop(), 1.0d)));
+ }
+ }
+
+ @Test
public void testClickingPrimaryActionIsSeparateFromSupplementalAction() {
final boolean[] clicked = {false, false};
List<ListItem> items = Arrays.asList(
@@ -413,6 +504,35 @@
}
@Test
+ public void testCheckingSwitch() {
+ final boolean[] clicked = {false, false};
+ List<ListItem> items = Arrays.asList(
+ new ListItem.Builder(mActivity)
+ .withSwitch(false, false, (button, isChecked) -> {
+ // Initial value is false.
+ assertTrue(isChecked);
+ clicked[0] = true;
+ })
+ .build(),
+ new ListItem.Builder(mActivity)
+ .withSwitch(true, false, (button, isChecked) -> {
+ // Initial value is true.
+ assertFalse(isChecked);
+ clicked[1] = true;
+ })
+ .build());
+ setupPagedListView(items);
+
+ onView(withId(R.id.recycler_view)).perform(
+ actionOnItemAtPosition(0, clickChildViewWithId(R.id.switch_widget)));
+ assertTrue(clicked[0]);
+
+ onView(withId(R.id.recycler_view)).perform(
+ actionOnItemAtPosition(1, clickChildViewWithId(R.id.switch_widget)));
+ assertTrue(clicked[1]);
+ }
+
+ @Test
public void testClickingSupplementalAction() {
final boolean[] clicked = {false};
List<ListItem> items = Arrays.asList(
diff --git a/compat/api/current.txt b/compat/api/current.txt
index 66525e2..04aebf6 100644
--- a/compat/api/current.txt
+++ b/compat/api/current.txt
@@ -1,3 +1,58 @@
+package android.support.v13.view {
+
+ public final class DragAndDropPermissionsCompat {
+ method public void release();
+ }
+
+ public class DragStartHelper {
+ ctor public DragStartHelper(android.view.View, android.support.v13.view.DragStartHelper.OnDragStartListener);
+ method public void attach();
+ method public void detach();
+ method public void getTouchPosition(android.graphics.Point);
+ method public boolean onLongClick(android.view.View);
+ method public boolean onTouch(android.view.View, android.view.MotionEvent);
+ }
+
+ public static abstract interface DragStartHelper.OnDragStartListener {
+ method public abstract boolean onDragStart(android.view.View, android.support.v13.view.DragStartHelper);
+ }
+
+}
+
+package android.support.v13.view.inputmethod {
+
+ public final class EditorInfoCompat {
+ ctor public EditorInfoCompat();
+ method public static java.lang.String[] getContentMimeTypes(android.view.inputmethod.EditorInfo);
+ method public static void setContentMimeTypes(android.view.inputmethod.EditorInfo, java.lang.String[]);
+ field public static final int IME_FLAG_FORCE_ASCII = -2147483648; // 0x80000000
+ field public static final int IME_FLAG_NO_PERSONALIZED_LEARNING = 16777216; // 0x1000000
+ }
+
+ public final class InputConnectionCompat {
+ ctor public InputConnectionCompat();
+ method public static boolean commitContent(android.view.inputmethod.InputConnection, android.view.inputmethod.EditorInfo, android.support.v13.view.inputmethod.InputContentInfoCompat, int, android.os.Bundle);
+ method public static android.view.inputmethod.InputConnection createWrapper(android.view.inputmethod.InputConnection, android.view.inputmethod.EditorInfo, android.support.v13.view.inputmethod.InputConnectionCompat.OnCommitContentListener);
+ field public static int INPUT_CONTENT_GRANT_READ_URI_PERMISSION;
+ }
+
+ public static abstract interface InputConnectionCompat.OnCommitContentListener {
+ method public abstract boolean onCommitContent(android.support.v13.view.inputmethod.InputContentInfoCompat, int, android.os.Bundle);
+ }
+
+ public final class InputContentInfoCompat {
+ ctor public InputContentInfoCompat(android.net.Uri, android.content.ClipDescription, android.net.Uri);
+ method public android.net.Uri getContentUri();
+ method public android.content.ClipDescription getDescription();
+ method public android.net.Uri getLinkUri();
+ method public void releasePermission();
+ method public void requestPermission();
+ method public java.lang.Object unwrap();
+ method public static android.support.v13.view.inputmethod.InputContentInfoCompat wrap(java.lang.Object);
+ }
+
+}
+
package android.support.v4.accessibilityservice {
public final class AccessibilityServiceInfoCompat {
@@ -30,6 +85,7 @@
method public static android.net.Uri getReferrer(android.app.Activity);
method public static deprecated boolean invalidateOptionsMenu(android.app.Activity);
method public static void postponeEnterTransition(android.app.Activity);
+ method public static android.support.v13.view.DragAndDropPermissionsCompat requestDragAndDropPermissions(android.app.Activity, android.view.DragEvent);
method public static void requestPermissions(android.app.Activity, java.lang.String[], int);
method public static void setEnterSharedElementCallback(android.app.Activity, android.support.v4.app.SharedElementCallback);
method public static void setExitSharedElementCallback(android.app.Activity, android.support.v4.app.SharedElementCallback);
@@ -1057,6 +1113,7 @@
ctor public ArraySet();
ctor public ArraySet(int);
ctor public ArraySet(android.support.v4.util.ArraySet<E>);
+ ctor public ArraySet(java.util.Collection<E>);
method public boolean add(E);
method public void addAll(android.support.v4.util.ArraySet<? extends E>);
method public boolean addAll(java.util.Collection<? extends E>);
@@ -1167,6 +1224,8 @@
public class ObjectsCompat {
method public static boolean equals(java.lang.Object, java.lang.Object);
+ method public static int hash(java.lang.Object...);
+ method public static int hashCode(java.lang.Object);
}
public class Pair<F, S> {
@@ -1730,7 +1789,7 @@
field public static final int TYPE_TOUCH = 0; // 0x0
}
- public final deprecated class ViewConfigurationCompat {
+ public final class ViewConfigurationCompat {
method public static float getScaledHorizontalScrollFactor(android.view.ViewConfiguration, android.content.Context);
method public static deprecated int getScaledPagingTouchSlop(android.view.ViewConfiguration);
method public static float getScaledVerticalScrollFactor(android.view.ViewConfiguration, android.content.Context);
@@ -2338,6 +2397,7 @@
method public static void setCompoundDrawablesRelative(android.widget.TextView, android.graphics.drawable.Drawable, android.graphics.drawable.Drawable, android.graphics.drawable.Drawable, android.graphics.drawable.Drawable);
method public static void setCompoundDrawablesRelativeWithIntrinsicBounds(android.widget.TextView, android.graphics.drawable.Drawable, android.graphics.drawable.Drawable, android.graphics.drawable.Drawable, android.graphics.drawable.Drawable);
method public static void setCompoundDrawablesRelativeWithIntrinsicBounds(android.widget.TextView, int, int, int, int);
+ method public static void setCustomSelectionActionModeCallback(android.widget.TextView, android.view.ActionMode.Callback);
method public static void setTextAppearance(android.widget.TextView, int);
field public static final int AUTO_SIZE_TEXT_TYPE_NONE = 0; // 0x0
field public static final int AUTO_SIZE_TEXT_TYPE_UNIFORM = 1; // 0x1
diff --git a/v13/java/android/support/v13/view/DragAndDropPermissionsCompat.java b/compat/src/main/java/android/support/v13/view/DragAndDropPermissionsCompat.java
similarity index 100%
rename from v13/java/android/support/v13/view/DragAndDropPermissionsCompat.java
rename to compat/src/main/java/android/support/v13/view/DragAndDropPermissionsCompat.java
diff --git a/v13/java/android/support/v13/view/DragStartHelper.java b/compat/src/main/java/android/support/v13/view/DragStartHelper.java
similarity index 100%
rename from v13/java/android/support/v13/view/DragStartHelper.java
rename to compat/src/main/java/android/support/v13/view/DragStartHelper.java
diff --git a/v13/java/android/support/v13/view/inputmethod/EditorInfoCompat.java b/compat/src/main/java/android/support/v13/view/inputmethod/EditorInfoCompat.java
similarity index 100%
rename from v13/java/android/support/v13/view/inputmethod/EditorInfoCompat.java
rename to compat/src/main/java/android/support/v13/view/inputmethod/EditorInfoCompat.java
diff --git a/v13/java/android/support/v13/view/inputmethod/InputConnectionCompat.java b/compat/src/main/java/android/support/v13/view/inputmethod/InputConnectionCompat.java
similarity index 100%
rename from v13/java/android/support/v13/view/inputmethod/InputConnectionCompat.java
rename to compat/src/main/java/android/support/v13/view/inputmethod/InputConnectionCompat.java
diff --git a/v13/java/android/support/v13/view/inputmethod/InputContentInfoCompat.java b/compat/src/main/java/android/support/v13/view/inputmethod/InputContentInfoCompat.java
similarity index 100%
rename from v13/java/android/support/v13/view/inputmethod/InputContentInfoCompat.java
rename to compat/src/main/java/android/support/v13/view/inputmethod/InputContentInfoCompat.java
diff --git a/compat/src/main/java/android/support/v4/app/ActivityCompat.java b/compat/src/main/java/android/support/v4/app/ActivityCompat.java
index 5833481..9d15be1 100644
--- a/compat/src/main/java/android/support/v4/app/ActivityCompat.java
+++ b/compat/src/main/java/android/support/v4/app/ActivityCompat.java
@@ -34,7 +34,9 @@
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.annotation.RestrictTo;
+import android.support.v13.view.DragAndDropPermissionsCompat;
import android.support.v4.content.ContextCompat;
+import android.view.DragEvent;
import android.view.View;
import java.util.List;
@@ -528,6 +530,19 @@
return false;
}
+ /**
+ * Create {@link DragAndDropPermissionsCompat} object bound to this activity and controlling
+ * the access permissions for content URIs associated with the {@link android.view.DragEvent}.
+ * @param dragEvent Drag event to request permission for
+ * @return The {@link DragAndDropPermissionsCompat} object used to control access to the content
+ * URIs. {@code null} if no content URIs are associated with the event or if permissions could
+ * not be granted.
+ */
+ public static DragAndDropPermissionsCompat requestDragAndDropPermissions(Activity activity,
+ DragEvent dragEvent) {
+ return DragAndDropPermissionsCompat.request(activity, dragEvent);
+ }
+
@RequiresApi(21)
private static class SharedElementCallback21Impl extends android.app.SharedElementCallback {
diff --git a/compat/src/main/java/android/support/v4/app/NotificationCompat.java b/compat/src/main/java/android/support/v4/app/NotificationCompat.java
index 80c7757..b3f0d32 100644
--- a/compat/src/main/java/android/support/v4/app/NotificationCompat.java
+++ b/compat/src/main/java/android/support/v4/app/NotificationCompat.java
@@ -447,6 +447,7 @@
public @interface StreamType {}
/** @hide */
+ @RestrictTo(LIBRARY_GROUP)
@Retention(SOURCE)
@IntDef({VISIBILITY_PUBLIC, VISIBILITY_PRIVATE, VISIBILITY_SECRET})
public @interface NotificationVisibility {}
diff --git a/compat/src/main/java/android/support/v4/graphics/TypefaceCompatUtil.java b/compat/src/main/java/android/support/v4/graphics/TypefaceCompatUtil.java
index b5d206c..c524f82 100644
--- a/compat/src/main/java/android/support/v4/graphics/TypefaceCompatUtil.java
+++ b/compat/src/main/java/android/support/v4/graphics/TypefaceCompatUtil.java
@@ -94,11 +94,15 @@
@RequiresApi(19)
public static ByteBuffer mmap(Context context, CancellationSignal cancellationSignal, Uri uri) {
final ContentResolver resolver = context.getContentResolver();
- try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r", cancellationSignal);
- FileInputStream fis = new FileInputStream(pfd.getFileDescriptor())) {
- FileChannel channel = fis.getChannel();
- final long size = channel.size();
- return channel.map(FileChannel.MapMode.READ_ONLY, 0, size);
+ try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r", cancellationSignal)) {
+ if (pfd == null) {
+ return null;
+ }
+ try (FileInputStream fis = new FileInputStream(pfd.getFileDescriptor())) {
+ FileChannel channel = fis.getChannel();
+ final long size = channel.size();
+ return channel.map(FileChannel.MapMode.READ_ONLY, 0, size);
+ }
} catch (IOException e) {
return null;
}
diff --git a/compat/src/main/java/android/support/v4/util/ArraySet.java b/compat/src/main/java/android/support/v4/util/ArraySet.java
index ab080fa..8444d2c 100644
--- a/compat/src/main/java/android/support/v4/util/ArraySet.java
+++ b/compat/src/main/java/android/support/v4/util/ArraySet.java
@@ -18,6 +18,8 @@
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.util.Log;
@@ -69,16 +71,15 @@
* The first entry in the array is a pointer to the next array in the
* list; the second entry is a pointer to the int[] hash code array for it.
*/
- static Object[] sBaseCache;
- static int sBaseCacheSize;
- static Object[] sTwiceBaseCache;
- static int sTwiceBaseCacheSize;
+ private static Object[] sBaseCache;
+ private static int sBaseCacheSize;
+ private static Object[] sTwiceBaseCache;
+ private static int sTwiceBaseCacheSize;
- final boolean mIdentityHashCode;
- int[] mHashes;
- Object[] mArray;
- int mSize;
- MapCollections<E, E> mCollections;
+ private int[] mHashes;
+ private Object[] mArray;
+ private int mSize;
+ private MapCollections<E, E> mCollections;
private int indexOf(Object key, int hash) {
final int N = mSize;
@@ -238,19 +239,13 @@
* will grow once items are added to it.
*/
public ArraySet() {
- this(0, false);
+ this(0);
}
/**
* Create a new ArraySet with a given initial capacity.
*/
public ArraySet(int capacity) {
- this(capacity, false);
- }
-
- /** {@hide} */
- public ArraySet(int capacity, boolean identityHashCode) {
- mIdentityHashCode = identityHashCode;
if (capacity == 0) {
mHashes = INT;
mArray = OBJECT;
@@ -263,15 +258,17 @@
/**
* Create a new ArraySet with the mappings from the given ArraySet.
*/
- public ArraySet(ArraySet<E> set) {
+ public ArraySet(@Nullable ArraySet<E> set) {
this();
if (set != null) {
addAll(set);
}
}
- /** {@hide} */
- public ArraySet(Collection<E> set) {
+ /**
+ * Create a new ArraySet with the mappings from the given {@link Collection}.
+ */
+ public ArraySet(@Nullable Collection<E> set) {
this();
if (set != null) {
addAll(set);
@@ -326,8 +323,7 @@
* @return Returns the index of the value if it exists, else a negative integer.
*/
public int indexOf(Object key) {
- return key == null ? indexOfNull()
- : indexOf(key, mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
+ return key == null ? indexOfNull() : indexOf(key, key.hashCode());
}
/**
@@ -335,6 +331,7 @@
* @param index The desired index, must be between 0 and {@link #size()}-1.
* @return Returns the value stored at the given index.
*/
+ @Nullable
public E valueAt(int index) {
return (E) mArray[index];
}
@@ -357,14 +354,14 @@
* when the class of the object is inappropriate for this set.
*/
@Override
- public boolean add(E value) {
+ public boolean add(@Nullable E value) {
final int hash;
int index;
if (value == null) {
hash = 0;
index = indexOfNull();
} else {
- hash = mIdentityHashCode ? System.identityHashCode(value) : value.hashCode();
+ hash = value.hashCode();
index = indexOf(value, hash);
}
if (index >= 0) {
@@ -413,8 +410,7 @@
@RestrictTo(LIBRARY_GROUP)
public void append(E value) {
final int index = mSize;
- final int hash = value == null ? 0
- : (mIdentityHashCode ? System.identityHashCode(value) : value.hashCode());
+ final int hash = value == null ? 0 : value.hashCode();
if (index >= mHashes.length) {
throw new IllegalStateException("Array is full");
}
@@ -439,7 +435,7 @@
* Perform a {@link #add(Object)} of all values in <var>array</var>
* @param array The array whose contents are to be retrieved.
*/
- public void addAll(ArraySet<? extends E> array) {
+ public void addAll(@NonNull ArraySet<? extends E> array) {
final int N = array.mSize;
ensureCapacity(mSize + N);
if (mSize == 0) {
@@ -555,6 +551,7 @@
return mSize;
}
+ @NonNull
@Override
public Object[] toArray() {
Object[] result = new Object[mSize];
@@ -562,8 +559,9 @@
return result;
}
+ @NonNull
@Override
- public <T> T[] toArray(T[] array) {
+ public <T> T[] toArray(@NonNull T[] array) {
if (array.length < mSize) {
@SuppressWarnings("unchecked") T[] newArray =
(T[]) Array.newInstance(array.getClass().getComponentType(), mSize);
@@ -732,7 +730,7 @@
* in <var>collection</var>, else returns false.
*/
@Override
- public boolean containsAll(Collection<?> collection) {
+ public boolean containsAll(@NonNull Collection<?> collection) {
Iterator<?> it = collection.iterator();
while (it.hasNext()) {
if (!contains(it.next())) {
@@ -747,7 +745,7 @@
* @param collection The collection whose contents are to be retrieved.
*/
@Override
- public boolean addAll(Collection<? extends E> collection) {
+ public boolean addAll(@NonNull Collection<? extends E> collection) {
ensureCapacity(mSize + collection.size());
boolean added = false;
for (E value : collection) {
@@ -762,7 +760,7 @@
* @return Returns true if any values were removed from the array set, else false.
*/
@Override
- public boolean removeAll(Collection<?> collection) {
+ public boolean removeAll(@NonNull Collection<?> collection) {
boolean removed = false;
for (Object value : collection) {
removed |= remove(value);
@@ -777,7 +775,7 @@
* @return Returns true if any values were removed from the array set, else false.
*/
@Override
- public boolean retainAll(Collection<?> collection) {
+ public boolean retainAll(@NonNull Collection<?> collection) {
boolean removed = false;
for (int i = mSize - 1; i >= 0; i--) {
if (!collection.contains(mArray[i])) {
diff --git a/compat/src/main/java/android/support/v4/util/ObjectsCompat.java b/compat/src/main/java/android/support/v4/util/ObjectsCompat.java
index b6c740e..747cfb4 100644
--- a/compat/src/main/java/android/support/v4/util/ObjectsCompat.java
+++ b/compat/src/main/java/android/support/v4/util/ObjectsCompat.java
@@ -18,6 +18,7 @@
import android.os.Build;
import android.support.annotation.Nullable;
+import java.util.Arrays;
import java.util.Objects;
/**
@@ -51,4 +52,46 @@
return (a == b) || (a != null && a.equals(b));
}
}
+
+ /**
+ * Returns the hash code of a non-{@code null} argument and 0 for a {@code null} argument.
+ *
+ * @param o an object
+ * @return the hash code of a non-{@code null} argument and 0 for a {@code null} argument
+ * @see Object#hashCode
+ */
+ public static int hashCode(@Nullable Object o) {
+ return o != null ? o.hashCode() : 0;
+ }
+
+ /**
+ * Generates a hash code for a sequence of input values. The hash code is generated as if all
+ * the input values were placed into an array, and that array were hashed by calling
+ * {@link Arrays#hashCode(Object[])}.
+ *
+ * <p>This method is useful for implementing {@link Object#hashCode()} on objects containing
+ * multiple fields. For example, if an object that has three fields, {@code x}, {@code y}, and
+ * {@code z}, one could write:
+ *
+ * <blockquote><pre>
+ * @Override public int hashCode() {
+ * return ObjectsCompat.hash(x, y, z);
+ * }
+ * </pre></blockquote>
+ *
+ * <b>Warning: When a single object reference is supplied, the returned value does not equal the
+ * hash code of that object reference.</b> This value can be computed by calling
+ * {@link #hashCode(Object)}.
+ *
+ * @param values the values to be hashed
+ * @return a hash value of the sequence of input values
+ * @see Arrays#hashCode(Object[])
+ */
+ public static int hash(@Nullable Object... values) {
+ if (Build.VERSION.SDK_INT >= 19) {
+ return Objects.hash(values);
+ } else {
+ return Arrays.hashCode(values);
+ }
+ }
}
diff --git a/compat/src/main/java/android/support/v4/view/ViewConfigurationCompat.java b/compat/src/main/java/android/support/v4/view/ViewConfigurationCompat.java
index f14b806..60d37a9 100644
--- a/compat/src/main/java/android/support/v4/view/ViewConfigurationCompat.java
+++ b/compat/src/main/java/android/support/v4/view/ViewConfigurationCompat.java
@@ -27,10 +27,7 @@
/**
* Helper for accessing features in {@link ViewConfiguration}.
- *
- * @deprecated Use {@link ViewConfiguration} directly.
*/
-@Deprecated
public final class ViewConfigurationCompat {
private static final String TAG = "ViewConfigCompat";
diff --git a/compat/src/main/java/android/support/v4/widget/TextViewCompat.java b/compat/src/main/java/android/support/v4/widget/TextViewCompat.java
index dc87a38..8789815 100644
--- a/compat/src/main/java/android/support/v4/widget/TextViewCompat.java
+++ b/compat/src/main/java/android/support/v4/widget/TextViewCompat.java
@@ -18,6 +18,11 @@
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.DrawableRes;
@@ -28,14 +33,22 @@
import android.support.annotation.RestrictTo;
import android.support.annotation.StyleRes;
import android.support.v4.os.BuildCompat;
+import android.text.Editable;
import android.util.Log;
import android.util.TypedValue;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
/**
* Helper for accessing features in {@link TextView}.
@@ -219,6 +232,11 @@
}
return new int[0];
}
+
+ public void setCustomSelectionActionModeCallback(TextView textView,
+ ActionMode.Callback callback) {
+ textView.setCustomSelectionActionModeCallback(callback);
+ }
}
@RequiresApi(16)
@@ -314,8 +332,160 @@
}
}
+ @RequiresApi(26)
+ static class TextViewCompatApi26Impl extends TextViewCompatApi23Impl {
+ @Override
+ public void setCustomSelectionActionModeCallback(final TextView textView,
+ final ActionMode.Callback callback) {
+ if (Build.VERSION.SDK_INT != Build.VERSION_CODES.O
+ && Build.VERSION.SDK_INT != Build.VERSION_CODES.O_MR1) {
+ super.setCustomSelectionActionModeCallback(textView, callback);
+ return;
+ }
+
+
+ // A bug in O and O_MR1 causes a number of options for handling the ACTION_PROCESS_TEXT
+ // intent after selection to not be displayed in the menu, although they should be.
+ // Here we fix this, by removing the menu items created by the framework code, and
+ // adding them (and the missing ones) back correctly.
+ textView.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
+ // This constant should be correlated with its definition in the
+ // android.widget.Editor class.
+ private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100;
+
+ // References to the MenuBuilder class and its removeItemAt(int) method.
+ // Since in most cases the menu instance processed by this callback is going
+ // to be a MenuBuilder, we keep these references to avoid querying for them
+ // frequently by reflection in recomputeProcessTextMenuItems.
+ private Class mMenuBuilderClass;
+ private Method mMenuBuilderRemoveItemAtMethod;
+ private boolean mCanUseMenuBuilderReferences;
+ private boolean mInitializedMenuBuilderReferences = false;
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ return callback.onCreateActionMode(mode, menu);
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ recomputeProcessTextMenuItems(menu);
+ return callback.onPrepareActionMode(mode, menu);
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ return callback.onActionItemClicked(mode, item);
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ callback.onDestroyActionMode(mode);
+ }
+
+ private void recomputeProcessTextMenuItems(final Menu menu) {
+ final Context context = textView.getContext();
+ final PackageManager packageManager = context.getPackageManager();
+
+ if (!mInitializedMenuBuilderReferences) {
+ mInitializedMenuBuilderReferences = true;
+ try {
+ mMenuBuilderClass =
+ Class.forName("com.android.internal.view.menu.MenuBuilder");
+ mMenuBuilderRemoveItemAtMethod = mMenuBuilderClass
+ .getDeclaredMethod("removeItemAt", Integer.TYPE);
+ mCanUseMenuBuilderReferences = true;
+ } catch (ClassNotFoundException | NoSuchMethodException e) {
+ mMenuBuilderClass = null;
+ mMenuBuilderRemoveItemAtMethod = null;
+ mCanUseMenuBuilderReferences = false;
+ }
+ }
+ // Remove the menu items created for ACTION_PROCESS_TEXT handlers.
+ try {
+ final Method removeItemAtMethod =
+ (mCanUseMenuBuilderReferences && mMenuBuilderClass.isInstance(menu))
+ ? mMenuBuilderRemoveItemAtMethod
+ : menu.getClass()
+ .getDeclaredMethod("removeItemAt", Integer.TYPE);
+ for (int i = menu.size() - 1; i >= 0; --i) {
+ final MenuItem item = menu.getItem(i);
+ if (item.getIntent() != null && Intent.ACTION_PROCESS_TEXT
+ .equals(item.getIntent().getAction())) {
+ removeItemAtMethod.invoke(menu, i);
+ }
+ }
+ } catch (NoSuchMethodException | IllegalAccessException
+ | InvocationTargetException e) {
+ // There is a menu custom implementation used which is not providing
+ // a removeItemAt(int) menu. There is nothing we can do in this case.
+ return;
+ }
+
+ // Populate the menu again with the ACTION_PROCESS_TEXT handlers.
+ final List<ResolveInfo> supportedActivities =
+ getSupportedActivities(context, packageManager);
+ for (int i = 0; i < supportedActivities.size(); ++i) {
+ final ResolveInfo info = supportedActivities.get(i);
+ menu.add(Menu.NONE, Menu.NONE,
+ MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i,
+ info.loadLabel(packageManager))
+ .setIntent(createProcessTextIntentForResolveInfo(info, textView))
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ }
+ }
+
+ private List<ResolveInfo> getSupportedActivities(final Context context,
+ final PackageManager packageManager) {
+ final List<ResolveInfo> supportedActivities = new ArrayList<>();
+ boolean canStartActivityForResult = context instanceof Activity;
+ if (!canStartActivityForResult) {
+ return supportedActivities;
+ }
+ final List<ResolveInfo> unfiltered =
+ packageManager.queryIntentActivities(createProcessTextIntent(), 0);
+ for (ResolveInfo info : unfiltered) {
+ if (isSupportedActivity(info, context)) {
+ supportedActivities.add(info);
+ }
+ }
+ return supportedActivities;
+ }
+
+ private boolean isSupportedActivity(final ResolveInfo info, final Context context) {
+ if (context.getPackageName().equals(info.activityInfo.packageName)) {
+ return true;
+ }
+ if (!info.activityInfo.exported) {
+ return false;
+ }
+ return info.activityInfo.permission == null
+ || context.checkSelfPermission(info.activityInfo.permission)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ private Intent createProcessTextIntentForResolveInfo(final ResolveInfo info,
+ final TextView textView) {
+ return createProcessTextIntent()
+ .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !isEditable(textView))
+ .setClassName(info.activityInfo.packageName, info.activityInfo.name);
+ }
+
+ private boolean isEditable(final TextView textView) {
+ return textView instanceof Editable
+ && textView.onCheckIsTextEditor()
+ && textView.isEnabled();
+ }
+
+ private Intent createProcessTextIntent() {
+ return new Intent().setAction(Intent.ACTION_PROCESS_TEXT).setType("text/plain");
+ }
+ });
+ }
+ }
+
@RequiresApi(27)
- static class TextViewCompatApi27Impl extends TextViewCompatApi23Impl {
+ static class TextViewCompatApi27Impl extends TextViewCompatApi26Impl {
@Override
public void setAutoSizeTextTypeWithDefaults(TextView textView, int autoSizeTextType) {
textView.setAutoSizeTextTypeWithDefaults(autoSizeTextType);
@@ -369,6 +539,8 @@
static {
if (BuildCompat.isAtLeastOMR1()) {
IMPL = new TextViewCompatApi27Impl();
+ } else if (Build.VERSION.SDK_INT >= 26) {
+ IMPL = new TextViewCompatApi26Impl();
} else if (Build.VERSION.SDK_INT >= 23) {
IMPL = new TextViewCompatApi23Impl();
} else if (Build.VERSION.SDK_INT >= 18) {
@@ -600,4 +772,31 @@
public static int[] getAutoSizeTextAvailableSizes(@NonNull TextView textView) {
return IMPL.getAutoSizeTextAvailableSizes(textView);
}
+
+ /**
+ * Sets a selection action mode callback on a TextView.
+ *
+ * Also this method can be used to fix a bug in framework SDK 26. On these affected devices,
+ * the bug causes the menu containing the options for handling ACTION_PROCESS_TEXT after text
+ * selection to miss a number of items. This method can be used to fix this wrong behaviour for
+ * a text view, by passing any custom callback implementation. If no custom callback is desired,
+ * a no-op implementation should be provided.
+ *
+ * Note that, by default, the bug will only be fixed when the default floating toolbar menu
+ * implementation is used. If a custom implementation of {@link Menu} is provided, this should
+ * provide the method Menu#removeItemAt(int) which removes a menu item by its position,
+ * as given by Menu#getItem(int). Also, the following post condition should hold: a call
+ * to removeItemAt(i), should not modify the results of getItem(j) for any j < i. Intuitively,
+ * removing an element from the menu should behave as removing an element from a list.
+ * Note that this method does not exist in the {@link Menu} interface. However, it is required,
+ * and going to be called by reflection, in order to display the correct process text items in
+ * the menu.
+ *
+ * @param textView The TextView to set the action selection mode callback on.
+ * @param callback The action selection mode callback to set on textView.
+ */
+ public static void setCustomSelectionActionModeCallback(@NonNull TextView textView,
+ @NonNull ActionMode.Callback callback) {
+ IMPL.setCustomSelectionActionModeCallback(textView, callback);
+ }
}
diff --git a/compat/tests/AndroidManifest.xml b/compat/tests/AndroidManifest.xml
index ed6727f..8f78188 100644
--- a/compat/tests/AndroidManifest.xml
+++ b/compat/tests/AndroidManifest.xml
@@ -40,6 +40,8 @@
<activity android:name="android.support.v4.app.TestSupportActivity"
android:icon="@drawable/test_drawable_blue"/>
+ <activity android:name="android.support.v13.view.DragStartHelperTestActivity"/>
+
<provider android:name="android.support.v4.provider.MockFontProvider"
android:authorities="android.support.provider.fonts.font"
android:exported="false"
diff --git a/v13/tests/java/android/support/v13/view/DragStartHelperTest.java b/compat/tests/java/android/support/v13/view/DragStartHelperTest.java
similarity index 99%
rename from v13/tests/java/android/support/v13/view/DragStartHelperTest.java
rename to compat/tests/java/android/support/v13/view/DragStartHelperTest.java
index 993a4e5..67dda14 100644
--- a/v13/tests/java/android/support/v13/view/DragStartHelperTest.java
+++ b/compat/tests/java/android/support/v13/view/DragStartHelperTest.java
@@ -38,7 +38,7 @@
import android.support.test.filters.SmallTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
-import android.support.v13.test.R;
+import android.support.compat.test.R;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.View;
diff --git a/v13/tests/java/android/support/v13/view/DragStartHelperTestActivity.java b/compat/tests/java/android/support/v13/view/DragStartHelperTestActivity.java
similarity index 95%
rename from v13/tests/java/android/support/v13/view/DragStartHelperTestActivity.java
rename to compat/tests/java/android/support/v13/view/DragStartHelperTestActivity.java
index 6a3605b..40da559 100644
--- a/v13/tests/java/android/support/v13/view/DragStartHelperTestActivity.java
+++ b/compat/tests/java/android/support/v13/view/DragStartHelperTestActivity.java
@@ -18,7 +18,7 @@
import android.app.Activity;
import android.os.Bundle;
-import android.support.v13.test.R;
+import android.support.compat.test.R;
public class DragStartHelperTestActivity extends Activity {
@Override
diff --git a/compat/tests/java/android/support/v4/app/ActivityCompatTest.java b/compat/tests/java/android/support/v4/app/ActivityCompatTest.java
index 35889fb..3b4f1b5 100644
--- a/compat/tests/java/android/support/v4/app/ActivityCompatTest.java
+++ b/compat/tests/java/android/support/v4/app/ActivityCompatTest.java
@@ -25,7 +25,6 @@
import android.Manifest;
import android.app.Activity;
-import android.support.test.filters.SdkSuppress;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.support.v4.BaseInstrumentationTestCase;
@@ -41,7 +40,6 @@
super(TestSupportActivity.class);
}
- @SdkSuppress(minSdkVersion = 24)
@SmallTest
@Test
public void testPermissionDelegate() {
diff --git a/compat/tests/java/android/support/v4/graphics/TypefaceCompatUtilTest.java b/compat/tests/java/android/support/v4/graphics/TypefaceCompatUtilTest.java
new file mode 100644
index 0000000..47e6130
--- /dev/null
+++ b/compat/tests/java/android/support/v4/graphics/TypefaceCompatUtilTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.graphics;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.v4.provider.MockFontProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@SmallTest
+public class TypefaceCompatUtilTest {
+
+ public Context mContext;
+
+ @Before
+ public void setUp() {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ }
+
+ @Test
+ @RequiresApi(19)
+ public void testMmapNullPfd() {
+ if (Build.VERSION.SDK_INT < 19) {
+ // The API tested here requires SDK level 19.
+ return;
+ }
+ final Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(MockFontProvider.AUTHORITY).build();
+ final Uri fileUri = ContentUris.withAppendedId(uri, MockFontProvider.INVALID_FONT_FILE_ID);
+ // Should not crash.
+ TypefaceCompatUtil.mmap(mContext, null, fileUri);
+ }
+}
diff --git a/compat/tests/java/android/support/v4/provider/MockFontProvider.java b/compat/tests/java/android/support/v4/provider/MockFontProvider.java
index f584d68..04759d0 100644
--- a/compat/tests/java/android/support/v4/provider/MockFontProvider.java
+++ b/compat/tests/java/android/support/v4/provider/MockFontProvider.java
@@ -40,10 +40,12 @@
* Provides a test Content Provider implementing {@link FontsContractCompat}.
*/
public class MockFontProvider extends ContentProvider {
+ public static final String AUTHORITY = "android.support.provider.fonts.font";
+
static final String[] FONT_FILES = {
"samplefont.ttf", "large_a.ttf", "large_b.ttf", "large_c.ttf", "large_d.ttf"
};
- private static final int INVALID_FONT_FILE_ID = -1;
+ public static final int INVALID_FONT_FILE_ID = -1;
private static final int SAMPLE_FONT_FILE_0_ID = 0;
private static final int LARGE_A_FILE_ID = 1;
private static final int LARGE_B_FILE_ID = 2;
diff --git a/compat/tests/java/android/support/v4/util/ObjectsCompatTest.java b/compat/tests/java/android/support/v4/util/ObjectsCompatTest.java
index b836ae0..f48f9b7 100644
--- a/compat/tests/java/android/support/v4/util/ObjectsCompatTest.java
+++ b/compat/tests/java/android/support/v4/util/ObjectsCompatTest.java
@@ -16,6 +16,7 @@
package android.support.v4.util;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -48,4 +49,12 @@
assertTrue(ObjectsCompat.equals(a, c));
}
+ @Test
+ public void testHashCode() {
+ String a = "aaa";
+ String n = null;
+
+ assertEquals(ObjectsCompat.hashCode(a), a.hashCode());
+ assertEquals(ObjectsCompat.hashCode(n), 0);
+ }
}
diff --git a/compat/tests/java/android/support/v4/widget/TextViewCompatTest.java b/compat/tests/java/android/support/v4/widget/TextViewCompatTest.java
index 0a66e6b..cf2b52f 100644
--- a/compat/tests/java/android/support/v4/widget/TextViewCompatTest.java
+++ b/compat/tests/java/android/support/v4/widget/TextViewCompatTest.java
@@ -30,20 +30,43 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
+import android.os.Looper;
import android.support.annotation.ColorInt;
import android.support.compat.test.R;
+import android.support.test.filters.SdkSuppress;
import android.support.test.filters.SmallTest;
import android.support.v4.BaseInstrumentationTestCase;
import android.support.v4.testutils.TestUtils;
import android.support.v4.view.ViewCompat;
+import android.support.v7.view.menu.MenuBuilder;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
import android.widget.TextView;
import org.junit.Before;
import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.List;
@SmallTest
public class TextViewCompatTest extends BaseInstrumentationTestCase<TextViewTestActivity> {
@@ -411,4 +434,130 @@
assertEquals(drawableEnd, drawablesRelative[2]);
assertEquals(drawableBottom, drawablesRelative[3]);
}
+
+ @Test
+ public void testSetCustomSelectionActionModeCallback_doesNotIgnoreTheGivenCallback() {
+ // JB devices require the current thread to be prepared as a looper for this test.
+ // The test causes the creation of an Editor object, which uses an UserDictionaryListener
+ // that is handled on the main looper.
+ Looper.prepare();
+
+ final boolean[] callbackCalled = new boolean[4];
+ TextViewCompat.setCustomSelectionActionModeCallback(mTextView, new ActionMode.Callback() {
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ callbackCalled[0] = true;
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ callbackCalled[1] = true;
+ return true;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ callbackCalled[2] = true;
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ callbackCalled[3] = true;
+ }
+ });
+ final Menu menu = new MenuBuilder(mTextView.getContext());
+ final MenuItem item = menu.add("Option");
+ mTextView.getCustomSelectionActionModeCallback().onCreateActionMode(null, menu);
+ mTextView.getCustomSelectionActionModeCallback().onPrepareActionMode(null, menu);
+ mTextView.getCustomSelectionActionModeCallback().onActionItemClicked(null, item);
+ mTextView.getCustomSelectionActionModeCallback().onDestroyActionMode(null);
+ for (boolean called : callbackCalled) {
+ assertTrue(called);
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 26, maxSdkVersion = 27)
+ public void testSetCustomSelectionActionModeCallback_fixesBugInO() {
+ // Create mock context and package manager for the text view.
+ final PackageManager packageManagerMock = spy(mTextView.getContext().getPackageManager());
+ final Context contextMock = spy(mTextView.getContext());
+ when(contextMock.getPackageManager()).thenReturn(packageManagerMock);
+ final TextView tvMock = spy(mTextView);
+ // Set the new context on textViewMock by reflection, as TextView#getContext() is final.
+ try {
+ final Field contextField = View.class.getDeclaredField("mContext");
+ contextField.setAccessible(true);
+ contextField.set(tvMock, contextMock);
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ // We should be able to set mContext by reflection.
+ assertTrue(false);
+ }
+ // Create fake activities able to handle the ACTION_PROCESS_TEXT intent.
+ final ResolveInfo info1 = new ResolveInfo();
+ info1.activityInfo = new ActivityInfo();
+ info1.activityInfo.packageName = contextMock.getPackageName();
+ info1.activityInfo.name = "Activity 1";
+ info1.nonLocalizedLabel = "Option 3";
+ final ResolveInfo info2 = new ResolveInfo();
+ info2.activityInfo = new ActivityInfo();
+ info2.activityInfo.packageName = contextMock.getPackageName();
+ info2.activityInfo.name = "Activity 2";
+ info2.nonLocalizedLabel = "Option 4";
+ final ResolveInfo info3 = new ResolveInfo();
+ info3.activityInfo = new ActivityInfo();
+ info3.activityInfo.packageName = contextMock.getPackageName();
+ info3.activityInfo.name = "Activity 3";
+ info3.nonLocalizedLabel = "Option 5";
+ final List<ResolveInfo> infos = Arrays.asList(info1, info2, info3);
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(final InvocationOnMock invocation) throws Throwable {
+ final Intent intent = invocation.getArgument(0);
+ if (Intent.ACTION_PROCESS_TEXT.equals(intent.getAction())) {
+ return infos;
+ }
+ return invocation.callRealMethod();
+ }
+ }).when(packageManagerMock).queryIntentActivities((Intent) any(), anyInt());
+ // Set a no op callback on the mocked text view, which should fix the SDK26 bug.
+ TextViewCompat.setCustomSelectionActionModeCallback(tvMock, new ActionMode.Callback() {
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return true;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+
+ }
+ });
+ // Create a fake menu with two non process text items and two process text items.
+ final Menu menu = new MenuBuilder(tvMock.getContext());
+ menu.add(Menu.NONE, Menu.NONE, 1, "Option 1");
+ menu.add(Menu.NONE, Menu.NONE, 2, "Option 2");
+ menu.add(Menu.NONE, Menu.NONE, 100, "Option 3")
+ .setIntent(new Intent(Intent.ACTION_PROCESS_TEXT));
+ menu.add(Menu.NONE, Menu.NONE, 101, "Option 5")
+ .setIntent(new Intent(Intent.ACTION_PROCESS_TEXT));
+ // Run the callback and verify that the menu was updated. Its size should have increased
+ // with 1, as now there are 3 process text options instead of 2 to be displayed.
+ tvMock.getCustomSelectionActionModeCallback().onPrepareActionMode(null, menu);
+ assertEquals(5, menu.size());
+ for (int i = 0; i < menu.size(); ++i) {
+ assertEquals("Option " + (i + 1), menu.getItem(i).getTitle());
+ }
+ }
}
diff --git a/v13/tests/res/layout/drag_source_activity.xml b/compat/tests/res/layout/drag_source_activity.xml
similarity index 100%
rename from v13/tests/res/layout/drag_source_activity.xml
rename to compat/tests/res/layout/drag_source_activity.xml
diff --git a/core-ui/src/main/java/android/support/v4/widget/CursorAdapter.java b/core-ui/src/main/java/android/support/v4/widget/CursorAdapter.java
index e68229e..3ea6fc8 100644
--- a/core-ui/src/main/java/android/support/v4/widget/CursorAdapter.java
+++ b/core-ui/src/main/java/android/support/v4/widget/CursorAdapter.java
@@ -43,55 +43,55 @@
CursorFilter.CursorFilterClient {
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected boolean mDataValid;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected boolean mAutoRequery;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected Cursor mCursor;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected Context mContext;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected int mRowIDColumn;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected ChangeObserver mChangeObserver;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected DataSetObserver mDataSetObserver;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected CursorFilter mCursorFilter;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected FilterQueryProvider mFilterQueryProvider;
diff --git a/core-ui/src/main/java/android/support/v4/widget/SimpleCursorAdapter.java b/core-ui/src/main/java/android/support/v4/widget/SimpleCursorAdapter.java
index 291f9e1..ba3ee50 100644
--- a/core-ui/src/main/java/android/support/v4/widget/SimpleCursorAdapter.java
+++ b/core-ui/src/main/java/android/support/v4/widget/SimpleCursorAdapter.java
@@ -37,14 +37,14 @@
/**
* A list of columns containing the data to bind to the UI.
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected int[] mFrom;
/**
* A list of View ids representing the views to which the data must be bound.
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected int[] mTo;
diff --git a/customtabs/api/current.txt b/customtabs/api/current.txt
index 8494391..e172dba 100644
--- a/customtabs/api/current.txt
+++ b/customtabs/api/current.txt
@@ -155,3 +155,54 @@
}
+package androidx.browser.browseractions {
+
+ public class BrowserActionItem {
+ ctor public BrowserActionItem(java.lang.String, android.app.PendingIntent, int);
+ ctor public BrowserActionItem(java.lang.String, android.app.PendingIntent);
+ method public android.app.PendingIntent getAction();
+ method public int getIconId();
+ method public java.lang.String getTitle();
+ }
+
+ public class BrowserActionsIntent {
+ method public static java.lang.String getCreatorPackageName(android.content.Intent);
+ method public android.content.Intent getIntent();
+ method public static void launchIntent(android.content.Context, android.content.Intent);
+ method public static void openBrowserAction(android.content.Context, android.net.Uri);
+ method public static void openBrowserAction(android.content.Context, android.net.Uri, int, java.util.ArrayList<androidx.browser.browseractions.BrowserActionItem>, android.app.PendingIntent);
+ method public static java.util.List<androidx.browser.browseractions.BrowserActionItem> parseBrowserActionItems(java.util.ArrayList<android.os.Bundle>);
+ field public static final java.lang.String ACTION_BROWSER_ACTIONS_OPEN = "androidx.browser.browseractions.browser_action_open";
+ field public static final java.lang.String EXTRA_APP_ID = "androidx.browser.browseractions.APP_ID";
+ field public static final java.lang.String EXTRA_MENU_ITEMS = "androidx.browser.browseractions.extra.MENU_ITEMS";
+ field public static final java.lang.String EXTRA_SELECTED_ACTION_PENDING_INTENT = "androidx.browser.browseractions.extra.SELECTED_ACTION_PENDING_INTENT";
+ field public static final java.lang.String EXTRA_TYPE = "androidx.browser.browseractions.extra.TYPE";
+ field public static final int ITEM_COPY = 3; // 0x3
+ field public static final int ITEM_DOWNLOAD = 2; // 0x2
+ field public static final int ITEM_INVALID_ITEM = -1; // 0xffffffff
+ field public static final int ITEM_OPEN_IN_INCOGNITO = 1; // 0x1
+ field public static final int ITEM_OPEN_IN_NEW_TAB = 0; // 0x0
+ field public static final int ITEM_SHARE = 4; // 0x4
+ field public static final java.lang.String KEY_ACTION = "androidx.browser.browseractions.ACTION";
+ field public static final java.lang.String KEY_ICON_ID = "androidx.browser.browseractions.ICON_ID";
+ field public static final java.lang.String KEY_TITLE = "androidx.browser.browseractions.TITLE";
+ field public static final int MAX_CUSTOM_ITEMS = 5; // 0x5
+ field public static final int URL_TYPE_AUDIO = 3; // 0x3
+ field public static final int URL_TYPE_FILE = 4; // 0x4
+ field public static final int URL_TYPE_IMAGE = 1; // 0x1
+ field public static final int URL_TYPE_NONE = 0; // 0x0
+ field public static final int URL_TYPE_PLUGIN = 5; // 0x5
+ field public static final int URL_TYPE_VIDEO = 2; // 0x2
+ }
+
+ public static final class BrowserActionsIntent.Builder {
+ ctor public BrowserActionsIntent.Builder(android.content.Context, android.net.Uri);
+ method public androidx.browser.browseractions.BrowserActionsIntent build();
+ method public androidx.browser.browseractions.BrowserActionsIntent.Builder setCustomItems(java.util.ArrayList<androidx.browser.browseractions.BrowserActionItem>);
+ method public androidx.browser.browseractions.BrowserActionsIntent.Builder setCustomItems(androidx.browser.browseractions.BrowserActionItem...);
+ method public androidx.browser.browseractions.BrowserActionsIntent.Builder setOnItemSelectedAction(android.app.PendingIntent);
+ method public androidx.browser.browseractions.BrowserActionsIntent.Builder setUrlType(int);
+ }
+
+}
+
diff --git a/customtabs/src/main/java/android/support/customtabs/CustomTabsClient.java b/customtabs/src/main/java/android/support/customtabs/CustomTabsClient.java
index 2e955cb..371b5a1 100644
--- a/customtabs/src/main/java/android/support/customtabs/CustomTabsClient.java
+++ b/customtabs/src/main/java/android/support/customtabs/CustomTabsClient.java
@@ -45,7 +45,7 @@
private final ICustomTabsService mService;
private final ComponentName mServiceComponentName;
- /**@hide*/
+ /** @hide */
@RestrictTo(LIBRARY_GROUP)
CustomTabsClient(ICustomTabsService service, ComponentName componentName) {
mService = service;
diff --git a/customtabs/src/main/java/androidx/browser/browseractions/BrowserActionItem.java b/customtabs/src/main/java/androidx/browser/browseractions/BrowserActionItem.java
new file mode 100644
index 0000000..4bcb83e
--- /dev/null
+++ b/customtabs/src/main/java/androidx/browser/browseractions/BrowserActionItem.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.browser.browseractions;
+
+import android.app.PendingIntent;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+
+/**
+ * A wrapper class holding custom item of Browser Actions menu.
+ * The Bitmap is optional for a BrowserActionItem.
+ */
+public class BrowserActionItem {
+ private final String mTitle;
+ private final PendingIntent mAction;
+ @DrawableRes
+ private final int mIconId;
+
+ /**
+ * Constructor for BrowserActionItem with icon, string and action provided.
+ * @param title The string shown for a custom item.
+ * @param action The PendingIntent executed when a custom item is selected
+ * @param iconId The resource id of the icon shown for a custom item.
+ */
+ public BrowserActionItem(
+ @NonNull String title, @NonNull PendingIntent action, @DrawableRes int iconId) {
+ mTitle = title;
+ mAction = action;
+ mIconId = iconId;
+ }
+
+ /**
+ * Constructor for BrowserActionItem with only string and action provided.
+ * @param title The icon shown for a custom item.
+ * @param action The string shown for a custom item.
+ */
+ public BrowserActionItem(@NonNull String title, @NonNull PendingIntent action) {
+ this(title, action, 0);
+ }
+
+ /**
+ * @return The resource id of the icon.
+ */
+ public int getIconId() {
+ return mIconId;
+ }
+
+ /**
+ * @return The title of a custom item.
+ */
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * @return The action of a custom item.
+ */
+ public PendingIntent getAction() {
+ return mAction;
+ }
+}
diff --git a/customtabs/src/main/java/androidx/browser/browseractions/BrowserActionsIntent.java b/customtabs/src/main/java/androidx/browser/browseractions/BrowserActionsIntent.java
new file mode 100644
index 0000000..beb3d6c
--- /dev/null
+++ b/customtabs/src/main/java/androidx/browser/browseractions/BrowserActionsIntent.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.browser.browseractions;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
+import android.support.v4.content.ContextCompat;
+import android.text.TextUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Class holding the {@link Intent} and start bundle for a Browser Actions Activity.
+ *
+ * <p>
+ * <strong>Note:</strong> The constants below are public for the browser implementation's benefit.
+ * You are strongly encouraged to use {@link BrowserActionsIntent.Builder}.</p>
+ */
+public class BrowserActionsIntent {
+ private static final String TAG = "BrowserActions";
+ // Used to verify that an URL intent handler exists.
+ private static final String TEST_URL = "https://www.example.com";
+
+ /**
+ * Extra that specifies {@link PendingIntent} indicating which Application sends the {@link
+ * BrowserActionsIntent}.
+ */
+ public static final String EXTRA_APP_ID = "androidx.browser.browseractions.APP_ID";
+
+ /**
+ * Indicates that the user explicitly opted out of Browser Actions in the calling application.
+ */
+ public static final String ACTION_BROWSER_ACTIONS_OPEN =
+ "androidx.browser.browseractions.browser_action_open";
+
+ /**
+ * Extra resource id that specifies the icon of a custom item shown in the Browser Actions menu.
+ */
+ public static final String KEY_ICON_ID = "androidx.browser.browseractions.ICON_ID";
+
+ /**
+ * Extra string that specifies the title of a custom item shown in the Browser Actions menu.
+ */
+ public static final String KEY_TITLE = "androidx.browser.browseractions.TITLE";
+
+ /**
+ * Extra PendingIntent to be launched when a custom item is selected in the Browser Actions
+ * menu.
+ */
+ public static final String KEY_ACTION = "androidx.browser.browseractions.ACTION";
+
+ /**
+ * Extra that specifies the type of url for the Browser Actions menu.
+ */
+ public static final String EXTRA_TYPE = "androidx.browser.browseractions.extra.TYPE";
+
+ /**
+ * Extra that specifies List<Bundle> used for adding custom items to the Browser Actions menu.
+ */
+ public static final String EXTRA_MENU_ITEMS =
+ "androidx.browser.browseractions.extra.MENU_ITEMS";
+
+ /**
+ * Extra that specifies the PendingIntent to be launched when a browser specified menu item is
+ * selected. The id of the chosen item will be notified through the data of its Intent.
+ */
+ public static final String EXTRA_SELECTED_ACTION_PENDING_INTENT =
+ "androidx.browser.browseractions.extra.SELECTED_ACTION_PENDING_INTENT";
+
+ /**
+ * The maximum allowed number of custom items.
+ */
+ public static final int MAX_CUSTOM_ITEMS = 5;
+
+ /**
+ * Defines the types of url for Browser Actions menu.
+ */
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef({URL_TYPE_NONE, URL_TYPE_IMAGE, URL_TYPE_VIDEO, URL_TYPE_AUDIO, URL_TYPE_FILE,
+ URL_TYPE_PLUGIN})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BrowserActionsUrlType {}
+ public static final int URL_TYPE_NONE = 0;
+ public static final int URL_TYPE_IMAGE = 1;
+ public static final int URL_TYPE_VIDEO = 2;
+ public static final int URL_TYPE_AUDIO = 3;
+ public static final int URL_TYPE_FILE = 4;
+ public static final int URL_TYPE_PLUGIN = 5;
+
+ /**
+ * Defines the the ids of the browser specified menu items in Browser Actions.
+ * TODO(ltian): A long term solution need, since other providers might have customized menus.
+ */
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef({ITEM_INVALID_ITEM, ITEM_OPEN_IN_NEW_TAB, ITEM_OPEN_IN_INCOGNITO, ITEM_DOWNLOAD,
+ ITEM_COPY, ITEM_SHARE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BrowserActionsItemId {}
+ public static final int ITEM_INVALID_ITEM = -1;
+ public static final int ITEM_OPEN_IN_NEW_TAB = 0;
+ public static final int ITEM_OPEN_IN_INCOGNITO = 1;
+ public static final int ITEM_DOWNLOAD = 2;
+ public static final int ITEM_COPY = 3;
+ public static final int ITEM_SHARE = 4;
+
+ /**
+ * An {@link Intent} used to start the Browser Actions Activity.
+ */
+ @NonNull private final Intent mIntent;
+
+ /**
+ * Gets the Intent of {@link BrowserActionsIntent}.
+ * @return the Intent of {@link BrowserActionsIntent}.
+ */
+ @NonNull public Intent getIntent() {
+ return mIntent;
+ }
+
+ private BrowserActionsIntent(@NonNull Intent intent) {
+ this.mIntent = intent;
+ }
+
+ /**
+ * Builder class for opening a Browser Actions context menu.
+ */
+ public static final class Builder {
+ private final Intent mIntent = new Intent(BrowserActionsIntent.ACTION_BROWSER_ACTIONS_OPEN);
+ private Context mContext;
+ private Uri mUri;
+ @BrowserActionsUrlType
+ private int mType;
+ private ArrayList<Bundle> mMenuItems = null;
+ private PendingIntent mOnItemSelectedPendingIntent = null;
+
+ /**
+ * Constructs a {@link BrowserActionsIntent.Builder} object associated with default setting
+ * for a selected url.
+ * @param context The context requesting the Browser Actions context menu.
+ * @param uri The selected url for Browser Actions menu.
+ */
+ public Builder(Context context, Uri uri) {
+ mContext = context;
+ mUri = uri;
+ mType = URL_TYPE_NONE;
+ mMenuItems = new ArrayList<>();
+ }
+
+ /**
+ * Sets the type of Browser Actions context menu.
+ * @param type The type of url.
+ */
+ public Builder setUrlType(@BrowserActionsUrlType int type) {
+ mType = type;
+ return this;
+ }
+
+ /**
+ * Sets the custom items list.
+ * Only maximum MAX_CUSTOM_ITEMS custom items are allowed,
+ * otherwise throws an {@link IllegalStateException}.
+ * @param items The list of {@link BrowserActionItem} for custom items.
+ */
+ public Builder setCustomItems(ArrayList<BrowserActionItem> items) {
+ if (items.size() > MAX_CUSTOM_ITEMS) {
+ throw new IllegalStateException(
+ "Exceeded maximum toolbar item count of " + MAX_CUSTOM_ITEMS);
+ }
+ for (int i = 0; i < items.size(); i++) {
+ if (TextUtils.isEmpty(items.get(i).getTitle())
+ || items.get(i).getAction() == null) {
+ throw new IllegalArgumentException(
+ "Custom item should contain a non-empty title and non-null intent.");
+ } else {
+ mMenuItems.add(getBundleFromItem(items.get(i)));
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Sets the custom items list.
+ * Only maximum MAX_CUSTOM_ITEMS custom items are allowed,
+ * otherwise throws an {@link IllegalStateException}.
+ * @param items The varargs of {@link BrowserActionItem} for custom items.
+ */
+ public Builder setCustomItems(BrowserActionItem... items) {
+ return setCustomItems(new ArrayList<BrowserActionItem>(Arrays.asList(items)));
+ }
+
+ /**
+ * Set the PendingIntent to be launched when a a browser specified menu item is selected.
+ * @param onItemSelectedPendingIntent The PendingIntent to be launched.
+ */
+ public Builder setOnItemSelectedAction(PendingIntent onItemSelectedPendingIntent) {
+ mOnItemSelectedPendingIntent = onItemSelectedPendingIntent;
+ return this;
+ }
+
+ /**
+ * Populates a {@link Bundle} to hold a custom item for Browser Actions menu.
+ * @param item A custom item for Browser Actions menu.
+ * @return The Bundle of custom item.
+ */
+ private Bundle getBundleFromItem(BrowserActionItem item) {
+ Bundle bundle = new Bundle();
+ bundle.putString(KEY_TITLE, item.getTitle());
+ bundle.putParcelable(KEY_ACTION, item.getAction());
+ if (item.getIconId() != 0) bundle.putInt(KEY_ICON_ID, item.getIconId());
+ return bundle;
+ }
+
+ /**
+ * Combines all the options that have been set and returns a new {@link
+ * BrowserActionsIntent} object.
+ */
+ public BrowserActionsIntent build() {
+ mIntent.setData(mUri);
+ mIntent.putExtra(EXTRA_TYPE, mType);
+ mIntent.putParcelableArrayListExtra(EXTRA_MENU_ITEMS, mMenuItems);
+ PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+ mIntent.putExtra(EXTRA_APP_ID, pendingIntent);
+ if (mOnItemSelectedPendingIntent != null) {
+ mIntent.putExtra(
+ EXTRA_SELECTED_ACTION_PENDING_INTENT, mOnItemSelectedPendingIntent);
+ }
+ return new BrowserActionsIntent(mIntent);
+ }
+ }
+
+ /**
+ * Construct a BrowserActionsIntent with default settings and launch it to open a Browser
+ * Actions menu.
+ * @param context The context requesting for a Browser Actions menu.
+ * @param uri The url for Browser Actions menu.
+ */
+ public static void openBrowserAction(Context context, Uri uri) {
+ BrowserActionsIntent intent = new BrowserActionsIntent.Builder(context, uri).build();
+ launchIntent(context, intent.getIntent());
+ }
+
+ /**
+ * Construct a BrowserActionsIntent with custom settings and launch it to open a Browser Actions
+ * menu.
+ * @param context The context requesting for a Browser Actions menu.
+ * @param uri The url for Browser Actions menu.
+ * @param type The type of the url for context menu to be opened.
+ * @param items List of custom items to be added to Browser Actions menu.
+ * @param pendingIntent The PendingIntent to be launched when a browser specified menu item is
+ * selected.
+ */
+ public static void openBrowserAction(Context context, Uri uri, int type,
+ ArrayList<BrowserActionItem> items, PendingIntent pendingIntent) {
+ BrowserActionsIntent intent = new BrowserActionsIntent.Builder(context, uri)
+ .setUrlType(type)
+ .setCustomItems(items)
+ .setOnItemSelectedAction(pendingIntent)
+ .build();
+ launchIntent(context, intent.getIntent());
+ }
+
+ /**
+ * Launch an Intent to open a Browser Actions menu.
+ * It first checks if any Browser Actions provider is available to create the menu.
+ * If the default Browser supports Browser Actions, menu will be opened by the default Browser,
+ * otherwise show a intent picker.
+ * If not provider, a Browser Actions menu is opened locally from support library.
+ * @param context The context requesting for a Browser Actions menu.
+ * @param intent The {@link Intent} holds the setting for Browser Actions menu.
+ */
+ public static void launchIntent(Context context, Intent intent) {
+ List<ResolveInfo> handlers = getBrowserActionsIntentHandlers(context);
+ if (handlers == null || handlers.size() == 0) {
+ openFallbackBrowserActionsMenu(context, intent);
+ return;
+ } else if (handlers.size() == 1) {
+ intent.setPackage(handlers.get(0).activityInfo.packageName);
+ } else {
+ Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(TEST_URL));
+ PackageManager pm = context.getPackageManager();
+ ResolveInfo defaultHandler =
+ pm.resolveActivity(viewIntent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (defaultHandler != null) {
+ String defaultPackageName = defaultHandler.activityInfo.packageName;
+ for (int i = 0; i < handlers.size(); i++) {
+ if (defaultPackageName.equals(handlers.get(i).activityInfo.packageName)) {
+ intent.setPackage(defaultPackageName);
+ break;
+ }
+ }
+ }
+ }
+ ContextCompat.startActivity(context, intent, null);
+ }
+
+ /**
+ * Returns a list of Browser Actions providers available to handle the {@link
+ * BrowserActionsIntent}.
+ * @param context The context requesting for a Browser Actions menu.
+ * @return List of Browser Actions providers available to handle the intent.
+ */
+ private static List<ResolveInfo> getBrowserActionsIntentHandlers(Context context) {
+ Intent intent =
+ new Intent(BrowserActionsIntent.ACTION_BROWSER_ACTIONS_OPEN, Uri.parse(TEST_URL));
+ PackageManager pm = context.getPackageManager();
+ return pm.queryIntentActivities(intent, PackageManager.MATCH_ALL);
+ }
+
+ private static void openFallbackBrowserActionsMenu(Context context, Intent intent) {
+ Uri uri = intent.getData();
+ int type = intent.getIntExtra(EXTRA_TYPE, URL_TYPE_NONE);
+ ArrayList<Bundle> bundles = intent.getParcelableArrayListExtra(EXTRA_MENU_ITEMS);
+ List<BrowserActionItem> items = bundles != null ? parseBrowserActionItems(bundles) : null;
+ // TODO(ltian): display a fallback dialog showing all custom items from support library.
+ // http://crbug.com/789806.
+ return;
+ }
+
+ /**
+ * Gets custom item list for browser action menu.
+ * @param bundles Data for custom items from {@link BrowserActionsIntent}.
+ * @return List of {@link BrowserActionItem}
+ */
+ public static List<BrowserActionItem> parseBrowserActionItems(ArrayList<Bundle> bundles) {
+ List<BrowserActionItem> mActions = new ArrayList<>();
+ for (int i = 0; i < bundles.size(); i++) {
+ Bundle bundle = bundles.get(i);
+ String title = bundle.getString(BrowserActionsIntent.KEY_TITLE);
+ PendingIntent action = bundle.getParcelable(BrowserActionsIntent.KEY_ACTION);
+ @DrawableRes
+ int iconId = bundle.getInt(BrowserActionsIntent.KEY_ICON_ID);
+ if (TextUtils.isEmpty(title) || action == null) {
+ throw new IllegalArgumentException(
+ "Custom item should contain a non-empty title and non-null intent.");
+ } else {
+ BrowserActionItem item = new BrowserActionItem(title, action, iconId);
+ mActions.add(item);
+ }
+ }
+ return mActions;
+ }
+
+ /**
+ * Get the package name of the creator application.
+ * @param intent The {@link BrowserActionsIntent}.
+ * @return The creator package name.
+ */
+ @SuppressWarnings("deprecation")
+ public static String getCreatorPackageName(Intent intent) {
+ PendingIntent pendingIntent = intent.getParcelableExtra(BrowserActionsIntent.EXTRA_APP_ID);
+ if (pendingIntent != null) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ return pendingIntent.getCreatorPackage();
+ } else {
+ return pendingIntent.getTargetPackage();
+ }
+ }
+ return null;
+ }
+}
diff --git a/customtabs/tests/src/androidx/browser/browseractions/BrowserActionsIntentTest.java b/customtabs/tests/src/androidx/browser/browseractions/BrowserActionsIntentTest.java
new file mode 100644
index 0000000..49f3fda
--- /dev/null
+++ b/customtabs/tests/src/androidx/browser/browseractions/BrowserActionsIntentTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.browser.browseractions;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Unit tests for {@link BrowserActionsIntent}. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class BrowserActionsIntentTest {
+ private static final String TEST_URL = "http://www.example.com";
+ private static final String CUSTOM_ITEM_TITLE = "Share url";
+ private Uri mUri = Uri.parse(TEST_URL);
+ private Context mContext = InstrumentationRegistry.getTargetContext();
+
+ /**
+ * Test whether default {@link BrowserActionsIntent} is populated correctly.
+ */
+ @Test
+ public void testDefaultBrowserActionsIntent() {
+ BrowserActionsIntent browserActionsIntent =
+ new BrowserActionsIntent.Builder(mContext, mUri).build();
+ Intent intent = browserActionsIntent.getIntent();
+ assertNotNull(intent);
+
+ assertEquals(BrowserActionsIntent.ACTION_BROWSER_ACTIONS_OPEN, intent.getAction());
+ assertEquals(mUri, intent.getData());
+ assertTrue(intent.hasExtra(BrowserActionsIntent.EXTRA_TYPE));
+ assertEquals(BrowserActionsIntent.URL_TYPE_NONE,
+ intent.getIntExtra(BrowserActionsIntent.EXTRA_TYPE, 0));
+ assertTrue(intent.hasExtra(BrowserActionsIntent.EXTRA_APP_ID));
+ assertEquals(mContext.getPackageName(), BrowserActionsIntent.getCreatorPackageName(intent));
+ assertFalse(intent.hasExtra(BrowserActionsIntent.EXTRA_SELECTED_ACTION_PENDING_INTENT));
+ }
+
+ @Test
+ /**
+ * Test whether custom items are set correctly.
+ */
+ public void testCustomItem() {
+ PendingIntent action1 = createCustomItemAction(TEST_URL);
+ BrowserActionItem customItemWithoutIcon = new BrowserActionItem(CUSTOM_ITEM_TITLE, action1);
+ PendingIntent action2 = createCustomItemAction(TEST_URL);
+ BrowserActionItem customItemWithIcon =
+ new BrowserActionItem(CUSTOM_ITEM_TITLE, action2, android.R.drawable.ic_menu_share);
+ ArrayList<BrowserActionItem> customItems = new ArrayList<>();
+ customItems.add(customItemWithIcon);
+ customItems.add(customItemWithoutIcon);
+
+ BrowserActionsIntent browserActionsIntent = new BrowserActionsIntent.Builder(mContext, mUri)
+ .setCustomItems(customItems)
+ .build();
+ Intent intent = browserActionsIntent.getIntent();
+ assertTrue(intent.hasExtra(BrowserActionsIntent.EXTRA_MENU_ITEMS));
+ ArrayList<Bundle> bundles =
+ intent.getParcelableArrayListExtra(BrowserActionsIntent.EXTRA_MENU_ITEMS);
+ assertNotNull(bundles);
+ List<BrowserActionItem> items = BrowserActionsIntent.parseBrowserActionItems(bundles);
+ assertEquals(2, items.size());
+ BrowserActionItem items1 = items.get(0);
+ assertEquals(CUSTOM_ITEM_TITLE, items1.getTitle());
+ assertEquals(android.R.drawable.ic_menu_share, items1.getIconId());
+ assertEquals(action1, items1.getAction());
+ BrowserActionItem items2 = items.get(1);
+ assertEquals(CUSTOM_ITEM_TITLE, items2.getTitle());
+ assertEquals(0, items2.getIconId());
+ assertEquals(action2, items2.getAction());
+ }
+
+ private PendingIntent createCustomItemAction(String url) {
+ Context context = InstrumentationRegistry.getTargetContext();
+ Intent customIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ return PendingIntent.getActivity(context, 0, customIntent, 0);
+ }
+}
diff --git a/dynamic-animation/src/main/java/android/support/animation/AnimationHandler.java b/dynamic-animation/src/main/java/android/support/animation/AnimationHandler.java
index 6c39b23..24bc43a 100644
--- a/dynamic-animation/src/main/java/android/support/animation/AnimationHandler.java
+++ b/dynamic-animation/src/main/java/android/support/animation/AnimationHandler.java
@@ -35,8 +35,6 @@
* The handler uses the Choreographer by default for doing periodic callbacks. A custom
* AnimationFrameCallbackProvider can be set on the handler to provide timing pulse that
* may be independent of UI frame update. This could be useful in testing.
- *
- * @hide
*/
class AnimationHandler {
/**
@@ -57,7 +55,7 @@
* the new frame, so that they can update animation values as needed.
*/
class AnimationCallbackDispatcher {
- public void dispatchAnimationFrame() {
+ void dispatchAnimationFrame() {
mCurrentFrameTime = SystemClock.uptimeMillis();
AnimationHandler.this.doAnimationFrame(mCurrentFrameTime);
if (mAnimationCallbacks.size() > 0) {
@@ -72,7 +70,6 @@
/**
* Internal per-thread collections used to avoid set collisions as animations start and end
* while being processed.
- * @hide
*/
private final SimpleArrayMap<AnimationFrameCallback, Long> mDelayedCallbackStartTime =
new SimpleArrayMap<>();
@@ -249,7 +246,7 @@
* timing pulse without using Choreographer. That way we could use any arbitrary interval for
* our timing pulse in the tests.
*/
- public abstract static class AnimationFrameCallbackProvider {
+ abstract static class AnimationFrameCallbackProvider {
final AnimationCallbackDispatcher mDispatcher;
AnimationFrameCallbackProvider(AnimationCallbackDispatcher dispatcher) {
mDispatcher = dispatcher;
diff --git a/dynamic-animation/src/main/java/android/support/animation/DynamicAnimation.java b/dynamic-animation/src/main/java/android/support/animation/DynamicAnimation.java
index 8ea48b9..7cbd5bb 100644
--- a/dynamic-animation/src/main/java/android/support/animation/DynamicAnimation.java
+++ b/dynamic-animation/src/main/java/android/support/animation/DynamicAnimation.java
@@ -18,6 +18,7 @@
import android.os.Looper;
import android.support.annotation.FloatRange;
+import android.support.annotation.RestrictTo;
import android.support.v4.view.ViewCompat;
import android.util.AndroidRuntimeException;
import android.view.View;
@@ -631,6 +632,7 @@
*
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@Override
public boolean doAnimationFrame(long frameTime) {
if (mLastFrameTime == 0) {
diff --git a/dynamic-animation/src/main/java/android/support/animation/SpringForce.java b/dynamic-animation/src/main/java/android/support/animation/SpringForce.java
index 5f95aa8..dfb4c67 100644
--- a/dynamic-animation/src/main/java/android/support/animation/SpringForce.java
+++ b/dynamic-animation/src/main/java/android/support/animation/SpringForce.java
@@ -17,6 +17,7 @@
package android.support.animation;
import android.support.annotation.FloatRange;
+import android.support.annotation.RestrictTo;
/**
* Spring Force defines the characteristics of the spring being used in the animation.
@@ -210,6 +211,7 @@
/**
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@Override
public float getAcceleration(float lastDisplacement, float lastVelocity) {
@@ -224,6 +226,7 @@
/**
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@Override
public boolean isAtEquilibrium(float value, float velocity) {
if (Math.abs(velocity) < mVelocityThreshold
diff --git a/emoji/core/src/main/java/android/support/text/emoji/widget/EmojiButton.java b/emoji/core/src/main/java/android/support/text/emoji/widget/EmojiButton.java
index 752e052..2999ec8 100644
--- a/emoji/core/src/main/java/android/support/text/emoji/widget/EmojiButton.java
+++ b/emoji/core/src/main/java/android/support/text/emoji/widget/EmojiButton.java
@@ -18,8 +18,10 @@
import android.content.Context;
import android.os.Build;
import android.support.annotation.RequiresApi;
+import android.support.v4.widget.TextViewCompat;
import android.text.InputFilter;
import android.util.AttributeSet;
+import android.view.ActionMode;
import android.widget.Button;
/**
@@ -80,4 +82,13 @@
}
return mEmojiTextViewHelper;
}
+
+ /**
+ * See
+ * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
+ */
+ @Override
+ public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+ TextViewCompat.setCustomSelectionActionModeCallback(this, actionModeCallback);
+ }
}
diff --git a/emoji/core/src/main/java/android/support/text/emoji/widget/EmojiEditText.java b/emoji/core/src/main/java/android/support/text/emoji/widget/EmojiEditText.java
index e1057e8..9457f59 100644
--- a/emoji/core/src/main/java/android/support/text/emoji/widget/EmojiEditText.java
+++ b/emoji/core/src/main/java/android/support/text/emoji/widget/EmojiEditText.java
@@ -21,8 +21,10 @@
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.text.emoji.EmojiCompat;
+import android.support.v4.widget.TextViewCompat;
import android.text.method.KeyListener;
import android.util.AttributeSet;
+import android.view.ActionMode;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.EditText;
@@ -121,4 +123,13 @@
}
return mEmojiEditTextHelper;
}
+
+ /**
+ * See
+ * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
+ */
+ @Override
+ public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+ TextViewCompat.setCustomSelectionActionModeCallback(this, actionModeCallback);
+ }
}
diff --git a/emoji/core/src/main/java/android/support/text/emoji/widget/EmojiTextView.java b/emoji/core/src/main/java/android/support/text/emoji/widget/EmojiTextView.java
index 3e450dc..2f09f65 100644
--- a/emoji/core/src/main/java/android/support/text/emoji/widget/EmojiTextView.java
+++ b/emoji/core/src/main/java/android/support/text/emoji/widget/EmojiTextView.java
@@ -18,8 +18,10 @@
import android.content.Context;
import android.os.Build;
import android.support.annotation.RequiresApi;
+import android.support.v4.widget.TextViewCompat;
import android.text.InputFilter;
import android.util.AttributeSet;
+import android.view.ActionMode;
import android.widget.TextView;
/**
@@ -80,4 +82,13 @@
}
return mEmojiTextViewHelper;
}
+
+ /**
+ * See
+ * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
+ */
+ @Override
+ public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+ TextViewCompat.setCustomSelectionActionModeCallback(this, actionModeCallback);
+ }
}
diff --git a/emoji/core/src/main/java/android/support/text/emoji/widget/ExtractButtonCompat.java b/emoji/core/src/main/java/android/support/text/emoji/widget/ExtractButtonCompat.java
index fc8eb78..55be022 100644
--- a/emoji/core/src/main/java/android/support/text/emoji/widget/ExtractButtonCompat.java
+++ b/emoji/core/src/main/java/android/support/text/emoji/widget/ExtractButtonCompat.java
@@ -22,7 +22,9 @@
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.support.annotation.RestrictTo;
+import android.support.v4.widget.TextViewCompat;
import android.util.AttributeSet;
+import android.view.ActionMode;
import android.widget.Button;
/**
@@ -58,4 +60,13 @@
public boolean hasWindowFocus() {
return isEnabled() && getVisibility() == VISIBLE ? true : false;
}
+
+ /**
+ * See
+ * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
+ */
+ @Override
+ public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+ TextViewCompat.setCustomSelectionActionModeCallback(this, actionModeCallback);
+ }
}
diff --git a/exifinterface/src/main/java/android/support/media/ExifInterface.java b/exifinterface/src/main/java/android/support/media/ExifInterface.java
index eea69ab..6b437a6 100644
--- a/exifinterface/src/main/java/android/support/media/ExifInterface.java
+++ b/exifinterface/src/main/java/android/support/media/ExifInterface.java
@@ -23,6 +23,7 @@
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
import android.util.Log;
import android.util.Pair;
@@ -3550,6 +3551,7 @@
// Indices of Exif Ifd tag groups
/** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({IFD_TYPE_PRIMARY, IFD_TYPE_EXIF, IFD_TYPE_GPS, IFD_TYPE_INTEROPERABILITY,
IFD_TYPE_THUMBNAIL, IFD_TYPE_PREVIEW, IFD_TYPE_ORF_MAKER_NOTE,
@@ -4567,6 +4569,7 @@
* @param timeStamp number of milliseconds since Jan. 1, 1970, midnight local time.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public void setDateTime(long timeStamp) {
long sub = timeStamp % 1000;
setAttribute(TAG_DATETIME, sFormatter.format(new Date(timeStamp)));
@@ -4578,6 +4581,7 @@
* Returns -1 if the date time information if not available.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public long getDateTime() {
String dateTimeString = getAttribute(TAG_DATETIME);
if (dateTimeString == null
@@ -4614,6 +4618,7 @@
* Returns -1 if the date time information if not available.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public long getGpsDateTime() {
String date = getAttribute(TAG_GPS_DATESTAMP);
String time = getAttribute(TAG_GPS_TIMESTAMP);
diff --git a/leanback/common/android/support/v17/leanback/transition/TransitionEpicenterCallback.java b/leanback/common/android/support/v17/leanback/transition/TransitionEpicenterCallback.java
index ec7f84c..bb8e686 100644
--- a/leanback/common/android/support/v17/leanback/transition/TransitionEpicenterCallback.java
+++ b/leanback/common/android/support/v17/leanback/transition/TransitionEpicenterCallback.java
@@ -14,11 +14,13 @@
package android.support.v17.leanback.transition;
import android.graphics.Rect;
+import android.support.annotation.RestrictTo;
/**
* Class to get the epicenter of Transition.
* @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public abstract class TransitionEpicenterCallback {
/**
@@ -31,4 +33,3 @@
*/
public abstract Rect onGetEpicenter(Object transition);
}
-
diff --git a/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java b/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
index e2e6be4..dc59e0e 100644
--- a/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
+++ b/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
@@ -29,6 +29,7 @@
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
import android.support.v17.leanback.R;
import android.support.v17.leanback.animation.LogAccelerateInterpolator;
import android.support.v17.leanback.animation.LogDecelerateInterpolator;
@@ -106,6 +107,7 @@
* Resets the focus on the button in the middle of control row.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public void resetFocus() {
ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder) getVerticalGridView()
.findViewHolderForAdapterPosition(0);
@@ -185,6 +187,7 @@
* @hide
* @deprecated use {@link PlaybackSupportFragment}
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@Deprecated
public static class OnFadeCompleteListener {
public void onFadeInComplete() {
@@ -366,6 +369,7 @@
* Sets the listener to be called when fade in or out has completed.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public void setFadeCompleteListener(OnFadeCompleteListener listener) {
mFadeCompleteListener = listener;
}
@@ -374,6 +378,7 @@
* Returns the listener to be called when fade in or out has completed.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public OnFadeCompleteListener getFadeCompleteListener() {
return mFadeCompleteListener;
}
diff --git a/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java b/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
index a8741ab..ee17e84 100644
--- a/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
+++ b/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
@@ -26,6 +26,7 @@
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
import android.support.v17.leanback.R;
import android.support.v17.leanback.animation.LogAccelerateInterpolator;
import android.support.v17.leanback.animation.LogDecelerateInterpolator;
@@ -101,6 +102,7 @@
* Resets the focus on the button in the middle of control row.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public void resetFocus() {
ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder) getVerticalGridView()
.findViewHolderForAdapterPosition(0);
@@ -179,6 +181,7 @@
* completion events.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public static class OnFadeCompleteListener {
public void onFadeInComplete() {
}
@@ -359,6 +362,7 @@
* Sets the listener to be called when fade in or out has completed.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public void setFadeCompleteListener(OnFadeCompleteListener listener) {
mFadeCompleteListener = listener;
}
@@ -367,6 +371,7 @@
* Returns the listener to be called when fade in or out has completed.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public OnFadeCompleteListener getFadeCompleteListener() {
return mFadeCompleteListener;
}
diff --git a/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java b/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
index 5bf6cc1..0a788f6 100644
--- a/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
+++ b/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
@@ -20,6 +20,7 @@
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Message;
+import android.support.annotation.RestrictTo;
import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
import android.support.v17.leanback.widget.Action;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
@@ -356,6 +357,7 @@
/**
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
protected SparseArrayObjectAdapter createPrimaryActionsAdapter(
PresenterSelector presenterSelector) {
SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(presenterSelector);
diff --git a/leanback/src/android/support/v17/leanback/util/MathUtil.java b/leanback/src/android/support/v17/leanback/util/MathUtil.java
index 487188d..bf74e40 100644
--- a/leanback/src/android/support/v17/leanback/util/MathUtil.java
+++ b/leanback/src/android/support/v17/leanback/util/MathUtil.java
@@ -13,10 +13,13 @@
*/
package android.support.v17.leanback.util;
+import android.support.annotation.RestrictTo;
+
/**
* Math Utilities for leanback library.
* @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public final class MathUtil {
private MathUtil() {
diff --git a/leanback/src/android/support/v17/leanback/widget/DetailsParallaxDrawable.java b/leanback/src/android/support/v17/leanback/widget/DetailsParallaxDrawable.java
index 37e3480..1eea797 100644
--- a/leanback/src/android/support/v17/leanback/widget/DetailsParallaxDrawable.java
+++ b/leanback/src/android/support/v17/leanback/widget/DetailsParallaxDrawable.java
@@ -22,6 +22,7 @@
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.ColorInt;
+import android.support.annotation.RestrictTo;
import android.support.v17.leanback.R;
import android.support.v17.leanback.graphics.CompositeDrawable;
import android.support.v17.leanback.graphics.FitWidthBitmapDrawable;
@@ -56,6 +57,7 @@
* </li>
* @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public class DetailsParallaxDrawable extends CompositeDrawable {
private Drawable mBottomDrawable;
diff --git a/leanback/src/android/support/v17/leanback/widget/GuidedActionEditText.java b/leanback/src/android/support/v17/leanback/widget/GuidedActionEditText.java
index f6a0eab..256e4f0 100644
--- a/leanback/src/android/support/v17/leanback/widget/GuidedActionEditText.java
+++ b/leanback/src/android/support/v17/leanback/widget/GuidedActionEditText.java
@@ -19,7 +19,9 @@
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
+import android.support.v4.widget.TextViewCompat;
import android.util.AttributeSet;
+import android.view.ActionMode;
import android.view.KeyEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.EditText;
@@ -115,4 +117,13 @@
setFocusable(false);
}
}
+
+ /**
+ * See
+ * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
+ */
+ @Override
+ public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+ TextViewCompat.setCustomSelectionActionModeCallback(this, actionModeCallback);
+ }
}
diff --git a/leanback/src/android/support/v17/leanback/widget/MediaRowFocusView.java b/leanback/src/android/support/v17/leanback/widget/MediaRowFocusView.java
index 1418a2a..471f64e 100644
--- a/leanback/src/android/support/v17/leanback/widget/MediaRowFocusView.java
+++ b/leanback/src/android/support/v17/leanback/widget/MediaRowFocusView.java
@@ -17,14 +17,16 @@
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
+import android.support.annotation.RestrictTo;
+import android.support.v17.leanback.R;
import android.util.AttributeSet;
import android.view.View;
-import android.support.v17.leanback.R;
/**
* Creates a view for a media item row in a playlist
* @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
class MediaRowFocusView extends View {
private final Paint mPaint;
diff --git a/leanback/src/android/support/v17/leanback/widget/ParallaxEffect.java b/leanback/src/android/support/v17/leanback/widget/ParallaxEffect.java
index 5c06e29..e1af762 100644
--- a/leanback/src/android/support/v17/leanback/widget/ParallaxEffect.java
+++ b/leanback/src/android/support/v17/leanback/widget/ParallaxEffect.java
@@ -17,6 +17,7 @@
package android.support.v17.leanback.widget;
import android.animation.PropertyValuesHolder;
+import android.support.annotation.RestrictTo;
import android.support.v17.leanback.widget.Parallax.FloatProperty;
import android.support.v17.leanback.widget.Parallax.FloatPropertyMarkerValue;
import android.support.v17.leanback.widget.Parallax.IntProperty;
@@ -70,6 +71,7 @@
* @return A list of Float objects that represents weight associated with each variable range.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public final List<Float> getWeights() {
return mWeights;
}
@@ -96,6 +98,7 @@
* range.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public final void setWeights(float... weights) {
for (float weight : weights) {
if (weight <= 0) {
@@ -121,6 +124,7 @@
* @return This ParallaxEffect object, allowing calls to methods in this class to be chained.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public final ParallaxEffect weights(float... weights) {
setWeights(weights);
return this;
diff --git a/leanback/src/android/support/v17/leanback/widget/ResizingTextView.java b/leanback/src/android/support/v17/leanback/widget/ResizingTextView.java
index 5888552..cc8fb55 100644
--- a/leanback/src/android/support/v17/leanback/widget/ResizingTextView.java
+++ b/leanback/src/android/support/v17/leanback/widget/ResizingTextView.java
@@ -16,9 +16,11 @@
import android.content.Context;
import android.content.res.TypedArray;
import android.support.v17.leanback.R;
+import android.support.v4.widget.TextViewCompat;
import android.text.Layout;
import android.util.AttributeSet;
import android.util.TypedValue;
+import android.view.ActionMode;
import android.widget.TextView;
/**
@@ -270,4 +272,13 @@
setPadding(getPaddingLeft(), paddingTop, getPaddingRight(), paddingBottom);
}
}
+
+ /**
+ * See
+ * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
+ */
+ @Override
+ public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+ TextViewCompat.setCustomSelectionActionModeCallback(this, actionModeCallback);
+ }
}
diff --git a/leanback/src/android/support/v17/leanback/widget/RowHeaderView.java b/leanback/src/android/support/v17/leanback/widget/RowHeaderView.java
index 0a8f98e..70b9908 100644
--- a/leanback/src/android/support/v17/leanback/widget/RowHeaderView.java
+++ b/leanback/src/android/support/v17/leanback/widget/RowHeaderView.java
@@ -13,9 +13,11 @@
*/
package android.support.v17.leanback.widget;
-import android.support.v17.leanback.R;
import android.content.Context;
+import android.support.v17.leanback.R;
+import android.support.v4.widget.TextViewCompat;
import android.util.AttributeSet;
+import android.view.ActionMode;
import android.widget.TextView;
/**
@@ -35,4 +37,12 @@
super(context, attrs, defStyle);
}
+ /**
+ * See
+ * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
+ */
+ @Override
+ public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+ TextViewCompat.setCustomSelectionActionModeCallback(this, actionModeCallback);
+ }
}
diff --git a/leanback/src/android/support/v17/leanback/widget/StreamingTextView.java b/leanback/src/android/support/v17/leanback/widget/StreamingTextView.java
index 0b8781c..d64d78e 100644
--- a/leanback/src/android/support/v17/leanback/widget/StreamingTextView.java
+++ b/leanback/src/android/support/v17/leanback/widget/StreamingTextView.java
@@ -20,6 +20,7 @@
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.v17.leanback.R;
+import android.support.v4.widget.TextViewCompat;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.SpannedString;
@@ -28,6 +29,7 @@
import android.util.AttributeSet;
import android.util.Log;
import android.util.Property;
+import android.view.ActionMode;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.EditText;
@@ -290,4 +292,14 @@
}
public void updateRecognizedText(String stableText, List<Float> rmsValues) {}
+
+ /**
+ * See
+ * {@link android.support.v4.widget.TextViewCompat
+ * #setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
+ */
+ @Override
+ public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+ TextViewCompat.setCustomSelectionActionModeCallback(this, actionModeCallback);
+ }
}
diff --git a/leanback/src/android/support/v17/leanback/widget/VideoSurfaceView.java b/leanback/src/android/support/v17/leanback/widget/VideoSurfaceView.java
index 29d778c..d42a60d 100644
--- a/leanback/src/android/support/v17/leanback/widget/VideoSurfaceView.java
+++ b/leanback/src/android/support/v17/leanback/widget/VideoSurfaceView.java
@@ -17,6 +17,7 @@
package android.support.v17.leanback.widget;
import android.content.Context;
+import android.support.annotation.RestrictTo;
import android.util.AttributeSet;
import android.view.SurfaceView;
@@ -26,6 +27,7 @@
* This class disables setTransitionVisibility() to avoid the problem.
* @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public class VideoSurfaceView extends SurfaceView {
public VideoSurfaceView(Context context) {
diff --git a/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestActivity.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestActivity.java
index dd17fd3..da7add8 100644
--- a/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestActivity.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestActivity.java
@@ -17,13 +17,10 @@
package android.support.v17.leanback.app;
+import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
-import android.app.Activity;
-/**
- * @hide from javadoc
- */
public class GuidedStepFragmentTestActivity extends Activity {
/**
diff --git a/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestBase.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestBase.java
index 34ec694..305be96 100644
--- a/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestBase.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestBase.java
@@ -34,9 +34,6 @@
import org.junit.Rule;
import org.junit.rules.TestName;
-/**
- * @hide from javadoc
- */
public class GuidedStepFragmentTestBase {
private static final long TIMEOUT = 5000;
diff --git a/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestActivity.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestActivity.java
index bac2f49..cfeb386 100644
--- a/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestActivity.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestActivity.java
@@ -18,9 +18,6 @@
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
-/**
- * @hide from javadoc
- */
public class GuidedStepSupportFragmentTestActivity extends FragmentActivity {
/**
diff --git a/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestBase.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestBase.java
index 12e4d09..adecf44 100644
--- a/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestBase.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestBase.java
@@ -31,9 +31,6 @@
import org.junit.Rule;
import org.junit.rules.TestName;
-/**
- * @hide from javadoc
- */
public class GuidedStepSupportFragmentTestBase {
private static final long TIMEOUT = 5000;
diff --git a/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestFragment.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestFragment.java
index 73e4083..555c9b0 100644
--- a/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestFragment.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestFragment.java
@@ -29,9 +29,6 @@
import java.util.HashMap;
import java.util.List;
-/**
- * @hide from javadoc
- */
public class GuidedStepTestFragment extends GuidedStepFragment {
private static final String KEY_TEST_NAME = "key_test_name";
diff --git a/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestSupportFragment.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestSupportFragment.java
index 95491ce..b356420 100644
--- a/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestSupportFragment.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestSupportFragment.java
@@ -26,9 +26,6 @@
import java.util.HashMap;
import java.util.List;
-/**
- * @hide from javadoc
- */
public class GuidedStepTestSupportFragment extends GuidedStepSupportFragment {
private static final String KEY_TEST_NAME = "key_test_name";
diff --git a/leanback/tests/java/android/support/v17/leanback/media/PlaybackControlGlueTest.java b/leanback/tests/java/android/support/v17/leanback/media/PlaybackControlGlueTest.java
index d6ec18e..6a3b212 100644
--- a/leanback/tests/java/android/support/v17/leanback/media/PlaybackControlGlueTest.java
+++ b/leanback/tests/java/android/support/v17/leanback/media/PlaybackControlGlueTest.java
@@ -23,6 +23,7 @@
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
import android.support.v17.leanback.widget.PlaybackControlsRow;
import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
import android.support.v17.leanback.widget.PlaybackRowPresenter;
@@ -33,6 +34,7 @@
import org.junit.Test;
import org.mockito.Mockito;
+@LargeTest
public class PlaybackControlGlueTest {
public static class PlaybackControlGlueImpl extends PlaybackControlGlue {
diff --git a/lifecycle/common/src/main/java/android/arch/lifecycle/GenericLifecycleObserver.java b/lifecycle/common/src/main/java/android/arch/lifecycle/GenericLifecycleObserver.java
index 59f09c4..4601478 100644
--- a/lifecycle/common/src/main/java/android/arch/lifecycle/GenericLifecycleObserver.java
+++ b/lifecycle/common/src/main/java/android/arch/lifecycle/GenericLifecycleObserver.java
@@ -16,10 +16,13 @@
package android.arch.lifecycle;
+import android.support.annotation.RestrictTo;
+
/**
* Internal class that can receive any lifecycle change and dispatch it to the receiver.
* @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
@SuppressWarnings({"WeakerAccess", "unused"})
public interface GenericLifecycleObserver extends LifecycleObserver {
/**
diff --git a/lifecycle/extensions/api/1.0.0.ignore b/lifecycle/extensions/api/1.0.0.ignore
index 8d6e57b..8d56990 100644
--- a/lifecycle/extensions/api/1.0.0.ignore
+++ b/lifecycle/extensions/api/1.0.0.ignore
@@ -8,4 +8,6 @@
e8aa0bd
5900545
aadc61e
-f39f879
\ No newline at end of file
+f39f879
+1b622fa
+fa6b788
\ No newline at end of file
diff --git a/lifecycle/extensions/api/current.txt b/lifecycle/extensions/api/current.txt
index bf093a9..8bd7d59 100644
--- a/lifecycle/extensions/api/current.txt
+++ b/lifecycle/extensions/api/current.txt
@@ -1,18 +1,5 @@
package android.arch.lifecycle {
- public class AndroidViewModel extends android.arch.lifecycle.ViewModel {
- ctor public AndroidViewModel(android.app.Application);
- method public <T extends android.app.Application> T getApplication();
- }
-
- public deprecated class LifecycleActivity extends android.support.v4.app.FragmentActivity {
- ctor public LifecycleActivity();
- }
-
- public deprecated class LifecycleFragment extends android.support.v4.app.Fragment {
- ctor public LifecycleFragment();
- }
-
public class LifecycleService extends android.app.Service implements android.arch.lifecycle.LifecycleOwner {
ctor public LifecycleService();
method public android.arch.lifecycle.Lifecycle getLifecycle();
@@ -35,15 +22,15 @@
}
public class ViewModelProviders {
- ctor public ViewModelProviders();
+ ctor public deprecated ViewModelProviders();
method public static android.arch.lifecycle.ViewModelProvider of(android.support.v4.app.Fragment);
method public static android.arch.lifecycle.ViewModelProvider of(android.support.v4.app.FragmentActivity);
method public static android.arch.lifecycle.ViewModelProvider of(android.support.v4.app.Fragment, android.arch.lifecycle.ViewModelProvider.Factory);
method public static android.arch.lifecycle.ViewModelProvider of(android.support.v4.app.FragmentActivity, android.arch.lifecycle.ViewModelProvider.Factory);
}
- public static class ViewModelProviders.DefaultFactory extends android.arch.lifecycle.ViewModelProvider.NewInstanceFactory {
- ctor public ViewModelProviders.DefaultFactory(android.app.Application);
+ public static deprecated class ViewModelProviders.DefaultFactory extends android.arch.lifecycle.ViewModelProvider.AndroidViewModelFactory {
+ ctor public deprecated ViewModelProviders.DefaultFactory(android.app.Application);
}
public class ViewModelStores {
diff --git a/lifecycle/extensions/src/main/java/android/arch/lifecycle/LifecycleActivity.java b/lifecycle/extensions/src/main/java/android/arch/lifecycle/LifecycleActivity.java
deleted file mode 100644
index 26bd508..0000000
--- a/lifecycle/extensions/src/main/java/android/arch/lifecycle/LifecycleActivity.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.arch.lifecycle;
-
-import android.support.v4.app.FragmentActivity;
-
-/**
- * @deprecated Use {@code android.support.v7.app.AppCompatActivity} instead of this class.
- */
-@Deprecated
-public class LifecycleActivity extends FragmentActivity {
-}
diff --git a/lifecycle/extensions/src/main/java/android/arch/lifecycle/LifecycleFragment.java b/lifecycle/extensions/src/main/java/android/arch/lifecycle/LifecycleFragment.java
deleted file mode 100644
index c0da66b..0000000
--- a/lifecycle/extensions/src/main/java/android/arch/lifecycle/LifecycleFragment.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.arch.lifecycle;
-
-import android.support.v4.app.Fragment;
-
-/**
- * @deprecated Use {@link Fragment} instead of it.
- */
-@Deprecated
-public class LifecycleFragment extends Fragment {
-}
diff --git a/lifecycle/extensions/src/main/java/android/arch/lifecycle/ViewModelProviders.java b/lifecycle/extensions/src/main/java/android/arch/lifecycle/ViewModelProviders.java
index b4b20aa..ab6bce6 100644
--- a/lifecycle/extensions/src/main/java/android/arch/lifecycle/ViewModelProviders.java
+++ b/lifecycle/extensions/src/main/java/android/arch/lifecycle/ViewModelProviders.java
@@ -16,7 +16,6 @@
package android.arch.lifecycle;
-import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Application;
import android.arch.lifecycle.ViewModelProvider.Factory;
@@ -25,20 +24,16 @@
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
-import java.lang.reflect.InvocationTargetException;
-
/**
* Utilities methods for {@link ViewModelStore} class.
*/
public class ViewModelProviders {
- @SuppressLint("StaticFieldLeak")
- private static DefaultFactory sDefaultFactory;
-
- private static void initializeFactoryIfNeeded(Application application) {
- if (sDefaultFactory == null) {
- sDefaultFactory = new DefaultFactory(application);
- }
+ /**
+ * @deprecated This class should not be directly instantiated
+ */
+ @Deprecated
+ public ViewModelProviders() {
}
private static Application checkApplication(Activity activity) {
@@ -62,30 +57,34 @@
* Creates a {@link ViewModelProvider}, which retains ViewModels while a scope of given
* {@code fragment} is alive. More detailed explanation is in {@link ViewModel}.
* <p>
- * It uses {@link DefaultFactory} to instantiate new ViewModels.
+ * It uses {@link ViewModelProvider.AndroidViewModelFactory} to instantiate new ViewModels.
*
* @param fragment a fragment, in whose scope ViewModels should be retained
* @return a ViewModelProvider instance
*/
@MainThread
public static ViewModelProvider of(@NonNull Fragment fragment) {
- initializeFactoryIfNeeded(checkApplication(checkActivity(fragment)));
- return new ViewModelProvider(ViewModelStores.of(fragment), sDefaultFactory);
+ ViewModelProvider.AndroidViewModelFactory factory =
+ ViewModelProvider.AndroidViewModelFactory.getInstance(
+ checkApplication(checkActivity(fragment)));
+ return new ViewModelProvider(ViewModelStores.of(fragment), factory);
}
/**
* Creates a {@link ViewModelProvider}, which retains ViewModels while a scope of given Activity
* is alive. More detailed explanation is in {@link ViewModel}.
* <p>
- * It uses {@link DefaultFactory} to instantiate new ViewModels.
+ * It uses {@link ViewModelProvider.AndroidViewModelFactory} to instantiate new ViewModels.
*
* @param activity an activity, in whose scope ViewModels should be retained
* @return a ViewModelProvider instance
*/
@MainThread
public static ViewModelProvider of(@NonNull FragmentActivity activity) {
- initializeFactoryIfNeeded(checkApplication(activity));
- return new ViewModelProvider(ViewModelStores.of(activity), sDefaultFactory);
+ ViewModelProvider.AndroidViewModelFactory factory =
+ ViewModelProvider.AndroidViewModelFactory.getInstance(
+ checkApplication(activity));
+ return new ViewModelProvider(ViewModelStores.of(activity), factory);
}
/**
@@ -124,39 +123,22 @@
/**
* {@link Factory} which may create {@link AndroidViewModel} and
* {@link ViewModel}, which have an empty constructor.
+ *
+ * @deprecated Use {@link ViewModelProvider.AndroidViewModelFactory}
*/
@SuppressWarnings("WeakerAccess")
- public static class DefaultFactory extends ViewModelProvider.NewInstanceFactory {
-
- private Application mApplication;
-
+ @Deprecated
+ public static class DefaultFactory extends ViewModelProvider.AndroidViewModelFactory {
/**
- * Creates a {@code DefaultFactory}
+ * Creates a {@code AndroidViewModelFactory}
*
* @param application an application to pass in {@link AndroidViewModel}
+ * @deprecated Use {@link ViewModelProvider.AndroidViewModelFactory} or
+ * {@link ViewModelProvider.AndroidViewModelFactory#getInstance(Application)}.
*/
+ @Deprecated
public DefaultFactory(@NonNull Application application) {
- mApplication = application;
- }
-
- @NonNull
- @Override
- public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
- if (AndroidViewModel.class.isAssignableFrom(modelClass)) {
- //noinspection TryWithIdenticalCatches
- try {
- return modelClass.getConstructor(Application.class).newInstance(mApplication);
- } catch (NoSuchMethodException e) {
- throw new RuntimeException("Cannot create an instance of " + modelClass, e);
- } catch (IllegalAccessException e) {
- throw new RuntimeException("Cannot create an instance of " + modelClass, e);
- } catch (InstantiationException e) {
- throw new RuntimeException("Cannot create an instance of " + modelClass, e);
- } catch (InvocationTargetException e) {
- throw new RuntimeException("Cannot create an instance of " + modelClass, e);
- }
- }
- return super.create(modelClass);
+ super(application);
}
}
}
diff --git a/lifecycle/extensions/src/main/java/android/arch/lifecycle/ViewModelStores.java b/lifecycle/extensions/src/main/java/android/arch/lifecycle/ViewModelStores.java
index d7d769d..e79c934 100644
--- a/lifecycle/extensions/src/main/java/android/arch/lifecycle/ViewModelStores.java
+++ b/lifecycle/extensions/src/main/java/android/arch/lifecycle/ViewModelStores.java
@@ -40,6 +40,9 @@
*/
@MainThread
public static ViewModelStore of(@NonNull FragmentActivity activity) {
+ if (activity instanceof ViewModelStoreOwner) {
+ return ((ViewModelStoreOwner) activity).getViewModelStore();
+ }
return holderFragmentFor(activity).getViewModelStore();
}
@@ -51,6 +54,9 @@
*/
@MainThread
public static ViewModelStore of(@NonNull Fragment fragment) {
+ if (fragment instanceof ViewModelStoreOwner) {
+ return ((ViewModelStoreOwner) fragment).getViewModelStore();
+ }
return holderFragmentFor(fragment).getViewModelStore();
}
}
diff --git a/lifecycle/livedata/src/main/java/android/arch/lifecycle/LiveData.java b/lifecycle/livedata/src/main/java/android/arch/lifecycle/LiveData.java
index 5b09c32..3a753a1 100644
--- a/lifecycle/livedata/src/main/java/android/arch/lifecycle/LiveData.java
+++ b/lifecycle/livedata/src/main/java/android/arch/lifecycle/LiveData.java
@@ -21,7 +21,6 @@
import android.arch.core.executor.ArchTaskExecutor;
import android.arch.core.internal.SafeIterableMap;
-import android.arch.lifecycle.Lifecycle.State;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -57,33 +56,12 @@
* @param <T> The type of data held by this instance
* @see ViewModel
*/
-@SuppressWarnings({"WeakerAccess", "unused"})
-// TODO: Thread checks are too strict right now, we may consider automatically moving them to main
-// thread.
public abstract class LiveData<T> {
private final Object mDataLock = new Object();
static final int START_VERSION = -1;
private static final Object NOT_SET = new Object();
- private static final LifecycleOwner ALWAYS_ON = new LifecycleOwner() {
-
- private LifecycleRegistry mRegistry = init();
-
- private LifecycleRegistry init() {
- LifecycleRegistry registry = new LifecycleRegistry(this);
- registry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
- registry.handleLifecycleEvent(Lifecycle.Event.ON_START);
- registry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
- return registry;
- }
-
- @Override
- public Lifecycle getLifecycle() {
- return mRegistry;
- }
- };
-
- private SafeIterableMap<Observer<T>, LifecycleBoundObserver> mObservers =
+ private SafeIterableMap<Observer<T>, ObserverWrapper> mObservers =
new SafeIterableMap<>();
// how many observers are in active state
@@ -110,8 +88,8 @@
}
};
- private void considerNotify(LifecycleBoundObserver observer) {
- if (!observer.active) {
+ private void considerNotify(ObserverWrapper observer) {
+ if (!observer.mActive) {
return;
}
// Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
@@ -119,19 +97,19 @@
// we still first check observer.active to keep it as the entrance for events. So even if
// the observer moved to an active state, if we've not received that event, we better not
// notify for a more predictable notification order.
- if (!isActiveState(observer.owner.getLifecycle().getCurrentState())) {
+ if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
- if (observer.lastVersion >= mVersion) {
+ if (observer.mLastVersion >= mVersion) {
return;
}
- observer.lastVersion = mVersion;
+ observer.mLastVersion = mVersion;
//noinspection unchecked
- observer.observer.onChanged((T) mData);
+ observer.mObserver.onChanged((T) mData);
}
- private void dispatchingValue(@Nullable LifecycleBoundObserver initiator) {
+ private void dispatchingValue(@Nullable ObserverWrapper initiator) {
if (mDispatchingValue) {
mDispatchInvalidated = true;
return;
@@ -143,7 +121,7 @@
considerNotify(initiator);
initiator = null;
} else {
- for (Iterator<Map.Entry<Observer<T>, LifecycleBoundObserver>> iterator =
+ for (Iterator<Map.Entry<Observer<T>, ObserverWrapper>> iterator =
mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
considerNotify(iterator.next().getValue());
if (mDispatchInvalidated) {
@@ -190,8 +168,8 @@
return;
}
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
- LifecycleBoundObserver existing = mObservers.putIfAbsent(observer, wrapper);
- if (existing != null && existing.owner != wrapper.owner) {
+ ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
+ if (existing != null && !existing.isAttachedTo(owner)) {
throw new IllegalArgumentException("Cannot add the same observer"
+ " with different lifecycles");
}
@@ -217,7 +195,16 @@
*/
@MainThread
public void observeForever(@NonNull Observer<T> observer) {
- observe(ALWAYS_ON, observer);
+ AlwaysActiveObserver wrapper = new AlwaysActiveObserver(observer);
+ ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
+ if (existing != null && existing instanceof LiveData.LifecycleBoundObserver) {
+ throw new IllegalArgumentException("Cannot add the same observer"
+ + " with different lifecycles");
+ }
+ if (existing != null) {
+ return;
+ }
+ wrapper.activeStateChanged(true);
}
/**
@@ -228,11 +215,11 @@
@MainThread
public void removeObserver(@NonNull final Observer<T> observer) {
assertMainThread("removeObserver");
- LifecycleBoundObserver removed = mObservers.remove(observer);
+ ObserverWrapper removed = mObservers.remove(observer);
if (removed == null) {
return;
}
- removed.owner.getLifecycle().removeObserver(removed);
+ removed.detachObserver();
removed.activeStateChanged(false);
}
@@ -241,11 +228,12 @@
*
* @param owner The {@code LifecycleOwner} scope for the observers to be removed.
*/
+ @SuppressWarnings("WeakerAccess")
@MainThread
public void removeObservers(@NonNull final LifecycleOwner owner) {
assertMainThread("removeObservers");
- for (Map.Entry<Observer<T>, LifecycleBoundObserver> entry : mObservers) {
- if (entry.getValue().owner == owner) {
+ for (Map.Entry<Observer<T>, ObserverWrapper> entry : mObservers) {
+ if (entry.getValue().isAttachedTo(owner)) {
removeObserver(entry.getKey());
}
}
@@ -343,6 +331,7 @@
*
* @return true if this LiveData has observers
*/
+ @SuppressWarnings("WeakerAccess")
public boolean hasObservers() {
return mObservers.size() > 0;
}
@@ -352,56 +341,96 @@
*
* @return true if this LiveData has active observers
*/
+ @SuppressWarnings("WeakerAccess")
public boolean hasActiveObservers() {
return mActiveCount > 0;
}
- class LifecycleBoundObserver implements GenericLifecycleObserver {
- public final LifecycleOwner owner;
- public final Observer<T> observer;
- public boolean active;
- public int lastVersion = START_VERSION;
+ class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver {
+ @NonNull final LifecycleOwner mOwner;
- LifecycleBoundObserver(LifecycleOwner owner, Observer<T> observer) {
- this.owner = owner;
- this.observer = observer;
+ LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<T> observer) {
+ super(observer);
+ mOwner = owner;
+ }
+
+ @Override
+ boolean shouldBeActive() {
+ return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
}
@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
- if (owner.getLifecycle().getCurrentState() == DESTROYED) {
- removeObserver(observer);
+ if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
+ removeObserver(mObserver);
+ return;
+ }
+ activeStateChanged(shouldBeActive());
+ }
+
+ @Override
+ boolean isAttachedTo(LifecycleOwner owner) {
+ return mOwner == owner;
+ }
+
+ @Override
+ void detachObserver() {
+ mOwner.getLifecycle().removeObserver(this);
+ }
+ }
+
+ private abstract class ObserverWrapper {
+ final Observer<T> mObserver;
+ boolean mActive;
+ int mLastVersion = START_VERSION;
+
+ ObserverWrapper(Observer<T> observer) {
+ mObserver = observer;
+ }
+
+ abstract boolean shouldBeActive();
+
+ boolean isAttachedTo(LifecycleOwner owner) {
+ return false;
+ }
+
+ void detachObserver() {
+ }
+
+ void activeStateChanged(boolean newActive) {
+ if (newActive == mActive) {
return;
}
// immediately set active state, so we'd never dispatch anything to inactive
// owner
- activeStateChanged(isActiveState(owner.getLifecycle().getCurrentState()));
- }
-
- void activeStateChanged(boolean newActive) {
- if (newActive == active) {
- return;
- }
- active = newActive;
+ mActive = newActive;
boolean wasInactive = LiveData.this.mActiveCount == 0;
- LiveData.this.mActiveCount += active ? 1 : -1;
- if (wasInactive && active) {
+ LiveData.this.mActiveCount += mActive ? 1 : -1;
+ if (wasInactive && mActive) {
onActive();
}
- if (LiveData.this.mActiveCount == 0 && !active) {
+ if (LiveData.this.mActiveCount == 0 && !mActive) {
onInactive();
}
- if (active) {
+ if (mActive) {
dispatchingValue(this);
}
}
}
- static boolean isActiveState(State state) {
- return state.isAtLeast(STARTED);
+ private class AlwaysActiveObserver extends ObserverWrapper {
+
+ AlwaysActiveObserver(Observer<T> observer) {
+ super(observer);
+ }
+
+ @Override
+ boolean shouldBeActive() {
+ return true;
+ }
}
- private void assertMainThread(String methodName) {
+ private static void assertMainThread(String methodName) {
if (!ArchTaskExecutor.getInstance().isMainThread()) {
throw new IllegalStateException("Cannot invoke " + methodName + " on a background"
+ " thread");
diff --git a/lifecycle/livedata/src/test/java/android/arch/lifecycle/LiveDataTest.java b/lifecycle/livedata/src/test/java/android/arch/lifecycle/LiveDataTest.java
index c1dc54d..dafa46d 100644
--- a/lifecycle/livedata/src/test/java/android/arch/lifecycle/LiveDataTest.java
+++ b/lifecycle/livedata/src/test/java/android/arch/lifecycle/LiveDataTest.java
@@ -30,6 +30,7 @@
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.only;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@@ -779,6 +780,28 @@
verify(mObserver4, never()).onChanged(anyString());
}
+ @Test
+ public void nestedForeverObserver() {
+ mLiveData.setValue(".");
+ mLiveData.observeForever(new Observer<String>() {
+ @Override
+ public void onChanged(@Nullable String s) {
+ mLiveData.observeForever(mock(Observer.class));
+ mLiveData.removeObserver(this);
+ }
+ });
+ verify(mActiveObserversChanged, only()).onCall(true);
+ }
+
+ @Test
+ public void readdForeverObserver() {
+ Observer observer = mock(Observer.class);
+ mLiveData.observeForever(observer);
+ mLiveData.observeForever(observer);
+ mLiveData.removeObserver(observer);
+ assertThat(mLiveData.hasObservers(), is(false));
+ }
+
private GenericLifecycleObserver getGenericLifecycleObserver(Lifecycle lifecycle) {
ArgumentCaptor<GenericLifecycleObserver> captor =
ArgumentCaptor.forClass(GenericLifecycleObserver.class);
diff --git a/lifecycle/livedata/src/test/java/android/arch/lifecycle/TransformationsTest.java b/lifecycle/livedata/src/test/java/android/arch/lifecycle/TransformationsTest.java
index 940a3e8..02397da 100644
--- a/lifecycle/livedata/src/test/java/android/arch/lifecycle/TransformationsTest.java
+++ b/lifecycle/livedata/src/test/java/android/arch/lifecycle/TransformationsTest.java
@@ -21,6 +21,7 @@
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.only;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -191,4 +192,25 @@
verify(observer, never()).onChanged(anyString());
assertThat(first.hasObservers(), is(false));
}
+
+ @Test
+ public void noObsoleteValueTest() {
+ MutableLiveData<Integer> numbers = new MutableLiveData<>();
+ LiveData<Integer> squared = Transformations.map(numbers, new Function<Integer, Integer>() {
+ @Override
+ public Integer apply(Integer input) {
+ return input * input;
+ }
+ });
+
+ Observer observer = mock(Observer.class);
+ squared.setValue(1);
+ squared.observeForever(observer);
+ verify(observer).onChanged(1);
+ squared.removeObserver(observer);
+ reset(observer);
+ numbers.setValue(2);
+ squared.observeForever(observer);
+ verify(observer, only()).onChanged(4);
+ }
}
diff --git a/lifecycle/viewmodel/api/current.txt b/lifecycle/viewmodel/api/current.txt
index 56de7fe..6a82d4c 100644
--- a/lifecycle/viewmodel/api/current.txt
+++ b/lifecycle/viewmodel/api/current.txt
@@ -1,5 +1,10 @@
package android.arch.lifecycle {
+ public class AndroidViewModel extends android.arch.lifecycle.ViewModel {
+ ctor public AndroidViewModel(android.app.Application);
+ method public <T extends android.app.Application> T getApplication();
+ }
+
public abstract class ViewModel {
ctor public ViewModel();
method protected void onCleared();
@@ -12,6 +17,11 @@
method public <T extends android.arch.lifecycle.ViewModel> T get(java.lang.String, java.lang.Class<T>);
}
+ public static class ViewModelProvider.AndroidViewModelFactory extends android.arch.lifecycle.ViewModelProvider.NewInstanceFactory {
+ ctor public ViewModelProvider.AndroidViewModelFactory(android.app.Application);
+ method public static android.arch.lifecycle.ViewModelProvider.AndroidViewModelFactory getInstance(android.app.Application);
+ }
+
public static abstract interface ViewModelProvider.Factory {
method public abstract <T extends android.arch.lifecycle.ViewModel> T create(java.lang.Class<T>);
}
diff --git a/lifecycle/extensions/src/main/java/android/arch/lifecycle/AndroidViewModel.java b/lifecycle/viewmodel/src/main/java/android/arch/lifecycle/AndroidViewModel.java
similarity index 100%
rename from lifecycle/extensions/src/main/java/android/arch/lifecycle/AndroidViewModel.java
rename to lifecycle/viewmodel/src/main/java/android/arch/lifecycle/AndroidViewModel.java
diff --git a/lifecycle/viewmodel/src/main/java/android/arch/lifecycle/ViewModelProvider.java b/lifecycle/viewmodel/src/main/java/android/arch/lifecycle/ViewModelProvider.java
index a7b3aeb..e01aa19 100644
--- a/lifecycle/viewmodel/src/main/java/android/arch/lifecycle/ViewModelProvider.java
+++ b/lifecycle/viewmodel/src/main/java/android/arch/lifecycle/ViewModelProvider.java
@@ -16,9 +16,12 @@
package android.arch.lifecycle;
+import android.app.Application;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
+import java.lang.reflect.InvocationTargetException;
+
/**
* An utility class that provides {@code ViewModels} for a scope.
* <p>
@@ -152,4 +155,57 @@
}
}
}
+
+ /**
+ * {@link Factory} which may create {@link AndroidViewModel} and
+ * {@link ViewModel}, which have an empty constructor.
+ */
+ public static class AndroidViewModelFactory extends ViewModelProvider.NewInstanceFactory {
+
+ private static AndroidViewModelFactory sInstance;
+
+ /**
+ * Retrieve a singleton instance of AndroidViewModelFactory.
+ *
+ * @param application an application to pass in {@link AndroidViewModel}
+ * @return A valid {@link AndroidViewModelFactory}
+ */
+ public static AndroidViewModelFactory getInstance(@NonNull Application application) {
+ if (sInstance == null) {
+ sInstance = new AndroidViewModelFactory(application);
+ }
+ return sInstance;
+ }
+
+ private Application mApplication;
+
+ /**
+ * Creates a {@code AndroidViewModelFactory}
+ *
+ * @param application an application to pass in {@link AndroidViewModel}
+ */
+ public AndroidViewModelFactory(@NonNull Application application) {
+ mApplication = application;
+ }
+
+ @NonNull
+ @Override
+ public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
+ if (AndroidViewModel.class.isAssignableFrom(modelClass)) {
+ //noinspection TryWithIdenticalCatches
+ try {
+ return modelClass.getConstructor(Application.class).newInstance(mApplication);
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException("Cannot create an instance of " + modelClass, e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Cannot create an instance of " + modelClass, e);
+ } catch (InstantiationException e) {
+ throw new RuntimeException("Cannot create an instance of " + modelClass, e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException("Cannot create an instance of " + modelClass, e);
+ }
+ }
+ return super.create(modelClass);
+ }
+ }
}
diff --git a/paging/runtime/src/main/java/android/arch/paging/PagedListAdapter.java b/paging/runtime/src/main/java/android/arch/paging/PagedListAdapter.java
index a8158c2..be23271 100644
--- a/paging/runtime/src/main/java/android/arch/paging/PagedListAdapter.java
+++ b/paging/runtime/src/main/java/android/arch/paging/PagedListAdapter.java
@@ -50,7 +50,7 @@
* class MyViewModel extends ViewModel {
* public final LiveData<PagedList<User>> usersList;
* public MyViewModel(UserDao userDao) {
- * usersList = LivePagedListBuilder<>(
+ * usersList = new LivePagedListBuilder<>(
* 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 7a0b81a..ba8ffab 100644
--- a/paging/runtime/src/main/java/android/arch/paging/PagedListAdapterHelper.java
+++ b/paging/runtime/src/main/java/android/arch/paging/PagedListAdapterHelper.java
@@ -54,7 +54,7 @@
* class MyViewModel extends ViewModel {
* public final LiveData<PagedList<User>> usersList;
* public MyViewModel(UserDao userDao) {
- * usersList = LivePagedListBuilder<>(
+ * usersList = new LivePagedListBuilder<>(
* userDao.usersByLastName(), /* page size {@literal *}/ 20).build();
* }
* }
@@ -72,10 +72,8 @@
* }
*
* class UserAdapter extends RecyclerView.Adapter<UserViewHolder> {
- * private final PagedListAdapterHelper<User> mHelper;
- * public UserAdapter(PagedListAdapterHelper.Builder<User> builder) {
- * mHelper = new PagedListAdapterHelper(this, DIFF_CALLBACK);
- * }
+ * private final PagedListAdapterHelper<User> mHelper
+ * = new PagedListAdapterHelper(this, DIFF_CALLBACK);
* {@literal @}Override
* public int getItemCount() {
* return mHelper.getItemCount();
diff --git a/paging/runtime/src/main/java/android/support/v7/recyclerview/extensions/ListAdapterHelper.java b/paging/runtime/src/main/java/android/support/v7/recyclerview/extensions/ListAdapterHelper.java
index d0c7bb3..0c7806f 100644
--- a/paging/runtime/src/main/java/android/support/v7/recyclerview/extensions/ListAdapterHelper.java
+++ b/paging/runtime/src/main/java/android/support/v7/recyclerview/extensions/ListAdapterHelper.java
@@ -68,10 +68,8 @@
* }
*
* class UserAdapter extends RecyclerView.Adapter<UserViewHolder> {
- * private final ListAdapterHelper<User> mHelper;
- * public UserAdapter(ListAdapterHelper.Builder<User> builder) {
- * mHelper = new ListAdapterHelper(this, User.DIFF_CALLBACK);
- * }
+ * private final ListAdapterHelper<User> mHelper
+ * = new ListAdapterHelper(this, User.DIFF_CALLBACK);
* {@literal @}Override
* public int getItemCount() {
* return mHelper.getItemCount();
diff --git a/persistence/db-framework/api/current.txt b/persistence/db-framework/api/current.txt
new file mode 100644
index 0000000..7051765
--- /dev/null
+++ b/persistence/db-framework/api/current.txt
@@ -0,0 +1,9 @@
+package android.arch.persistence.db.framework {
+
+ public final class FrameworkSQLiteOpenHelperFactory implements android.arch.persistence.db.SupportSQLiteOpenHelper.Factory {
+ ctor public FrameworkSQLiteOpenHelperFactory();
+ method public android.arch.persistence.db.SupportSQLiteOpenHelper create(android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration);
+ }
+
+}
+
diff --git a/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java b/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java
index e9c2b74..d564a03 100644
--- a/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java
+++ b/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java
@@ -16,6 +16,8 @@
package android.arch.persistence.db.framework;
+import static android.text.TextUtils.isEmpty;
+
import android.arch.persistence.db.SimpleSQLiteQuery;
import android.arch.persistence.db.SupportSQLiteDatabase;
import android.arch.persistence.db.SupportSQLiteQuery;
@@ -312,8 +314,4 @@
public void close() throws IOException {
mDelegate.close();
}
-
- private static boolean isEmpty(String input) {
- return input == null || input.length() == 0;
- }
}
diff --git a/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java b/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java
index ccb5614..7f07865 100644
--- a/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java
+++ b/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java
@@ -22,7 +22,7 @@
/**
* Delegates all calls to a {@link SQLiteStatement}.
*/
-class FrameworkSQLiteStatement implements SupportSQLiteStatement {
+class FrameworkSQLiteStatement extends FrameworkSQLiteProgram implements SupportSQLiteStatement {
private final SQLiteStatement mDelegate;
/**
@@ -31,40 +31,11 @@
* @param delegate The SQLiteStatement to delegate calls to.
*/
FrameworkSQLiteStatement(SQLiteStatement delegate) {
+ super(delegate);
mDelegate = delegate;
}
@Override
- public void bindNull(int index) {
- mDelegate.bindNull(index);
- }
-
- @Override
- public void bindLong(int index, long value) {
- mDelegate.bindLong(index, value);
- }
-
- @Override
- public void bindDouble(int index, double value) {
- mDelegate.bindDouble(index, value);
- }
-
- @Override
- public void bindString(int index, String value) {
- mDelegate.bindString(index, value);
- }
-
- @Override
- public void bindBlob(int index, byte[] value) {
- mDelegate.bindBlob(index, value);
- }
-
- @Override
- public void clearBindings() {
- mDelegate.clearBindings();
- }
-
- @Override
public void execute() {
mDelegate.execute();
}
@@ -88,9 +59,4 @@
public String simpleQueryForString() {
return mDelegate.simpleQueryForString();
}
-
- @Override
- public void close() {
- mDelegate.close();
- }
}
diff --git a/persistence/db/api/current.txt b/persistence/db/api/current.txt
new file mode 100644
index 0000000..f96f17a
--- /dev/null
+++ b/persistence/db/api/current.txt
@@ -0,0 +1,123 @@
+package android.arch.persistence.db {
+
+ public final class SimpleSQLiteQuery implements android.arch.persistence.db.SupportSQLiteQuery {
+ ctor public SimpleSQLiteQuery(java.lang.String, java.lang.Object[]);
+ ctor public SimpleSQLiteQuery(java.lang.String);
+ method public static void bind(android.arch.persistence.db.SupportSQLiteProgram, java.lang.Object[]);
+ method public void bindTo(android.arch.persistence.db.SupportSQLiteProgram);
+ method public java.lang.String getSql();
+ }
+
+ public abstract interface SupportSQLiteDatabase implements java.io.Closeable {
+ method public abstract void beginTransaction();
+ method public abstract void beginTransactionNonExclusive();
+ method public abstract void beginTransactionWithListener(android.database.sqlite.SQLiteTransactionListener);
+ method public abstract void beginTransactionWithListenerNonExclusive(android.database.sqlite.SQLiteTransactionListener);
+ method public abstract android.arch.persistence.db.SupportSQLiteStatement compileStatement(java.lang.String);
+ method public abstract int delete(java.lang.String, java.lang.String, java.lang.Object[]);
+ method public abstract void disableWriteAheadLogging();
+ method public abstract boolean enableWriteAheadLogging();
+ method public abstract void endTransaction();
+ method public abstract void execSQL(java.lang.String) throws android.database.SQLException;
+ method public abstract void execSQL(java.lang.String, java.lang.Object[]) throws android.database.SQLException;
+ method public abstract java.util.List<android.util.Pair<java.lang.String, java.lang.String>> getAttachedDbs();
+ method public abstract long getMaximumSize();
+ method public abstract long getPageSize();
+ method public abstract java.lang.String getPath();
+ method public abstract int getVersion();
+ method public abstract boolean inTransaction();
+ method public abstract long insert(java.lang.String, int, android.content.ContentValues) throws android.database.SQLException;
+ method public abstract boolean isDatabaseIntegrityOk();
+ method public abstract boolean isDbLockedByCurrentThread();
+ method public abstract boolean isOpen();
+ method public abstract boolean isReadOnly();
+ method public abstract boolean isWriteAheadLoggingEnabled();
+ method public abstract boolean needUpgrade(int);
+ method public abstract android.database.Cursor query(java.lang.String);
+ method public abstract android.database.Cursor query(java.lang.String, java.lang.Object[]);
+ method public abstract android.database.Cursor query(android.arch.persistence.db.SupportSQLiteQuery);
+ method public abstract android.database.Cursor query(android.arch.persistence.db.SupportSQLiteQuery, android.os.CancellationSignal);
+ method public abstract void setForeignKeyConstraintsEnabled(boolean);
+ method public abstract void setLocale(java.util.Locale);
+ method public abstract void setMaxSqlCacheSize(int);
+ method public abstract long setMaximumSize(long);
+ method public abstract void setPageSize(long);
+ method public abstract void setTransactionSuccessful();
+ method public abstract void setVersion(int);
+ method public abstract int update(java.lang.String, int, android.content.ContentValues, java.lang.String, java.lang.Object[]);
+ method public abstract boolean yieldIfContendedSafely();
+ method public abstract boolean yieldIfContendedSafely(long);
+ }
+
+ public abstract interface SupportSQLiteOpenHelper {
+ method public abstract void close();
+ method public abstract java.lang.String getDatabaseName();
+ method public abstract android.arch.persistence.db.SupportSQLiteDatabase getReadableDatabase();
+ method public abstract android.arch.persistence.db.SupportSQLiteDatabase getWritableDatabase();
+ method public abstract void setWriteAheadLoggingEnabled(boolean);
+ }
+
+ public static abstract class SupportSQLiteOpenHelper.Callback {
+ ctor public SupportSQLiteOpenHelper.Callback(int);
+ method public void onConfigure(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public void onCorruption(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public abstract void onCreate(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public void onDowngrade(android.arch.persistence.db.SupportSQLiteDatabase, int, int);
+ method public void onOpen(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public abstract void onUpgrade(android.arch.persistence.db.SupportSQLiteDatabase, int, int);
+ field public final int version;
+ }
+
+ public static class SupportSQLiteOpenHelper.Configuration {
+ method public static android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration.Builder builder(android.content.Context);
+ field public final android.arch.persistence.db.SupportSQLiteOpenHelper.Callback callback;
+ field public final android.content.Context context;
+ field public final java.lang.String name;
+ }
+
+ public static class SupportSQLiteOpenHelper.Configuration.Builder {
+ method public android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration build();
+ method public android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration.Builder callback(android.arch.persistence.db.SupportSQLiteOpenHelper.Callback);
+ method public android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration.Builder name(java.lang.String);
+ }
+
+ public static abstract interface SupportSQLiteOpenHelper.Factory {
+ method public abstract android.arch.persistence.db.SupportSQLiteOpenHelper create(android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration);
+ }
+
+ public abstract interface SupportSQLiteProgram implements java.io.Closeable {
+ method public abstract void bindBlob(int, byte[]);
+ method public abstract void bindDouble(int, double);
+ method public abstract void bindLong(int, long);
+ method public abstract void bindNull(int);
+ method public abstract void bindString(int, java.lang.String);
+ method public abstract void clearBindings();
+ }
+
+ public abstract interface SupportSQLiteQuery {
+ method public abstract void bindTo(android.arch.persistence.db.SupportSQLiteProgram);
+ method public abstract java.lang.String getSql();
+ }
+
+ public final class SupportSQLiteQueryBuilder {
+ method public static android.arch.persistence.db.SupportSQLiteQueryBuilder builder(java.lang.String);
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder columns(java.lang.String[]);
+ method public android.arch.persistence.db.SupportSQLiteQuery create();
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder distinct();
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder groupBy(java.lang.String);
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder having(java.lang.String);
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder limit(java.lang.String);
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder orderBy(java.lang.String);
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder selection(java.lang.String, java.lang.Object[]);
+ }
+
+ public abstract interface SupportSQLiteStatement implements android.arch.persistence.db.SupportSQLiteProgram {
+ method public abstract void execute();
+ method public abstract long executeInsert();
+ method public abstract int executeUpdateDelete();
+ method public abstract long simpleQueryForLong();
+ method public abstract java.lang.String simpleQueryForString();
+ }
+
+}
+
diff --git a/persistence/db/src/main/java/android/arch/persistence/db/SimpleSQLiteQuery.java b/persistence/db/src/main/java/android/arch/persistence/db/SimpleSQLiteQuery.java
index e2a3829..bcf4f49 100644
--- a/persistence/db/src/main/java/android/arch/persistence/db/SimpleSQLiteQuery.java
+++ b/persistence/db/src/main/java/android/arch/persistence/db/SimpleSQLiteQuery.java
@@ -17,8 +17,8 @@
package android.arch.persistence.db;
/**
- * A basic implemtation of {@link SupportSQLiteQuery} which receives a query and its args and binds
- * args based on the passed in Object type.
+ * A basic implementation of {@link SupportSQLiteQuery} which receives a query and its args and
+ * binds args based on the passed in Object type.
*/
public final class SimpleSQLiteQuery implements SupportSQLiteQuery {
private final String mQuery;
diff --git a/room/common/src/main/java/android/arch/persistence/room/ColumnInfo.java b/room/common/src/main/java/android/arch/persistence/room/ColumnInfo.java
index 65da379..32b5818 100644
--- a/room/common/src/main/java/android/arch/persistence/room/ColumnInfo.java
+++ b/room/common/src/main/java/android/arch/persistence/room/ColumnInfo.java
@@ -68,7 +68,7 @@
* collation sequence to the column, and SQLite treats it like {@link #BINARY}.
*
* @return The collation sequence of the column. This is either {@link #UNSPECIFIED},
- * {@link #BINARY}, {@link #NOCASE}, or {@link #RTRIM}.
+ * {@link #BINARY}, {@link #NOCASE}, {@link #RTRIM}, {@link #LOCALIZED} or {@link #UNICODE}.
*/
@Collate int collate() default UNSPECIFIED;
@@ -141,8 +141,20 @@
* @see #collate()
*/
int RTRIM = 4;
+ /**
+ * Collation sequence that uses system's current locale.
+ *
+ * @see #collate()
+ */
+ int LOCALIZED = 5;
+ /**
+ * Collation sequence that uses Unicode Collation Algorithm.
+ *
+ * @see #collate()
+ */
+ int UNICODE = 6;
- @IntDef({UNSPECIFIED, BINARY, NOCASE, RTRIM})
+ @IntDef({UNSPECIFIED, BINARY, NOCASE, RTRIM, LOCALIZED, UNICODE})
@interface Collate {
}
}
diff --git a/room/common/src/main/java/android/arch/persistence/room/RawQuery.java b/room/common/src/main/java/android/arch/persistence/room/RawQuery.java
new file mode 100644
index 0000000..b41feab
--- /dev/null
+++ b/room/common/src/main/java/android/arch/persistence/room/RawQuery.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a method in a {@link Dao} annotated class as a raw query method where you can pass the
+ * query as a {@link String} or a
+ * {@link android.arch.persistence.db.SupportSQLiteQuery SupportSQLiteQuery}.
+ * <pre>
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery
+ * User getUser(String query);
+ * {@literal @}RawQuery
+ * User getUserViaQuery(SupportSQLiteQuery query);
+ * }
+ * User user = rawDao.getUser("SELECT * FROM User WHERE id = 3 LIMIT 1");
+ * SimpleSQLiteQuery query = new SimpleSQLiteQuery("SELECT * FROM User WHERE id = ? LIMIT 1",
+ * new Object[]{3});
+ * User user2 = rawDao.getUserViaQuery(query);
+ * </pre>
+ * <p>
+ * Room will generate the code based on the return type of the function and failure to
+ * pass a proper query will result in a runtime failure or an undefined result.
+ * <p>
+ * If you know the query at compile time, you should always prefer {@link Query} since it validates
+ * the query at compile time and also generates more efficient code since Room can compute the
+ * query result at compile time (e.g. it does not need to account for possibly missing columns in
+ * the response).
+ * <p>
+ * On the other hand, {@code RawQuery} serves as an escape hatch where you can build your own
+ * SQL query at runtime but still use Room to convert it into objects.
+ * <p>
+ * {@code RawQuery} methods must return a non-void type. If you want to execute a raw query that
+ * does not return any value, use {@link android.arch.persistence.room.RoomDatabase#query
+ * RoomDatabase#query} methods.
+ * <p>
+ * <b>Observable Queries:</b>
+ * <p>
+ * {@code RawQuery} methods can return observable types but you need to specify which tables are
+ * accessed in the query using the {@link #observedEntities()} field in the annotation.
+ * <pre>
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery(observedEntities = User.class)
+ * LiveData<List<User>> getUsers(String query);
+ * }
+ * LiveData<List<User>> liveUsers = rawDao.getUsers("SELECT * FROM User ORDER BY name DESC");
+ * </pre>
+ * <b>Returning Pojos:</b>
+ * <p>
+ * RawQueries can also return plain old java objects, similar to {@link Query} methods.
+ * <pre>
+ * public class NameAndLastName {
+ * public final String name;
+ * public final String lastName;
+ *
+ * public NameAndLastName(String name, String lastName) {
+ * this.name = name;
+ * this.lastName = lastName;
+ * }
+ * }
+ *
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery
+ * NameAndLastName getNameAndLastName(String query);
+ * }
+ * NameAndLastName result = rawDao.getNameAndLastName("SELECT * FROM User WHERE id = 3")
+ * // or
+ * NameAndLastName result = rawDao.getNameAndLastName("SELECT name, lastName FROM User WHERE id =
+ * 3")
+ * </pre>
+ * <p>
+ * <b>Pojos with Embedded Fields:</b>
+ * <p>
+ * {@code RawQuery} methods can return pojos that include {@link Embedded} fields as well.
+ * <pre>
+ * public class UserAndPet {
+ * {@literal @}Embedded
+ * public User user;
+ * {@literal @}Embedded
+ * public Pet pet;
+ * }
+ *
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery
+ * UserAndPet getUserAndPet(String query);
+ * }
+ * UserAndPet received = rawDao.getUserAndPet(
+ * "SELECT * FROM User, Pet WHERE User.id = Pet.userId LIMIT 1")
+ * </pre>
+ *
+ * <b>Relations:</b>
+ * <p>
+ * {@code RawQuery} return types can also be objects with {@link Relation Relations}.
+ * <pre>
+ * public class UserAndAllPets {
+ * {@literal @}Embedded
+ * public User user;
+ * {@literal @}Relation(parentColumn = "id", entityColumn = "userId")
+ * public List<Pet> pets;
+ * }
+ *
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery
+ * List<UserAndAllPets> getUsersAndAllPets(String query);
+ * }
+ * List<UserAndAllPets> result = rawDao.getUsersAndAllPets("SELECT * FROM users");
+ * </pre>
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.CLASS)
+public @interface RawQuery {
+ /**
+ * Denotes the list of entities which are accessed in the provided query and should be observed
+ * for invalidation if the query is observable.
+ * <p>
+ * The listed classes should be {@link Entity Entities} that are linked from the containing
+ * {@link Database}.
+ * <p>
+ * Providing this field in a non-observable query has no impact.
+ * <pre>
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery(observedEntities = User.class)
+ * LiveData<List<User>> getUsers(String query);
+ * }
+ * LiveData<List<User>> liveUsers = rawDao.getUsers("select * from User ORDER BY name
+ * DESC");
+ * </pre>
+ *
+ * @return List of entities that should invalidate the query if changed.
+ */
+ Class[] observedEntities() default {};
+}
diff --git a/room/compiler/build.gradle b/room/compiler/build.gradle
index f311a30..0d62cf8 100644
--- a/room/compiler/build.gradle
+++ b/room/compiler/build.gradle
@@ -14,6 +14,9 @@
* limitations under the License.
*/
+
+import android.support.SupportConfig
+
import static android.support.dependencies.DependenciesKt.*
import android.support.LibraryGroups
import android.support.LibraryVersions
@@ -53,7 +56,7 @@
testCompile(INTELLIJ_ANNOTATIONS)
testCompile(JSR250)
testCompile(MOCKITO_CORE)
- testCompile fileTree(dir: "${sdkHandler.sdkFolder}/platforms/android-$rootProject.ext.currentSdk/",
+ testCompile fileTree(dir: "${sdkHandler.sdkFolder}/platforms/android-$SupportConfig.CURRENT_SDK_VERSION/",
include : "android.jar")
testCompile fileTree(dir: "${new File(project(":room:runtime").buildDir, "libJar")}",
include : "*.jar")
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/element_ext.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/element_ext.kt
index 521c271..44800fc 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/element_ext.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/element_ext.kt
@@ -204,7 +204,7 @@
}
// converts ? in Set< ? extends Foo> to Foo
-private fun TypeMirror.extendsBound(): TypeMirror? {
+fun TypeMirror.extendsBound(): TypeMirror? {
return this.accept(object : SimpleTypeVisitor7<TypeMirror?, Void?>() {
override fun visitWildcard(type: WildcardType, ignored: Void?): TypeMirror? {
return type.extendsBound
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/javapoet_ext.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/javapoet_ext.kt
index 367c926..5893e8f 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/javapoet_ext.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/javapoet_ext.kt
@@ -46,6 +46,8 @@
val SQLITE_OPEN_HELPER_CONFIG_BUILDER: ClassName =
ClassName.get("android.arch.persistence.db",
"SupportSQLiteOpenHelper.Configuration.Builder")
+ val QUERY: ClassName =
+ ClassName.get("android.arch.persistence.db", "SupportSQLiteQuery")
}
object RoomTypeNames {
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/parser/ParsedQuery.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/parser/ParsedQuery.kt
index fcf2e08..55a9bf9 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/parser/ParsedQuery.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/parser/ParsedQuery.kt
@@ -38,15 +38,18 @@
data class Table(val name: String, val alias: String)
-data class ParsedQuery(val original: String, val type: QueryType,
- val inputs: List<TerminalNode>,
- // pairs of table name and alias,
- val tables: Set<Table>,
- val syntaxErrors: List<String>) {
+data class ParsedQuery(
+ val original: String,
+ val type: QueryType,
+ val inputs: List<TerminalNode>,
+ // pairs of table name and alias,
+ val tables: Set<Table>,
+ val syntaxErrors: List<String>,
+ val runtimeQueryPlaceholder: Boolean) {
companion object {
val STARTS_WITH_NUMBER = "^\\?[0-9]".toRegex()
val MISSING = ParsedQuery("missing query", QueryType.UNKNOWN, emptyList(), emptySet(),
- emptyList())
+ emptyList(), false)
}
/**
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 affa8c9..c5c5f17 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
@@ -28,16 +28,21 @@
import javax.lang.model.type.TypeKind
import javax.lang.model.type.TypeMirror
-class QueryVisitor(val original: String, val syntaxErrors: ArrayList<String>,
- statement: ParseTree) : SQLiteBaseVisitor<Void?>() {
- val bindingExpressions = arrayListOf<TerminalNode>()
+@Suppress("FunctionName")
+class QueryVisitor(
+ private val original: String,
+ private val syntaxErrors: ArrayList<String>,
+ statement: ParseTree,
+ private val forRuntimeQuery: Boolean
+) : SQLiteBaseVisitor<Void?>() {
+ private val bindingExpressions = arrayListOf<TerminalNode>()
// table name alias mappings
- val tableNames = mutableSetOf<Table>()
- val withClauseNames = mutableSetOf<String>()
- val queryType: QueryType
+ private val tableNames = mutableSetOf<Table>()
+ private val withClauseNames = mutableSetOf<String>()
+ private val queryType: QueryType
init {
- queryType = (0..statement.childCount - 1).map {
+ queryType = (0 until statement.childCount).map {
findQueryType(statement.getChild(it))
}.filterNot { it == QueryType.UNKNOWN }.firstOrNull() ?: QueryType.UNKNOWN
@@ -78,11 +83,13 @@
}
fun createParsedQuery(): ParsedQuery {
- return ParsedQuery(original,
- queryType,
- bindingExpressions.sortedBy { it.sourceInterval.a },
- tableNames,
- syntaxErrors)
+ return ParsedQuery(
+ original = original,
+ type = queryType,
+ inputs = bindingExpressions.sortedBy { it.sourceInterval.a },
+ tables = tableNames,
+ syntaxErrors = syntaxErrors,
+ runtimeQueryPlaceholder = forRuntimeQuery)
}
override fun visitCommon_table_expression(
@@ -129,9 +136,10 @@
val parser = SQLiteParser(tokenStream)
val syntaxErrors = arrayListOf<String>()
parser.addErrorListener(object : BaseErrorListener() {
- override fun syntaxError(recognizer: Recognizer<*, *>, offendingSymbol: Any,
- line: Int, charPositionInLine: Int, msg: String,
- e: RecognitionException?) {
+ override fun syntaxError(
+ recognizer: Recognizer<*, *>, offendingSymbol: Any,
+ line: Int, charPositionInLine: Int, msg: String,
+ e: RecognitionException?) {
syntaxErrors.add(msg)
}
})
@@ -141,7 +149,7 @@
if (statementList.isEmpty()) {
syntaxErrors.add(ParserErrors.NOT_ONE_QUERY)
return ParsedQuery(input, QueryType.UNKNOWN, emptyList(), emptySet(),
- listOf(ParserErrors.NOT_ONE_QUERY))
+ listOf(ParserErrors.NOT_ONE_QUERY), false)
}
val statements = statementList.first().children
.filter { it is SQLiteParser.Sql_stmtContext }
@@ -149,15 +157,34 @@
syntaxErrors.add(ParserErrors.NOT_ONE_QUERY)
}
val statement = statements.first()
- return QueryVisitor(input, syntaxErrors, statement).createParsedQuery()
+ return QueryVisitor(
+ original = input,
+ syntaxErrors = syntaxErrors,
+ statement = statement,
+ forRuntimeQuery = false).createParsedQuery()
} catch (antlrError: RuntimeException) {
return ParsedQuery(input, QueryType.UNKNOWN, emptyList(), emptySet(),
- listOf("unknown error while parsing $input : ${antlrError.message}"))
+ listOf("unknown error while parsing $input : ${antlrError.message}"),
+ false)
}
}
fun isValidIdentifier(input: String): Boolean =
input.isNotBlank() && INVALID_IDENTIFIER_CHARS.none { input.contains(it) }
+
+ /**
+ * creates a dummy select query for raw queries that queries the given list of tables.
+ */
+ fun rawQueryForTables(tableNames: Set<String>): ParsedQuery {
+ return ParsedQuery(
+ original = "raw query",
+ type = QueryType.UNKNOWN,
+ inputs = emptyList(),
+ tables = tableNames.map { Table(name = it, alias = it) }.toSet(),
+ syntaxErrors = emptyList(),
+ runtimeQueryPlaceholder = true
+ )
+ }
}
}
@@ -181,6 +208,7 @@
INTEGER,
REAL,
BLOB;
+
fun getTypeMirrors(env: ProcessingEnvironment): List<TypeMirror>? {
val typeUtils = env.typeUtils
return when (this) {
@@ -219,7 +247,9 @@
enum class Collate {
BINARY,
NOCASE,
- RTRIM;
+ RTRIM,
+ LOCALIZED,
+ UNICODE;
companion object {
fun fromAnnotationValue(value: Int): Collate? {
@@ -227,6 +257,8 @@
ColumnInfo.BINARY -> BINARY
ColumnInfo.NOCASE -> NOCASE
ColumnInfo.RTRIM -> RTRIM
+ ColumnInfo.LOCALIZED -> LOCALIZED
+ ColumnInfo.UNICODE -> UNICODE
else -> null
}
}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt
index e856a56..cc758eb 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt
@@ -19,6 +19,7 @@
import android.arch.persistence.room.Delete
import android.arch.persistence.room.Insert
import android.arch.persistence.room.Query
+import android.arch.persistence.room.RawQuery
import android.arch.persistence.room.SkipQueryVerification
import android.arch.persistence.room.Transaction
import android.arch.persistence.room.Update
@@ -42,7 +43,7 @@
companion object {
val PROCESSED_ANNOTATIONS = listOf(Insert::class, Delete::class, Query::class,
- Update::class)
+ Update::class, RawQuery::class)
}
fun process(): Dao {
@@ -71,11 +72,14 @@
Delete::class
} else if (method.hasAnnotation(Update::class)) {
Update::class
+ } else if (method.hasAnnotation(RawQuery::class)) {
+ RawQuery::class
} else {
Any::class
}
}
- val processorVerifier = if (element.hasAnnotation(SkipQueryVerification::class)) {
+ val processorVerifier = if (element.hasAnnotation(SkipQueryVerification::class) ||
+ element.hasAnnotation(RawQuery::class)) {
null
} else {
dbVerifier
@@ -89,6 +93,14 @@
dbVerifier = processorVerifier).process()
} ?: emptyList()
+ val rawQueryMethods = methods[RawQuery::class]?.map {
+ RawQueryMethodProcessor(
+ baseContext = context,
+ containing = declaredType,
+ executableElement = it
+ ).process()
+ } ?: emptyList()
+
val insertionMethods = methods[Insert::class]?.map {
InsertionMethodProcessor(
baseContext = context,
@@ -149,6 +161,7 @@
return Dao(element = element,
type = declaredType,
queryMethods = queryMethods,
+ rawQueryMethods = rawQueryMethods,
insertionMethods = insertionMethods,
deletionMethods = deletionMethods,
updateMethods = updateMethods,
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt
index 0b17d3c..88961e7 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt
@@ -19,6 +19,7 @@
import android.arch.persistence.room.Delete
import android.arch.persistence.room.Insert
import android.arch.persistence.room.Query
+import android.arch.persistence.room.RawQuery
import android.arch.persistence.room.Update
import android.arch.persistence.room.ext.RoomTypeNames
import android.arch.persistence.room.parser.SQLTypeAffinity
@@ -34,6 +35,8 @@
val MISSING_INSERT_ANNOTATION = "Insertion methods must be annotated with ${Insert::class.java}"
val MISSING_DELETE_ANNOTATION = "Deletion methods must be annotated with ${Delete::class.java}"
val MISSING_UPDATE_ANNOTATION = "Update methods must be annotated with ${Update::class.java}"
+ val MISSING_RAWQUERY_ANNOTATION = "RawQuery methods must be annotated with" +
+ " ${RawQuery::class.java}"
val INVALID_ON_CONFLICT_VALUE = "On conflict value must be one of @OnConflictStrategy values."
val INVALID_INSERTION_METHOD_RETURN_TYPE = "Methods annotated with @Insert can return either" +
" void, long, Long, long[], Long[] or List<Long>."
@@ -144,7 +147,8 @@
val OBSERVABLE_QUERY_NOTHING_TO_OBSERVE = "Observable query return type (LiveData, Flowable" +
" etc) can only be used with SELECT queries that directly or indirectly (via" +
- " @Relation, for example) access at least one table."
+ " @Relation, for example) access at least one table. For @RawQuery, you should" +
+ " specify the list of tables to be observed via the observedEntities field."
val RECURSIVE_REFERENCE_DETECTED = "Recursive referencing through @Embedded and/or @Relation " +
"detected: %s"
@@ -490,4 +494,9 @@
" names"
val INVALID_TABLE_NAME = "Invalid table name. Room does not allow using ` or \" in table names"
+
+ val RAW_QUERY_BAD_PARAMS = "RawQuery methods should have 1 and only 1 parameter with type" +
+ " String or SupportSQLiteQuery"
+
+ val RAW_QUERY_BAD_RETURN_TYPE = "RawQuery methods must return a non-void type."
}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/RawQueryMethodProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/RawQueryMethodProcessor.kt
new file mode 100644
index 0000000..6858ccc
--- /dev/null
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/RawQueryMethodProcessor.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.processor
+
+import android.arch.persistence.room.RawQuery
+import android.arch.persistence.room.Transaction
+import android.arch.persistence.room.ext.SupportDbTypeNames
+import android.arch.persistence.room.ext.hasAnnotation
+import android.arch.persistence.room.ext.toListOfClassTypes
+import android.arch.persistence.room.ext.typeName
+import android.arch.persistence.room.parser.SqlParser
+import android.arch.persistence.room.vo.Entity
+import android.arch.persistence.room.vo.RawQueryMethod
+import com.google.auto.common.AnnotationMirrors
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import com.squareup.javapoet.TypeName
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.type.DeclaredType
+
+class RawQueryMethodProcessor(
+ baseContext: Context,
+ val containing: DeclaredType,
+ val executableElement: ExecutableElement) {
+ val context = baseContext.fork(executableElement)
+ fun process(): RawQueryMethod {
+ val types = context.processingEnv.typeUtils
+ val asMember = types.asMemberOf(containing, executableElement)
+ val executableType = MoreTypes.asExecutable(asMember)
+
+ val annotation = MoreElements.getAnnotationMirror(executableElement,
+ RawQuery::class.java).orNull()
+ context.checker.check(annotation != null, executableElement,
+ ProcessorErrors.MISSING_RAWQUERY_ANNOTATION)
+
+ val returnTypeName = TypeName.get(executableType.returnType)
+ context.checker.notUnbound(returnTypeName, executableElement,
+ ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_QUERY_METHODS)
+ val observedEntities = processObservedTables()
+ val query = SqlParser.rawQueryForTables(
+ observedEntities.map { it.tableName }.toSet())
+ // build the query but don't calculate result info since we just guessed it.
+ val resultBinder = context.typeAdapterStore
+ .findQueryResultBinder(executableType.returnType, query)
+
+ val runtimeQueryParam = findRuntimeQueryParameter()
+ val inTransaction = executableElement.hasAnnotation(Transaction::class)
+ val rawQueryMethod = RawQueryMethod(
+ element = executableElement,
+ name = executableElement.simpleName.toString(),
+ observedEntities = observedEntities,
+ returnType = executableType.returnType,
+ runtimeQueryParam = runtimeQueryParam,
+ inTransaction = inTransaction,
+ queryResultBinder = resultBinder
+ )
+ context.checker.check(rawQueryMethod.returnsValue, executableElement,
+ ProcessorErrors.RAW_QUERY_BAD_RETURN_TYPE)
+ return rawQueryMethod
+ }
+
+ private fun processObservedTables(): List<Entity> {
+ val annotation = MoreElements
+ .getAnnotationMirror(executableElement,
+ android.arch.persistence.room.RawQuery::class.java)
+ .orNull() ?: return emptyList()
+ val entityList = AnnotationMirrors.getAnnotationValue(annotation, "observedEntities")
+ return entityList
+ .toListOfClassTypes()
+ .map {
+ EntityProcessor(
+ baseContext = context,
+ element = MoreTypes.asTypeElement(it)
+ ).process()
+ }
+ }
+
+ private fun findRuntimeQueryParameter(): RawQueryMethod.RuntimeQueryParameter? {
+ val types = context.processingEnv.typeUtils
+ if (executableElement.parameters.size == 1 && !executableElement.isVarArgs) {
+ val param = MoreTypes.asMemberOf(
+ types,
+ containing,
+ executableElement.parameters[0])
+ val elementUtils = context.processingEnv.elementUtils
+ val supportQueryType = elementUtils
+ .getTypeElement(SupportDbTypeNames.QUERY.toString()).asType()
+ val isSupportSql = types.isAssignable(param, supportQueryType)
+ if (isSupportSql) {
+ return RawQueryMethod.RuntimeQueryParameter(
+ paramName = executableElement.parameters[0].simpleName.toString(),
+ type = supportQueryType.typeName())
+ }
+ val stringType = elementUtils.getTypeElement("java.lang.String").asType()
+ val isString = types.isAssignable(param, stringType)
+ if (isString) {
+ return RawQueryMethod.RuntimeQueryParameter(
+ paramName = executableElement.parameters[0].simpleName.toString(),
+ type = stringType.typeName())
+ }
+ }
+ context.logger.e(executableElement, ProcessorErrors.RAW_QUERY_BAD_PARAMS)
+ return null
+ }
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ShortcutParameterProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ShortcutParameterProcessor.kt
index 6ccd12a..7fd6859 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ShortcutParameterProcessor.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ShortcutParameterProcessor.kt
@@ -17,6 +17,7 @@
package android.arch.persistence.room.processor
import android.arch.persistence.room.Entity
+import android.arch.persistence.room.ext.extendsBound
import android.arch.persistence.room.ext.hasAnnotation
import android.arch.persistence.room.vo.ShortcutQueryParameter
import com.google.auto.common.MoreTypes
@@ -60,7 +61,11 @@
fun verifyAndPair(entityType: TypeMirror, isMultiple: Boolean): Pair<TypeMirror?, Boolean> {
if (!MoreTypes.isType(entityType)) {
- return Pair(null, isMultiple)
+ // kotlin may generate ? extends T so we should reduce it.
+ val boundedVar = entityType.extendsBound()
+ return boundedVar?.let {
+ verifyAndPair(boundedVar, isMultiple)
+ } ?: Pair(null, isMultiple)
}
val entityElement = MoreTypes.asElement(entityType)
return if (entityElement.hasAnnotation(Entity::class)) {
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/ObservableQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/ObservableQueryResultBinderProvider.kt
index d64ef4b..4bd52cc 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/ObservableQueryResultBinderProvider.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/ObservableQueryResultBinderProvider.kt
@@ -39,9 +39,9 @@
val adapter = context.typeAdapterStore.findQueryResultAdapter(typeArg, query)
val tableNames = ((adapter?.accessedTableNames() ?: emptyList()) +
query.tables.map { it.name }).toSet()
- context.checker.check(!tableNames.isEmpty(),
- declared.asElement(),
- ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+ if (tableNames.isEmpty()) {
+ context.logger.e(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+ }
return create(
typeArg = typeArg,
resultAdapter = adapter,
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/TypeAdapterStore.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/TypeAdapterStore.kt
index d64300c..c465d4a 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/TypeAdapterStore.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/TypeAdapterStore.kt
@@ -24,8 +24,8 @@
import android.arch.persistence.room.processor.EntityProcessor
import android.arch.persistence.room.processor.FieldProcessor
import android.arch.persistence.room.processor.PojoProcessor
-import android.arch.persistence.room.solver.binderprovider.DataSourceQueryResultBinderProvider
import android.arch.persistence.room.solver.binderprovider.CursorQueryResultBinderProvider
+import android.arch.persistence.room.solver.binderprovider.DataSourceQueryResultBinderProvider
import android.arch.persistence.room.solver.binderprovider.FlowableQueryResultBinderProvider
import android.arch.persistence.room.solver.binderprovider.InstantQueryResultBinderProvider
import android.arch.persistence.room.solver.binderprovider.LiveDataQueryResultBinderProvider
@@ -332,7 +332,7 @@
}
}
- if (rowAdapter != null && !(rowAdapterLogs?.hasErrors() ?: false)) {
+ if (rowAdapter != null && rowAdapterLogs?.hasErrors() != true) {
rowAdapterLogs?.writeTo(context.processingEnv)
return rowAdapter
}
@@ -349,6 +349,21 @@
rowAdapterLogs?.writeTo(context.processingEnv)
return rowAdapter
}
+ if (query.runtimeQueryPlaceholder) {
+ // just go w/ pojo and hope for the best. this happens for @RawQuery where we
+ // try to guess user's intention and hope that their query fits the result.
+ val pojo = PojoProcessor(
+ baseContext = context,
+ element = MoreTypes.asTypeElement(typeMirror),
+ bindingScope = FieldProcessor.BindingScope.READ_FROM_CURSOR,
+ parent = null
+ ).process()
+ return PojoRowAdapter(
+ context = context,
+ info = null,
+ pojo = pojo,
+ out = typeMirror)
+ }
return null
} else {
val singleColumn = findCursorValueReader(typeMirror, null) ?: return null
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/CursorQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/CursorQueryResultBinder.kt
index fede566..7aa24cc 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/CursorQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/CursorQueryResultBinder.kt
@@ -25,6 +25,7 @@
class CursorQueryResultBinder : QueryResultBinder(NO_OP_RESULT_ADAPTER) {
override fun convertAndReturn(roomSQLiteQueryVar: String,
+ canReleaseQuery: Boolean,
dbField: FieldSpec,
inTransaction: Boolean,
scope: CodeGenScope) {
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/FlowableQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/FlowableQueryResultBinder.kt
index f4f1d43..dc8540e 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/FlowableQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/FlowableQueryResultBinder.kt
@@ -38,6 +38,7 @@
adapter: QueryResultAdapter?)
: BaseObservableQueryResultBinder(adapter) {
override fun convertAndReturn(roomSQLiteQueryVar: String,
+ canReleaseQuery: Boolean,
dbField: FieldSpec,
inTransaction: Boolean,
scope: CodeGenScope) {
@@ -55,7 +56,9 @@
dbField = dbField,
scope = scope)
}.build())
- addMethod(createFinalizeMethod(roomSQLiteQueryVar))
+ if (canReleaseQuery) {
+ addMethod(createFinalizeMethod(roomSQLiteQueryVar))
+ }
}.build()
scope.builder().apply {
val tableNamesList = queryTableNames.joinToString(",") { "\"$it\"" }
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/InstantQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/InstantQueryResultBinder.kt
index b9623c1..aa64b1b 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/InstantQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/InstantQueryResultBinder.kt
@@ -28,6 +28,7 @@
*/
class InstantQueryResultBinder(adapter: QueryResultAdapter?) : QueryResultBinder(adapter) {
override fun convertAndReturn(roomSQLiteQueryVar: String,
+ canReleaseQuery: Boolean,
dbField: FieldSpec,
inTransaction: Boolean,
scope: CodeGenScope) {
@@ -49,7 +50,9 @@
}
nextControlFlow("finally").apply {
addStatement("$L.close()", cursorVar)
- addStatement("$L.release()", roomSQLiteQueryVar)
+ if (canReleaseQuery) {
+ addStatement("$L.release()", roomSQLiteQueryVar)
+ }
}
endControlFlow()
}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LiveDataQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LiveDataQueryResultBinder.kt
index 1191ae3..416b8b8 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LiveDataQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LiveDataQueryResultBinder.kt
@@ -42,6 +42,7 @@
@Suppress("JoinDeclarationAndAssignment")
override fun convertAndReturn(
roomSQLiteQueryVar: String,
+ canReleaseQuery: Boolean,
dbField: FieldSpec,
inTransaction: Boolean,
scope: CodeGenScope
@@ -62,7 +63,9 @@
inTransaction = inTransaction,
scope = scope
))
- addMethod(createFinalizeMethod(roomSQLiteQueryVar))
+ if (canReleaseQuery) {
+ addMethod(createFinalizeMethod(roomSQLiteQueryVar))
+ }
}.build()
scope.builder().apply {
addStatement("return $L.getLiveData()", liveDataImpl)
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LivePagedListQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LivePagedListQueryResultBinder.kt
index ceb946e..25d8416 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LivePagedListQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LivePagedListQueryResultBinder.kt
@@ -33,6 +33,7 @@
val typeName = tiledDataSourceQueryResultBinder.itemTypeName
override fun convertAndReturn(
roomSQLiteQueryVar: String,
+ canReleaseQuery: Boolean,
dbField: FieldSpec,
inTransaction: Boolean,
scope: CodeGenScope
@@ -64,6 +65,7 @@
val countedBinderScope = scope.fork()
tiledDataSourceQueryResultBinder.convertAndReturn(
roomSQLiteQueryVar = roomSQLiteQueryVar,
+ canReleaseQuery = true,
dbField = dbField,
inTransaction = inTransaction,
scope = countedBinderScope)
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/PojoRowAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/PojoRowAdapter.kt
index a013dc7..ee6e88b 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/PojoRowAdapter.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/PojoRowAdapter.kt
@@ -38,49 +38,55 @@
* <p>
* The info comes from the query processor so we know about the order of columns in the result etc.
*/
-class PojoRowAdapter(context: Context, val info: QueryResultInfo,
- val pojo: Pojo, out: TypeMirror) : RowAdapter(out) {
+class PojoRowAdapter(
+ context: Context, private val info: QueryResultInfo?,
+ val pojo: Pojo, out: TypeMirror) : RowAdapter(out) {
val mapping: Mapping
val relationCollectors: List<RelationCollector>
init {
// toMutableList documentation is not clear if it copies so lets be safe.
- val remainingFields = pojo.fields.mapTo(mutableListOf<Field>(), { it })
+ val remainingFields = pojo.fields.mapTo(mutableListOf(), { it })
val unusedColumns = arrayListOf<String>()
- val matchedFields = info.columns.map { column ->
- // first check remaining, otherwise check any. maybe developer wants to map the same
- // column into 2 fields. (if they want to post process etc)
- val field = remainingFields.firstOrNull { it.columnName == column.name } ?:
- pojo.fields.firstOrNull { it.columnName == column.name }
- if (field == null) {
- unusedColumns.add(column.name)
- null
- } else {
- remainingFields.remove(field)
- field
+ val matchedFields: List<Field>
+ if (info != null) {
+ matchedFields = info.columns.map { column ->
+ // first check remaining, otherwise check any. maybe developer wants to map the same
+ // column into 2 fields. (if they want to post process etc)
+ val field = remainingFields.firstOrNull { it.columnName == column.name } ?:
+ pojo.fields.firstOrNull { it.columnName == column.name }
+ if (field == null) {
+ unusedColumns.add(column.name)
+ null
+ } else {
+ remainingFields.remove(field)
+ field
+ }
+ }.filterNotNull()
+ if (unusedColumns.isNotEmpty() || remainingFields.isNotEmpty()) {
+ val warningMsg = ProcessorErrors.cursorPojoMismatch(
+ pojoTypeName = pojo.typeName,
+ unusedColumns = unusedColumns,
+ allColumns = info.columns.map { it.name },
+ unusedFields = remainingFields,
+ allFields = pojo.fields
+ )
+ context.logger.w(Warning.CURSOR_MISMATCH, null, warningMsg)
}
- }.filterNotNull()
- if (unusedColumns.isNotEmpty() || remainingFields.isNotEmpty()) {
- val warningMsg = ProcessorErrors.cursorPojoMismatch(
- pojoTypeName = pojo.typeName,
- unusedColumns = unusedColumns,
- allColumns = info.columns.map { it.name },
- unusedFields = remainingFields,
- allFields = pojo.fields
- )
- context.logger.w(Warning.CURSOR_MISMATCH, null, warningMsg)
+ val nonNulls = remainingFields.filter { it.nonNull }
+ if (nonNulls.isNotEmpty()) {
+ context.logger.e(ProcessorErrors.pojoMissingNonNull(
+ pojoTypeName = pojo.typeName,
+ missingPojoFields = nonNulls.map { it.name },
+ allQueryColumns = info.columns.map { it.name }))
+ }
+ if (matchedFields.isEmpty()) {
+ context.logger.e(ProcessorErrors.CANNOT_FIND_QUERY_RESULT_ADAPTER)
+ }
+ } else {
+ matchedFields = remainingFields.map { it }
+ remainingFields.clear()
}
- val nonNulls = remainingFields.filter { it.nonNull }
- if (nonNulls.isNotEmpty()) {
- context.logger.e(ProcessorErrors.pojoMissingNonNull(
- pojoTypeName = pojo.typeName,
- missingPojoFields = nonNulls.map { it.name },
- allQueryColumns = info.columns.map { it.name }))
- }
- if (matchedFields.isEmpty()) {
- context.logger.e(ProcessorErrors.CANNOT_FIND_QUERY_RESULT_ADAPTER)
- }
-
relationCollectors = RelationCollector.createCollectors(context, pojo.relations)
mapping = Mapping(
@@ -105,9 +111,14 @@
relationCollectors.forEach { it.writeInitCode(scope) }
mapping.fieldsWithIndices = mapping.matchedFields.map {
val indexVar = scope.getTmpVar("_cursorIndexOf${it.name.stripNonJava().capitalize()}")
- scope.builder().addStatement("final $T $L = $L.getColumnIndexOrThrow($S)",
- TypeName.INT, indexVar, cursorVarName, it.columnName)
- FieldWithIndex(field = it, indexVar = indexVar, alwaysExists = true)
+ val indexMethod = if (info == null) {
+ "getColumnIndex"
+ } else {
+ "getColumnIndexOrThrow"
+ }
+ scope.builder().addStatement("final $T $L = $L.$L($S)",
+ TypeName.INT, indexVar, cursorVarName, indexMethod, it.columnName)
+ FieldWithIndex(field = it, indexVar = indexVar, alwaysExists = info != null)
}
}
@@ -136,9 +147,10 @@
}
}
- data class Mapping(val matchedFields: List<Field>,
- val unusedColumns: List<String>,
- val unusedFields: List<Field>) {
+ data class Mapping(
+ val matchedFields: List<Field>,
+ val unusedColumns: List<String>,
+ val unusedFields: List<Field>) {
// set when cursor is ready.
lateinit var fieldsWithIndices: List<FieldWithIndex>
}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultBinder.kt
index a77d97f..9fff325 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultBinder.kt
@@ -31,8 +31,10 @@
* receives the sql, bind args and adapter and generates the code that runs the query
* and returns the result.
*/
- abstract fun convertAndReturn(roomSQLiteQueryVar: String,
- dbField: FieldSpec,
- inTransaction: Boolean,
- scope: CodeGenScope)
+ abstract fun convertAndReturn(
+ roomSQLiteQueryVar: String,
+ canReleaseQuery: Boolean, // false if query is provided by the user
+ dbField: FieldSpec,
+ inTransaction: Boolean,
+ scope: CodeGenScope)
}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RxCallableQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RxCallableQueryResultBinder.kt
index 2aba076..1bdd134 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RxCallableQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RxCallableQueryResultBinder.kt
@@ -41,6 +41,7 @@
adapter: QueryResultAdapter?)
: QueryResultBinder(adapter) {
override fun convertAndReturn(roomSQLiteQueryVar: String,
+ canReleaseQuery: Boolean,
dbField: FieldSpec,
inTransaction: Boolean,
scope: CodeGenScope) {
@@ -50,6 +51,7 @@
typeName))
addMethod(createCallMethod(
roomSQLiteQueryVar = roomSQLiteQueryVar,
+ canReleaseQuery = canReleaseQuery,
dbField = dbField,
inTransaction = inTransaction,
scope = scope))
@@ -59,10 +61,11 @@
}
}
- fun createCallMethod(roomSQLiteQueryVar: String,
- dbField: FieldSpec,
- inTransaction: Boolean,
- scope: CodeGenScope): MethodSpec {
+ private fun createCallMethod(roomSQLiteQueryVar: String,
+ canReleaseQuery: Boolean,
+ dbField: FieldSpec,
+ inTransaction: Boolean,
+ scope: CodeGenScope): MethodSpec {
val adapterScope = scope.fork()
return MethodSpec.methodBuilder("call").apply {
returns(typeArg.typeName())
@@ -95,7 +98,9 @@
}
nextControlFlow("finally").apply {
addStatement("$L.close()", cursorVar)
- addStatement("$L.release()", roomSQLiteQueryVar)
+ if (canReleaseQuery) {
+ addStatement("$L.release()", roomSQLiteQueryVar)
+ }
}
endControlFlow()
transactionWrapper?.endTransactionWithControlFlow()
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TiledDataSourceQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TiledDataSourceQueryResultBinder.kt
index 733fdee..19917ff 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TiledDataSourceQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TiledDataSourceQueryResultBinder.kt
@@ -38,6 +38,7 @@
val typeName: ParameterizedTypeName = ParameterizedTypeName.get(
RoomTypeNames.LIMIT_OFFSET_DATA_SOURCE, itemTypeName)
override fun convertAndReturn(roomSQLiteQueryVar: String,
+ canReleaseQuery: Boolean,
dbField: FieldSpec,
inTransaction: Boolean,
scope: CodeGenScope) {
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Dao.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Dao.kt
index d0ef212..9afa44c 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Dao.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Dao.kt
@@ -21,15 +21,18 @@
import javax.lang.model.element.TypeElement
import javax.lang.model.type.DeclaredType
-data class Dao(val element: TypeElement, val type: DeclaredType,
- val queryMethods: List<QueryMethod>,
- val insertionMethods: List<InsertionMethod>,
- val deletionMethods: List<DeletionMethod>,
- val updateMethods: List<UpdateMethod>,
- val transactionMethods: List<TransactionMethod>,
- val constructorParamType: TypeName?) {
+data class Dao(
+ val element: TypeElement, val type: DeclaredType,
+ val queryMethods: List<QueryMethod>,
+ val rawQueryMethods: List<RawQueryMethod>,
+ val insertionMethods: List<InsertionMethod>,
+ val deletionMethods: List<DeletionMethod>,
+ val updateMethods: List<UpdateMethod>,
+ val transactionMethods: List<TransactionMethod>,
+ val constructorParamType: TypeName?) {
// parsed dao might have a suffix if it is used in multiple databases.
private var suffix: String? = null
+
fun setSuffix(newSuffix: String) {
if (this.suffix != null) {
throw IllegalStateException("cannot set suffix twice")
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Database.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Database.kt
index 02e83b3..2cffbf3 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Database.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Database.kt
@@ -56,6 +56,12 @@
* ensure developer didn't forget to update the version.
*/
val identityHash: String by lazy {
+ val idKey = SchemaIdentityKey()
+ idKey.appendSorted(entities)
+ idKey.hash()
+ }
+
+ val legacyIdentityHash: String by lazy {
val entityDescriptions = entities
.sortedBy { it.tableName }
.map { it.createTableQuery }
@@ -71,6 +77,14 @@
fun exportSchema(file: File) {
val schemaBundle = SchemaBundle(SchemaBundle.LATEST_FORMAT, bundle)
+ if (file.exists()) {
+ val existing = file.inputStream().use {
+ SchemaBundle.deserialize(it)
+ }
+ if (existing.isSchemaEqual(schemaBundle)) {
+ return
+ }
+ }
SchemaBundle.serialize(schemaBundle, file)
}
}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Entity.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Entity.kt
index 48592bd..b855f96 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Entity.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Entity.kt
@@ -22,18 +22,30 @@
import javax.lang.model.type.DeclaredType
// TODO make data class when move to kotlin 1.1
-class Entity(element: TypeElement, val tableName: String, type: DeclaredType,
- fields: List<Field>, embeddedFields: List<EmbeddedField>,
- val primaryKey: PrimaryKey, val indices: List<Index>,
- val foreignKeys: List<ForeignKey>,
- constructor: Constructor?)
- : Pojo(element, type, fields, embeddedFields, emptyList(), constructor) {
+class Entity(
+ element: TypeElement, val tableName: String, type: DeclaredType,
+ fields: List<Field>, embeddedFields: List<EmbeddedField>,
+ val primaryKey: PrimaryKey, val indices: List<Index>,
+ val foreignKeys: List<ForeignKey>,
+ constructor: Constructor?)
+ : Pojo(element, type, fields, embeddedFields, emptyList(), constructor), HasSchemaIdentity {
val createTableQuery by lazy {
createTableQuery(tableName)
}
- fun createTableQuery(tableName: String): String {
+ // a string defining the identity of this entity, which can be used for equality checks
+ override fun getIdKey(): String {
+ val identityKey = SchemaIdentityKey()
+ identityKey.append(tableName)
+ identityKey.append(primaryKey)
+ identityKey.appendSorted(fields)
+ identityKey.appendSorted(indices)
+ identityKey.appendSorted(foreignKeys)
+ return identityKey.hash()
+ }
+
+ private fun createTableQuery(tableName: String): String {
val definitions = (fields.map {
val autoIncrement = primaryKey.autoGenerateId && primaryKey.fields.contains(it)
it.databaseDefinition(autoIncrement)
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Field.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Field.kt
index 838016e..43e0605 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Field.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Field.kt
@@ -35,7 +35,7 @@
* embedded child of the main Pojo*/
val parent: EmbeddedField? = null,
// index might be removed when being merged into an Entity
- var indexed: Boolean = false) {
+ var indexed: Boolean = false) : HasSchemaIdentity {
lateinit var getter: FieldGetter
lateinit var setter: FieldSetter
// binds the field into a statement
@@ -47,6 +47,11 @@
/** Whether the table column for this field should be NOT NULL */
val nonNull = element.isNonNull() && (parent == null || parent.isNonNullRecursively())
+ override fun getIdKey(): String {
+ // we don't get the collate information from sqlite so ignoring it here.
+ return "$columnName-${affinity?.name ?: SQLTypeAffinity.TEXT.name}-$nonNull"
+ }
+
/**
* Used when reporting errors on duplicate names
*/
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/ForeignKey.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/ForeignKey.kt
index 66cf3a0..833de97 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/ForeignKey.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/ForeignKey.kt
@@ -26,7 +26,16 @@
val childFields: List<Field>,
val onDelete: ForeignKeyAction,
val onUpdate: ForeignKeyAction,
- val deferred: Boolean) {
+ val deferred: Boolean) : HasSchemaIdentity {
+ override fun getIdKey(): String {
+ return parentTable +
+ "-${parentColumns.joinToString(",")}" +
+ "-${childFields.joinToString(",") {it.columnName}}" +
+ "-${onDelete.sqlName}" +
+ "-${onUpdate.sqlName}" +
+ "-$deferred"
+ }
+
fun databaseDefinition(): String {
return "FOREIGN KEY(${joinEscaped(childFields.map { it.columnName })})" +
" REFERENCES `$parentTable`(${joinEscaped(parentColumns)})" +
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Index.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Index.kt
index 694c627..b4952cf 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Index.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Index.kt
@@ -22,11 +22,17 @@
/**
* Represents a processed index.
*/
-data class Index(val name: String, val unique: Boolean, val fields: List<Field>) {
+data class Index(val name: String, val unique: Boolean, val fields: List<Field>) :
+ HasSchemaIdentity {
companion object {
// should match the value in TableInfo.Index.DEFAULT_PREFIX
const val DEFAULT_PREFIX = "index_"
}
+
+ override fun getIdKey(): String {
+ return "$unique-$name-${fields.joinToString(",") { it.columnName }}"
+ }
+
fun createQuery(tableName: String): String {
val uniqueSQL = if (unique) {
"UNIQUE"
@@ -35,7 +41,7 @@
}
return """
CREATE $uniqueSQL INDEX `$name`
- ON `$tableName` (${fields.map { it.columnName }.joinToString(", ") { "`$it`"}})
+ ON `$tableName` (${fields.map { it.columnName }.joinToString(", ") { "`$it`" }})
""".trimIndent().replace("\n", " ")
}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/PrimaryKey.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/PrimaryKey.kt
index d1a2c21..1d76d40 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/PrimaryKey.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/PrimaryKey.kt
@@ -22,7 +22,7 @@
* Represents a PrimaryKey for an Entity.
*/
data class PrimaryKey(val declaredIn: Element?, val fields: List<Field>,
- val autoGenerateId: Boolean) {
+ val autoGenerateId: Boolean) : HasSchemaIdentity {
companion object {
val MISSING = PrimaryKey(null, emptyList(), false)
}
@@ -36,4 +36,8 @@
fun toBundle(): PrimaryKeyBundle = PrimaryKeyBundle(
autoGenerateId, fields.map { it.columnName })
+
+ override fun getIdKey(): String {
+ return "$autoGenerateId-${fields.map { it.columnName }}"
+ }
}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/RawQueryMethod.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/RawQueryMethod.kt
new file mode 100644
index 0000000..ad1f10d
--- /dev/null
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/RawQueryMethod.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.vo
+
+import android.arch.persistence.room.ext.CommonTypeNames
+import android.arch.persistence.room.ext.SupportDbTypeNames
+import android.arch.persistence.room.ext.typeName
+import android.arch.persistence.room.solver.query.result.QueryResultBinder
+import com.squareup.javapoet.TypeName
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.type.TypeMirror
+
+/**
+ * A class that holds information about a method annotated with RawQuery.
+ * It is self sufficient and must have all generics etc resolved once created.
+ */
+data class RawQueryMethod(
+ val element: ExecutableElement,
+ val name: String,
+ val returnType: TypeMirror,
+ val inTransaction: Boolean,
+ val observedEntities: List<Entity>,
+ val runtimeQueryParam: RuntimeQueryParameter?,
+ val queryResultBinder: QueryResultBinder) {
+ val returnsValue by lazy {
+ returnType.typeName() != TypeName.VOID
+ }
+
+ data class RuntimeQueryParameter(
+ val paramName: String,
+ val type: TypeName) {
+ fun isString() = CommonTypeNames.STRING == type
+ fun isSupportQuery() = SupportDbTypeNames.QUERY == type
+ }
+}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/SchemaIdentityKey.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/SchemaIdentityKey.kt
new file mode 100644
index 0000000..06c9ff5
--- /dev/null
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/SchemaIdentityKey.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.vo
+
+import org.apache.commons.codec.digest.DigestUtils
+import java.util.Locale
+
+interface HasSchemaIdentity {
+ fun getIdKey(): String
+}
+
+/**
+ * A class that can be converted into a unique identifier for an object
+ */
+class SchemaIdentityKey {
+ companion object {
+ private val SEPARATOR = "?:?"
+ private val ENGLISH_SORT = Comparator<String> { o1, o2 ->
+ o1.toLowerCase(Locale.ENGLISH).compareTo(o2.toLowerCase(Locale.ENGLISH))
+ }
+ }
+
+ private val sb = StringBuilder()
+ fun append(identity: HasSchemaIdentity) {
+ append(identity.getIdKey())
+ }
+
+ fun appendSorted(identities: List<HasSchemaIdentity>) {
+ identities.map { it.getIdKey() }.sortedWith(ENGLISH_SORT).forEach {
+ append(it)
+ }
+ }
+
+ fun hash() = DigestUtils.md5Hex(sb.toString())
+ fun append(identity: String) {
+ sb.append(identity).append(SEPARATOR)
+ }
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/ClassWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/ClassWriter.kt
index 8cf511f..ebd0adf 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/ClassWriter.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/ClassWriter.kt
@@ -112,11 +112,11 @@
abstract class SharedMethodSpec(val baseName: String) {
abstract fun getUniqueKey(): String
- abstract fun prepare(writer: ClassWriter, builder: MethodSpec.Builder)
+ abstract fun prepare(methodName: String, writer: ClassWriter, builder: MethodSpec.Builder)
fun build(writer: ClassWriter, name: String): MethodSpec {
val builder = MethodSpec.methodBuilder(name)
- prepare(writer, builder)
+ prepare(name, writer, builder)
return builder.build()
}
}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt
index 5125444..4e319a9 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt
@@ -29,6 +29,7 @@
import android.arch.persistence.room.vo.Entity
import android.arch.persistence.room.vo.InsertionMethod
import android.arch.persistence.room.vo.QueryMethod
+import android.arch.persistence.room.vo.RawQueryMethod
import android.arch.persistence.room.vo.ShortcutMethod
import android.arch.persistence.room.vo.TransactionMethod
import com.google.auto.common.MoreTypes
@@ -55,6 +56,7 @@
class DaoWriter(val dao: Dao, val processingEnv: ProcessingEnvironment)
: ClassWriter(dao.typeName) {
val declaredDao = MoreTypes.asDeclared(dao.element.asType())
+
companion object {
// TODO nothing prevents this from conflicting, we should fix.
val dbField: FieldSpec = FieldSpec
@@ -111,6 +113,9 @@
oneOffDeleteOrUpdateQueries.forEach {
addMethod(createDeleteOrUpdateQueryMethod(it))
}
+ dao.rawQueryMethods.forEach {
+ addMethod(createRawQueryMethod(it))
+ }
}
return builder
}
@@ -194,8 +199,9 @@
return methodBuilder.build()
}
- private fun MethodSpec.Builder.addDelegateToSuperStatement(element: ExecutableElement,
- result: String?) {
+ private fun MethodSpec.Builder.addDelegateToSuperStatement(
+ element: ExecutableElement,
+ result: String?) {
val params: MutableList<Any> = mutableListOf()
val format = buildString {
if (result != null) {
@@ -220,9 +226,10 @@
addStatement(format, *params.toTypedArray())
}
- private fun createConstructor(dbParam: ParameterSpec,
- shortcutMethods: List<PreparedStmtQuery>,
- callSuper: Boolean): MethodSpec {
+ private fun createConstructor(
+ dbParam: ParameterSpec,
+ shortcutMethods: List<PreparedStmtQuery>,
+ callSuper: Boolean): MethodSpec {
return MethodSpec.constructorBuilder().apply {
addParameter(dbParam)
addModifiers(PUBLIC)
@@ -250,6 +257,52 @@
}.build()
}
+ private fun createRawQueryMethod(method: RawQueryMethod): MethodSpec {
+ return overrideWithoutAnnotations(method.element, declaredDao).apply {
+ val scope = CodeGenScope(this@DaoWriter)
+ val roomSQLiteQueryVar: String
+ val queryParam = method.runtimeQueryParam
+ val shouldReleaseQuery: Boolean
+
+ when {
+ queryParam?.isString() == true -> {
+ roomSQLiteQueryVar = scope.getTmpVar("_statement")
+ shouldReleaseQuery = true
+ addStatement("$T $L = $T.acquire($L, 0)",
+ RoomTypeNames.ROOM_SQL_QUERY,
+ roomSQLiteQueryVar,
+ RoomTypeNames.ROOM_SQL_QUERY,
+ queryParam.paramName)
+ }
+ queryParam?.isSupportQuery() == true -> {
+ shouldReleaseQuery = false
+ roomSQLiteQueryVar = queryParam.paramName
+ }
+ else -> {
+ // try to generate compiling code. we would've already reported this error
+ roomSQLiteQueryVar = scope.getTmpVar("_statement")
+ shouldReleaseQuery = false
+ addStatement("$T $L = $T.acquire($L, 0)",
+ RoomTypeNames.ROOM_SQL_QUERY,
+ roomSQLiteQueryVar,
+ RoomTypeNames.ROOM_SQL_QUERY,
+ "missing query parameter")
+ }
+ }
+ if (method.returnsValue) {
+ // don't generate code because it will create 1 more error. The original error is
+ // already reported by the processor.
+ method.queryResultBinder.convertAndReturn(
+ roomSQLiteQueryVar = roomSQLiteQueryVar,
+ canReleaseQuery = shouldReleaseQuery,
+ dbField = dbField,
+ inTransaction = method.inTransaction,
+ scope = scope)
+ }
+ addCode(scope.builder().build())
+ }.build()
+ }
+
private fun createDeleteOrUpdateQueryMethod(method: QueryMethod): MethodSpec {
return overrideWithoutAnnotations(method.element, declaredDao).apply {
addCode(createDeleteOrUpdateQueryMethodBody(method))
@@ -451,14 +504,16 @@
queryWriter.prepareReadAndBind(sqlVar, roomSQLiteQueryVar, scope)
method.queryResultBinder.convertAndReturn(
roomSQLiteQueryVar = roomSQLiteQueryVar,
+ canReleaseQuery = true,
dbField = dbField,
inTransaction = method.inTransaction,
scope = scope)
return scope.builder().build()
}
- private fun overrideWithoutAnnotations(elm: ExecutableElement,
- owner: DeclaredType): MethodSpec.Builder {
+ private fun overrideWithoutAnnotations(
+ elm: ExecutableElement,
+ owner: DeclaredType): MethodSpec.Builder {
val baseSpec = MethodSpec.overriding(elm, owner, processingEnv.typeUtils).build()
return MethodSpec.methodBuilder(baseSpec.name).apply {
addAnnotation(Override::class.java)
@@ -477,8 +532,9 @@
* declaration to definition.
* @param methodImpl The body of the query method implementation.
*/
- data class PreparedStmtQuery(val fields: Map<String, Pair<FieldSpec, TypeSpec>>,
- val methodImpl: MethodSpec) {
+ data class PreparedStmtQuery(
+ val fields: Map<String, Pair<FieldSpec, TypeSpec>>,
+ val methodImpl: MethodSpec) {
companion object {
// The key to be used in `fields` where the method requires a field that is not
// associated with any of its parameters
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/EntityCursorConverterWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/EntityCursorConverterWriter.kt
index a96e23c..b9d6aec 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/EntityCursorConverterWriter.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/EntityCursorConverterWriter.kt
@@ -22,8 +22,8 @@
import android.arch.persistence.room.ext.S
import android.arch.persistence.room.ext.T
import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.vo.Entity
import android.arch.persistence.room.vo.EmbeddedField
+import android.arch.persistence.room.vo.Entity
import android.arch.persistence.room.vo.FieldWithIndex
import com.squareup.javapoet.CodeBlock
import com.squareup.javapoet.MethodSpec
@@ -38,7 +38,7 @@
return "generic_entity_converter_of_${entity.element.qualifiedName}"
}
- override fun prepare(writer: ClassWriter, builder: MethodSpec.Builder) {
+ override fun prepare(methodName: String, writer: ClassWriter, builder: MethodSpec.Builder) {
builder.apply {
val cursorParam = ParameterSpec
.builder(AndroidTypeNames.CURSOR, "cursor").build()
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/RelationCollectorMethodWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/RelationCollectorMethodWriter.kt
index 71f0510..ecd9b59 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/RelationCollectorMethodWriter.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/RelationCollectorMethodWriter.kt
@@ -19,6 +19,7 @@
import android.arch.persistence.room.ext.AndroidTypeNames
import android.arch.persistence.room.ext.L
import android.arch.persistence.room.ext.N
+import android.arch.persistence.room.ext.RoomTypeNames
import android.arch.persistence.room.ext.S
import android.arch.persistence.room.ext.T
import android.arch.persistence.room.solver.CodeGenScope
@@ -34,7 +35,7 @@
/**
* Writes the method that fetches the relations of a POJO and assigns them into the given map.
*/
-class RelationCollectorMethodWriter(val collector: RelationCollector)
+class RelationCollectorMethodWriter(private val collector: RelationCollector)
: ClassWriter.SharedMethodSpec(
"fetchRelationship${collector.relation.entity.tableName.stripNonJava()}" +
"As${collector.relation.pojoTypeName.toString().stripNonJava()}") {
@@ -51,7 +52,7 @@
"-${relation.createLoadAllSql()}"
}
- override fun prepare(writer: ClassWriter, builder: MethodSpec.Builder) {
+ override fun prepare(methodName: String, writer: ClassWriter, builder: MethodSpec.Builder) {
val scope = CodeGenScope(writer)
val relation = collector.relation
@@ -74,6 +75,41 @@
addStatement("return")
}
endControlFlow()
+ addStatement("// check if the size is too big, if so divide")
+ beginControlFlow("if($N.size() > $T.MAX_BIND_PARAMETER_CNT)",
+ param, RoomTypeNames.ROOM_DB).apply {
+ // divide it into chunks
+ val tmpMapVar = scope.getTmpVar("_tmpInnerMap")
+ addStatement("$T $L = new $T($L.MAX_BIND_PARAMETER_CNT)",
+ collector.mapTypeName, tmpMapVar,
+ collector.mapTypeName, RoomTypeNames.ROOM_DB)
+ val mapIndexVar = scope.getTmpVar("_mapIndex")
+ val tmpIndexVar = scope.getTmpVar("_tmpIndex")
+ val limitVar = scope.getTmpVar("_limit")
+ addStatement("$T $L = 0", TypeName.INT, mapIndexVar)
+ addStatement("$T $L = 0", TypeName.INT, tmpIndexVar)
+ addStatement("final $T $L = $N.size()", TypeName.INT, limitVar, param)
+ beginControlFlow("while($L < $L)", mapIndexVar, limitVar).apply {
+ addStatement("$L.put($N.keyAt($L), $N.valueAt($L))",
+ tmpMapVar, param, mapIndexVar, param, mapIndexVar)
+ addStatement("$L++", mapIndexVar)
+ addStatement("$L++", tmpIndexVar)
+ beginControlFlow("if($L == $T.MAX_BIND_PARAMETER_CNT)",
+ tmpIndexVar, RoomTypeNames.ROOM_DB).apply {
+ // recursively load that batch
+ addStatement("$L($L)", methodName, tmpMapVar)
+ // clear nukes the backing data hence we create a new one
+ addStatement("$L = new $T($T.MAX_BIND_PARAMETER_CNT)",
+ tmpMapVar, collector.mapTypeName, RoomTypeNames.ROOM_DB)
+ addStatement("$L = 0", tmpIndexVar)
+ }.endControlFlow()
+ }.endControlFlow()
+ beginControlFlow("if($L > 0)", tmpIndexVar).apply {
+ // load the last batch
+ addStatement("$L($L)", methodName, tmpMapVar)
+ }.endControlFlow()
+ addStatement("return")
+ }.endControlFlow()
collector.queryWriter.prepareReadAndBind(sqlQueryVar, stmtVar, scope)
addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR, cursorVar,
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/SQLiteOpenHelperWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/SQLiteOpenHelperWriter.kt
index 16fcd9c..d62cc1c 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/SQLiteOpenHelperWriter.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/SQLiteOpenHelperWriter.kt
@@ -40,10 +40,10 @@
scope.builder().apply {
val sqliteConfigVar = scope.getTmpVar("_sqliteConfig")
val callbackVar = scope.getTmpVar("_openCallback")
- addStatement("final $T $L = new $T($N, $L, $S)",
+ addStatement("final $T $L = new $T($N, $L, $S, $S)",
SupportDbTypeNames.SQLITE_OPEN_HELPER_CALLBACK,
callbackVar, RoomTypeNames.OPEN_HELPER, configuration,
- createOpenCallback(scope), database.identityHash)
+ createOpenCallback(scope), database.identityHash, database.legacyIdentityHash)
// build configuration
addStatement(
"""
diff --git a/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java b/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
index cfdc110..db2b450 100644
--- a/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
+++ b/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
@@ -28,7 +28,7 @@
public void createAllTables(SupportSQLiteDatabase _db) {
_db.execSQL("CREATE TABLE IF NOT EXISTS `User` (`uid` INTEGER NOT NULL, `name` TEXT, `lastName` TEXT, `ageColumn` INTEGER NOT NULL, PRIMARY KEY(`uid`))");
_db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
- _db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"6773601c5bcf94c71ee4eb0de04f21a4\")");
+ _db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"cd8098a1e968898879c194cef2dff8f7\")");
}
public void dropAllTables(SupportSQLiteDatabase _db) {
@@ -69,7 +69,7 @@
+ " Found:\n" + _existingUser);
}
}
- }, "6773601c5bcf94c71ee4eb0de04f21a4");
+ }, "cd8098a1e968898879c194cef2dff8f7", "6773601c5bcf94c71ee4eb0de04f21a4");
final SupportSQLiteOpenHelper.Configuration _sqliteConfig = SupportSQLiteOpenHelper.Configuration.builder(configuration.context)
.name(configuration.name)
.callback(_openCallback)
@@ -96,4 +96,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/RawQueryMethodProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/RawQueryMethodProcessorTest.kt
new file mode 100644
index 0000000..3eb629a
--- /dev/null
+++ b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/RawQueryMethodProcessorTest.kt
@@ -0,0 +1,246 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.processor
+
+import COMMON
+import android.arch.persistence.room.ColumnInfo
+import android.arch.persistence.room.Dao
+import android.arch.persistence.room.Entity
+import android.arch.persistence.room.PrimaryKey
+import android.arch.persistence.room.Query
+import android.arch.persistence.room.RawQuery
+import android.arch.persistence.room.ext.CommonTypeNames
+import android.arch.persistence.room.ext.SupportDbTypeNames
+import android.arch.persistence.room.ext.hasAnnotation
+import android.arch.persistence.room.ext.typeName
+import android.arch.persistence.room.testing.TestInvocation
+import android.arch.persistence.room.testing.TestProcessor
+import android.arch.persistence.room.vo.RawQueryMethod
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import com.google.common.truth.Truth
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourcesSubjectFactory
+import com.squareup.javapoet.ArrayTypeName
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.TypeName
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+
+class RawQueryMethodProcessorTest {
+ @Test
+ fun supportRawQuery() {
+ singleQueryMethod(
+ """
+ @RawQuery
+ abstract public int[] foo(SupportSQLiteQuery query);
+ """) { query, _ ->
+ assertThat(query.name, `is`("foo"))
+ assertThat(query.runtimeQueryParam, `is`(
+ RawQueryMethod.RuntimeQueryParameter(
+ paramName = "query",
+ type = SupportDbTypeNames.QUERY
+ )
+ ))
+ assertThat(query.returnType.typeName(),
+ `is`(ArrayTypeName.of(TypeName.INT) as TypeName))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun stringRawQuery() {
+ singleQueryMethod(
+ """
+ @RawQuery
+ abstract public int[] foo(String query);
+ """) { query, _ ->
+ assertThat(query.name, `is`("foo"))
+ assertThat(query.runtimeQueryParam, `is`(
+ RawQueryMethod.RuntimeQueryParameter(
+ paramName = "query",
+ type = CommonTypeNames.STRING
+ )
+ ))
+ assertThat(query.returnType.typeName(),
+ `is`(ArrayTypeName.of(TypeName.INT) as TypeName))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun withObservedEntities() {
+ singleQueryMethod(
+ """
+ @RawQuery(observedEntities = User.class)
+ abstract public LiveData<User> foo(SupportSQLiteQuery query);
+ """) { query, _ ->
+ assertThat(query.name, `is`("foo"))
+ assertThat(query.runtimeQueryParam, `is`(
+ RawQueryMethod.RuntimeQueryParameter(
+ paramName = "query",
+ type = SupportDbTypeNames.QUERY
+ )
+ ))
+ assertThat(query.observedEntities.size, `is`(1))
+ assertThat(
+ query.observedEntities.first().typeName,
+ `is`(COMMON.USER_TYPE_NAME as TypeName))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun observableWithoutEntities() {
+ singleQueryMethod(
+ """
+ @RawQuery(observedEntities = {})
+ abstract public LiveData<User> foo(SupportSQLiteQuery query);
+ """) { query, _ ->
+ assertThat(query.name, `is`("foo"))
+ assertThat(query.runtimeQueryParam, `is`(
+ RawQueryMethod.RuntimeQueryParameter(
+ paramName = "query",
+ type = SupportDbTypeNames.QUERY
+ )
+ ))
+ assertThat(query.observedEntities, `is`(emptyList()))
+ }.failsToCompile()
+ .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+ }
+
+ @Test
+ fun pojo() {
+ val pojo: TypeName = ClassName.get("foo.bar.MyClass", "MyPojo")
+ singleQueryMethod(
+ """
+ public class MyPojo {
+ public String foo;
+ public String bar;
+ }
+
+ @RawQuery
+ abstract public MyPojo foo(SupportSQLiteQuery query);
+ """) { query, _ ->
+ assertThat(query.name, `is`("foo"))
+ assertThat(query.runtimeQueryParam, `is`(
+ RawQueryMethod.RuntimeQueryParameter(
+ paramName = "query",
+ type = SupportDbTypeNames.QUERY
+ )
+ ))
+ assertThat(query.returnType.typeName(), `is`(pojo))
+ assertThat(query.observedEntities, `is`(emptyList()))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun void() {
+ singleQueryMethod(
+ """
+ @RawQuery
+ abstract public void foo(SupportSQLiteQuery query);
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RAW_QUERY_BAD_RETURN_TYPE
+ )
+ }
+
+ @Test
+ fun noArgs() {
+ singleQueryMethod(
+ """
+ @RawQuery
+ abstract public int[] foo();
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RAW_QUERY_BAD_PARAMS
+ )
+ }
+
+ @Test
+ fun tooManyArgs() {
+ singleQueryMethod(
+ """
+ @RawQuery
+ abstract public int[] foo(String query, String query2);
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RAW_QUERY_BAD_PARAMS
+ )
+ }
+
+ @Test
+ fun varargs() {
+ singleQueryMethod(
+ """
+ @RawQuery
+ abstract public int[] foo(String... query);
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RAW_QUERY_BAD_PARAMS
+ )
+ }
+
+ private fun singleQueryMethod(
+ vararg input: String,
+ handler: (RawQueryMethod, TestInvocation) -> Unit
+ ): CompileTester {
+ return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+ .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyClass",
+ DAO_PREFIX
+ + input.joinToString("\n")
+ + DAO_SUFFIX
+ ), COMMON.LIVE_DATA, COMMON.COMPUTABLE_LIVE_DATA, COMMON.USER))
+ .processedWith(TestProcessor.builder()
+ .forAnnotations(Query::class, Dao::class, ColumnInfo::class,
+ Entity::class, PrimaryKey::class, RawQueryMethod::class)
+ .nextRunHandler { invocation ->
+ val (owner, methods) = invocation.roundEnv
+ .getElementsAnnotatedWith(Dao::class.java)
+ .map {
+ Pair(it,
+ invocation.processingEnv.elementUtils
+ .getAllMembers(MoreElements.asType(it))
+ .filter {
+ it.hasAnnotation(RawQuery::class)
+ }
+ )
+ }.first { it.second.isNotEmpty() }
+ val parser = RawQueryMethodProcessor(
+ baseContext = invocation.context,
+ containing = MoreTypes.asDeclared(owner.asType()),
+ executableElement = MoreElements.asExecutable(methods.first()))
+ val parsedQuery = parser.process()
+ handler(parsedQuery, invocation)
+ true
+ }
+ .build())
+ }
+
+ companion object {
+ private const val DAO_PREFIX = """
+ package foo.bar;
+ import android.support.annotation.NonNull;
+ import android.arch.persistence.room.*;
+ import android.arch.persistence.db.SupportSQLiteQuery;
+ import android.arch.lifecycle.LiveData;
+ @Dao
+ abstract class MyClass {
+ """
+ private const val DAO_SUFFIX = "}"
+ }
+}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/BaseDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/BaseDao.kt
index 60d97ed..a80e840 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/BaseDao.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/BaseDao.kt
@@ -26,6 +26,12 @@
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(t: T)
+ @Insert
+ fun insertAll(t: List<T>)
+
+ @Insert
+ fun insertAllArg(vararg t: T)
+
@Update
fun update(t: T)
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/BooksDaoTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/BooksDaoTest.kt
index 77deca7..3f58eee 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/BooksDaoTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/BooksDaoTest.kt
@@ -49,9 +49,9 @@
booksDao.addPublishers(TestUtil.PUBLISHER)
booksDao.addBooks(TestUtil.BOOK_1)
- var expected = BookWithPublisher(TestUtil.BOOK_1.bookId, TestUtil.BOOK_1.title,
+ val expected = BookWithPublisher(TestUtil.BOOK_1.bookId, TestUtil.BOOK_1.title,
TestUtil.PUBLISHER)
- var expectedList = ArrayList<BookWithPublisher>()
+ val expectedList = ArrayList<BookWithPublisher>()
expectedList.add(expected)
assertThat(database.booksDao().getBooksWithPublisher(),
@@ -153,4 +153,22 @@
assertThat(booksDao.findByLanguages(setOf(Lang.EN)),
`is`(listOf(book3)))
}
+
+ @Test
+ fun insertVarargInInheritedDao() {
+ database.derivedDao().insertAllArg(TestUtil.AUTHOR_1, TestUtil.AUTHOR_2)
+
+ val author = database.derivedDao().getAuthor(TestUtil.AUTHOR_1.authorId)
+
+ assertThat(author, CoreMatchers.`is`<Author>(TestUtil.AUTHOR_1))
+ }
+
+ @Test
+ fun insertListInInheritedDao() {
+ database.derivedDao().insertAll(listOf(TestUtil.AUTHOR_1))
+
+ val author = database.derivedDao().getAuthor(TestUtil.AUTHOR_1.authorId)
+
+ assertThat(author, CoreMatchers.`is`<Author>(TestUtil.AUTHOR_1))
+ }
}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/TestDatabase.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/TestDatabase.java
index 610afb2..98282ab 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/TestDatabase.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/TestDatabase.java
@@ -25,6 +25,7 @@
import android.arch.persistence.room.integration.testapp.dao.PetCoupleDao;
import android.arch.persistence.room.integration.testapp.dao.PetDao;
import android.arch.persistence.room.integration.testapp.dao.ProductDao;
+import android.arch.persistence.room.integration.testapp.dao.RawDao;
import android.arch.persistence.room.integration.testapp.dao.SchoolDao;
import android.arch.persistence.room.integration.testapp.dao.SpecificDogDao;
import android.arch.persistence.room.integration.testapp.dao.ToyDao;
@@ -61,6 +62,7 @@
public abstract SpecificDogDao getSpecificDogDao();
public abstract WithClauseDao getWithClauseDao();
public abstract FunnyNamedDao getFunnyNamedDao();
+ public abstract RawDao getRawDao();
@SuppressWarnings("unused")
public static class Converters {
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/RawDao.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/RawDao.java
new file mode 100644
index 0000000..b4469c0
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/RawDao.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.integration.testapp.dao;
+
+import android.arch.lifecycle.LiveData;
+import android.arch.persistence.db.SupportSQLiteQuery;
+import android.arch.persistence.room.ColumnInfo;
+import android.arch.persistence.room.Dao;
+import android.arch.persistence.room.RawQuery;
+import android.arch.persistence.room.integration.testapp.vo.NameAndLastName;
+import android.arch.persistence.room.integration.testapp.vo.User;
+import android.arch.persistence.room.integration.testapp.vo.UserAndAllPets;
+import android.arch.persistence.room.integration.testapp.vo.UserAndPet;
+
+import java.util.Date;
+import java.util.List;
+
+@Dao
+public interface RawDao {
+ @RawQuery
+ User getUser(String query);
+ @RawQuery
+ UserAndAllPets getUserAndAllPets(String query);
+ @RawQuery
+ User getUser(SupportSQLiteQuery query);
+ @RawQuery
+ UserAndPet getUserAndPet(String query);
+ @RawQuery
+ NameAndLastName getUserNameAndLastName(String query);
+ @RawQuery(observedEntities = User.class)
+ NameAndLastName getUserNameAndLastName(SupportSQLiteQuery query);
+ @RawQuery
+ int count(String query);
+ @RawQuery
+ List<User> getUserList(String query);
+ @RawQuery
+ List<UserAndPet> getUserAndPetList(String query);
+ @RawQuery(observedEntities = User.class)
+ LiveData<User> getUserLiveData(String query);
+ @RawQuery
+ UserNameAndBirthday getUserAndBirthday(String query);
+ class UserNameAndBirthday {
+ @ColumnInfo(name = "mName")
+ public final String name;
+ @ColumnInfo(name = "mBirthday")
+ public final Date birthday;
+
+ public UserNameAndBirthday(String name, Date birthday) {
+ this.name = name;
+ this.birthday = birthday;
+ }
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
index 7fe2bc9..c850a4d 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
@@ -17,9 +17,13 @@
package android.arch.persistence.room.integration.testapp.migration;
import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.endsWith;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.MatcherAssert.assertThat;
import android.arch.persistence.db.SupportSQLiteDatabase;
@@ -33,6 +37,7 @@
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
+import org.hamcrest.MatcherAssert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -252,6 +257,95 @@
db.close();
}
+ @Test
+ public void failWithIdentityCheck() throws IOException {
+ for (int i = 1; i < MigrationDb.LATEST_VERSION; i++) {
+ String name = "test_" + i;
+ helper.createDatabase(name, i).close();
+ IllegalStateException exception = null;
+ try {
+ MigrationDb db = Room.databaseBuilder(
+ InstrumentationRegistry.getInstrumentation().getTargetContext(),
+ MigrationDb.class, name).build();
+ db.runInTransaction(new Runnable() {
+ @Override
+ public void run() {
+ // do nothing
+ }
+ });
+ } catch (IllegalStateException ex) {
+ exception = ex;
+ }
+ MatcherAssert.assertThat("identity detection should've failed",
+ exception, notNullValue());
+ }
+ }
+
+ @Test
+ public void fallbackToDestructiveMigrationFrom_destructiveMigrationOccursForSuppliedVersion()
+ throws IOException {
+ SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 6);
+ final MigrationDb.Dao_V1 dao = new MigrationDb.Dao_V1(database);
+ dao.insertIntoEntity1(2, "foo");
+ dao.insertIntoEntity1(3, "bar");
+ database.close();
+ Context targetContext = InstrumentationRegistry.getTargetContext();
+
+ MigrationDb db = Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
+ .fallbackToDestructiveMigrationFrom(6)
+ .build();
+
+ assertThat(db.dao().loadAllEntity1s().size(), is(0));
+ }
+
+ @Test
+ public void fallbackToDestructiveMigrationFrom_suppliedValueIsMigrationStartVersion_exception()
+ throws IOException {
+ SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 6);
+ database.close();
+ Context targetContext = InstrumentationRegistry.getTargetContext();
+
+ Throwable throwable = null;
+ try {
+ Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
+ .addMigrations(MIGRATION_6_7)
+ .fallbackToDestructiveMigrationFrom(6)
+ .build();
+ } catch (Throwable t) {
+ throwable = t;
+ }
+
+ assertThat(throwable, is(not(nullValue())));
+ //noinspection ConstantConditions
+ assertThat(throwable.getMessage(),
+ startsWith("Inconsistency detected. A Migration was supplied to"));
+ assertThat(throwable.getMessage(), endsWith("6"));
+ }
+
+ @Test
+ public void fallbackToDestructiveMigrationFrom_suppliedValueIsMigrationEndVersion_exception()
+ throws IOException {
+ SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 5);
+ database.close();
+ Context targetContext = InstrumentationRegistry.getTargetContext();
+
+ Throwable throwable = null;
+ try {
+ Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
+ .addMigrations(MIGRATION_5_6)
+ .fallbackToDestructiveMigrationFrom(6)
+ .build();
+ } catch (Throwable t) {
+ throwable = t;
+ }
+
+ assertThat(throwable, is(not(nullValue())));
+ //noinspection ConstantConditions
+ assertThat(throwable.getMessage(),
+ startsWith("Inconsistency detected. A Migration was supplied to"));
+ assertThat(throwable.getMessage(), endsWith("6"));
+ }
+
private void testFailure(int startVersion, int endVersion) throws IOException {
final SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, startVersion);
db.close();
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 f0285a0..7a2faf0 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
@@ -16,7 +16,7 @@
package android.arch.persistence.room.integration.testapp.paging;
-import static android.test.MoreAsserts.assertEmpty;
+import static junit.framework.Assert.assertFalse;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -79,7 +79,7 @@
List<User> initial = dataSource.loadRange(0, 10);
assertThat(initial.get(0), is(users.get(0)));
- assertEmpty(dataSource.loadRange(1, 10));
+ assertFalse(dataSource.loadRange(1, 10).iterator().hasNext());
}
@Test
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/CollationTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/CollationTest.java
new file mode 100644
index 0000000..7b0e933
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/CollationTest.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.integration.testapp.test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.arch.persistence.room.ColumnInfo;
+import android.arch.persistence.room.Dao;
+import android.arch.persistence.room.Database;
+import android.arch.persistence.room.Insert;
+import android.arch.persistence.room.PrimaryKey;
+import android.arch.persistence.room.Query;
+import android.arch.persistence.room.Room;
+import android.arch.persistence.room.RoomDatabase;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.hamcrest.CoreMatchers;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CollationTest {
+ private CollateDb mDb;
+ private CollateDao mDao;
+ private Locale mDefaultLocale;
+ private final CollateEntity mItem1 = new CollateEntity(1, "abı");
+ private final CollateEntity mItem2 = new CollateEntity(2, "abi");
+ private final CollateEntity mItem3 = new CollateEntity(3, "abj");
+ private final CollateEntity mItem4 = new CollateEntity(4, "abç");
+
+ @Before
+ public void init() {
+ mDefaultLocale = Locale.getDefault();
+ }
+
+ private void initDao(Locale systemLocale) {
+ Locale.setDefault(systemLocale);
+ mDb = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(),
+ CollateDb.class).build();
+ mDao = mDb.dao();
+ mDao.insert(mItem1);
+ mDao.insert(mItem2);
+ mDao.insert(mItem3);
+ mDao.insert(mItem4);
+ }
+
+ @After
+ public void closeDb() {
+ mDb.close();
+ Locale.setDefault(mDefaultLocale);
+ }
+
+ @Test
+ public void localized() {
+ initDao(new Locale("tr", "TR"));
+ List<CollateEntity> result = mDao.sortedByLocalized();
+ assertThat(result, CoreMatchers.is(Arrays.asList(
+ mItem4, mItem1, mItem2, mItem3
+ )));
+ }
+
+ @Test
+ public void localized_asUnicode() {
+ initDao(Locale.getDefault());
+ List<CollateEntity> result = mDao.sortedByLocalizedAsUnicode();
+ assertThat(result, CoreMatchers.is(Arrays.asList(
+ mItem4, mItem2, mItem1, mItem3
+ )));
+ }
+
+ @Test
+ public void unicode_asLocalized() {
+ initDao(new Locale("tr", "TR"));
+ List<CollateEntity> result = mDao.sortedByUnicodeAsLocalized();
+ assertThat(result, CoreMatchers.is(Arrays.asList(
+ mItem4, mItem1, mItem2, mItem3
+ )));
+ }
+
+ @Test
+ public void unicode() {
+ initDao(Locale.getDefault());
+ List<CollateEntity> result = mDao.sortedByUnicode();
+ assertThat(result, CoreMatchers.is(Arrays.asList(
+ mItem4, mItem2, mItem1, mItem3
+ )));
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @android.arch.persistence.room.Entity
+ static class CollateEntity {
+ @PrimaryKey
+ public final int id;
+ @ColumnInfo(collate = ColumnInfo.LOCALIZED)
+ public final String localizedName;
+ @ColumnInfo(collate = ColumnInfo.UNICODE)
+ public final String unicodeName;
+
+ CollateEntity(int id, String name) {
+ this.id = id;
+ this.localizedName = name;
+ this.unicodeName = name;
+ }
+
+ CollateEntity(int id, String localizedName, String unicodeName) {
+ this.id = id;
+ this.localizedName = localizedName;
+ this.unicodeName = unicodeName;
+ }
+
+ @SuppressWarnings("SimplifiableIfStatement")
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ CollateEntity that = (CollateEntity) o;
+
+ if (id != that.id) return false;
+ if (!localizedName.equals(that.localizedName)) return false;
+ return unicodeName.equals(that.unicodeName);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id;
+ result = 31 * result + localizedName.hashCode();
+ result = 31 * result + unicodeName.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "CollateEntity{"
+ + "id=" + id
+ + ", localizedName='" + localizedName + '\''
+ + ", unicodeName='" + unicodeName + '\''
+ + '}';
+ }
+ }
+
+ @Dao
+ interface CollateDao {
+ @Query("SELECT * FROM CollateEntity ORDER BY localizedName ASC")
+ List<CollateEntity> sortedByLocalized();
+
+ @Query("SELECT * FROM CollateEntity ORDER BY localizedName COLLATE UNICODE ASC")
+ List<CollateEntity> sortedByLocalizedAsUnicode();
+
+ @Query("SELECT * FROM CollateEntity ORDER BY unicodeName ASC")
+ List<CollateEntity> sortedByUnicode();
+
+ @Query("SELECT * FROM CollateEntity ORDER BY unicodeName COLLATE LOCALIZED ASC")
+ List<CollateEntity> sortedByUnicodeAsLocalized();
+
+ @Insert
+ void insert(CollateEntity... entities);
+ }
+
+ @Database(entities = CollateEntity.class, version = 1, exportSchema = false)
+ abstract static class CollateDb extends RoomDatabase {
+ abstract CollateDao dao();
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/PojoTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/PojoTest.java
index b43e274..b1579fc 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/PojoTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/PojoTest.java
@@ -19,15 +19,15 @@
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
-import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.runner.AndroidJUnit4;
-
import android.arch.persistence.room.Room;
import android.arch.persistence.room.integration.testapp.TestDatabase;
import android.arch.persistence.room.integration.testapp.dao.UserDao;
import android.arch.persistence.room.integration.testapp.vo.AvgWeightByAge;
import android.arch.persistence.room.integration.testapp.vo.User;
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
@@ -35,6 +35,7 @@
import java.util.Arrays;
+@LargeTest
@RunWith(AndroidJUnit4.class)
public class PojoTest {
private UserDao mUserDao;
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/PojoWithRelationTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/PojoWithRelationTest.java
index f9ff4c2..c3ebfe9 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/PojoWithRelationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/PojoWithRelationTest.java
@@ -35,6 +35,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
@@ -188,4 +189,46 @@
new UserAndPetAdoptionDates(user, Arrays.asList(new Date(300), new Date(700)))
)));
}
+
+ @Test
+ public void largeRelation_child() {
+ User user = TestUtil.createUser(3);
+ List<Pet> pets = new ArrayList<>();
+ for (int i = 0; i < 2000; i++) {
+ Pet pet = TestUtil.createPet(i + 1);
+ pet.setUserId(3);
+ }
+ mUserDao.insert(user);
+ mPetDao.insertAll(pets.toArray(new Pet[pets.size()]));
+ List<UserAndAllPets> result = mUserPetDao.loadAllUsersWithTheirPets();
+ assertThat(result.size(), is(1));
+ assertThat(result.get(0).user, is(user));
+ assertThat(result.get(0).pets, is(pets));
+ }
+
+ @Test
+ public void largeRelation_parent() {
+ final List<User> users = new ArrayList<>();
+ final List<Pet> pets = new ArrayList<>();
+ for (int i = 0; i < 2000; i++) {
+ User user = TestUtil.createUser(i + 1);
+ users.add(user);
+ Pet pet = TestUtil.createPet(i + 1);
+ pet.setUserId(user.getId());
+ pets.add(pet);
+ }
+ mDatabase.runInTransaction(new Runnable() {
+ @Override
+ public void run() {
+ mUserDao.insertAll(users.toArray(new User[users.size()]));
+ mPetDao.insertAll(pets.toArray(new Pet[pets.size()]));
+ }
+ });
+ List<UserAndAllPets> result = mUserPetDao.loadAllUsersWithTheirPets();
+ assertThat(result.size(), is(2000));
+ for (int i = 0; i < 2000; i++) {
+ assertThat(result.get(i).user, is(users.get(i)));
+ assertThat(result.get(i).pets, is(Collections.singletonList(pets.get(i))));
+ }
+ }
}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/RawQueryTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/RawQueryTest.java
new file mode 100644
index 0000000..4aae4ea
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/RawQueryTest.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.arch.core.executor.testing.CountingTaskExecutorRule;
+import android.arch.lifecycle.LiveData;
+import android.arch.persistence.db.SimpleSQLiteQuery;
+import android.arch.persistence.room.integration.testapp.dao.RawDao;
+import android.arch.persistence.room.integration.testapp.vo.NameAndLastName;
+import android.arch.persistence.room.integration.testapp.vo.Pet;
+import android.arch.persistence.room.integration.testapp.vo.User;
+import android.arch.persistence.room.integration.testapp.vo.UserAndAllPets;
+import android.arch.persistence.room.integration.testapp.vo.UserAndPet;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RawQueryTest extends TestDatabaseTest {
+ @Rule
+ public CountingTaskExecutorRule mExecutorRule = new CountingTaskExecutorRule();
+
+ @Test
+ public void entity_null() {
+ User user = mRawDao.getUser("SELECT * FROM User WHERE mId = 0");
+ assertThat(user, is(nullValue()));
+ }
+
+ @Test
+ public void entity_one() {
+ User expected = TestUtil.createUser(3);
+ mUserDao.insert(expected);
+ User received = mRawDao.getUser("SELECT * FROM User WHERE mId = 3");
+ assertThat(received, is(expected));
+ }
+
+ @Test
+ public void entity_list() {
+ List<User> expected = TestUtil.createUsersList(1, 2, 3, 4);
+ mUserDao.insertAll(expected.toArray(new User[4]));
+ List<User> received = mRawDao.getUserList("SELECT * FROM User ORDER BY mId ASC");
+ assertThat(received, is(expected));
+ }
+
+ @Test
+ public void entity_liveData() throws TimeoutException, InterruptedException {
+ LiveData<User> liveData = mRawDao.getUserLiveData("SELECT * FROM User WHERE mId = 3");
+ liveData.observeForever(user -> {
+ });
+ drain();
+ assertThat(liveData.getValue(), is(nullValue()));
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ drain();
+ assertThat(liveData.getValue(), is(user));
+ user.setLastName("cxZ");
+ mUserDao.insertOrReplace(user);
+ drain();
+ assertThat(liveData.getValue(), is(user));
+ }
+
+ @Test
+ public void entity_supportSql() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ SimpleSQLiteQuery query = new SimpleSQLiteQuery("SELECT * FROM User WHERE mId = ?",
+ new Object[]{3});
+ User received = mRawDao.getUser(query);
+ assertThat(received, is(user));
+ }
+
+ @Test
+ public void embedded() {
+ User user = TestUtil.createUser(3);
+ Pet[] pets = TestUtil.createPetsForUser(3, 1, 1);
+ mUserDao.insert(user);
+ mPetDao.insertAll(pets);
+ UserAndPet received = mRawDao.getUserAndPet(
+ "SELECT * FROM User, Pet WHERE User.mId = Pet.mUserId LIMIT 1");
+ assertThat(received.getUser(), is(user));
+ assertThat(received.getPet(), is(pets[0]));
+ }
+
+ @Test
+ public void relation() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ Pet[] pets = TestUtil.createPetsForUser(3, 1, 10);
+ mPetDao.insertAll(pets);
+ UserAndAllPets result = mRawDao
+ .getUserAndAllPets("SELECT * FROM User WHERE mId = 3");
+ assertThat(result.user, is(user));
+ assertThat(result.pets, is(Arrays.asList(pets)));
+ }
+
+ @Test
+ public void pojo() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ NameAndLastName result =
+ mRawDao.getUserNameAndLastName("SELECT * FROM User");
+ assertThat(result, is(new NameAndLastName(user.getName(), user.getLastName())));
+ }
+
+ @Test
+ public void pojo_supportSql() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ NameAndLastName result =
+ mRawDao.getUserNameAndLastName(new SimpleSQLiteQuery(
+ "SELECT * FROM User WHERE mId = ?",
+ new Object[] {3}
+ ));
+ assertThat(result, is(new NameAndLastName(user.getName(), user.getLastName())));
+ }
+
+ @Test
+ public void pojo_typeConverter() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ RawDao.UserNameAndBirthday result = mRawDao.getUserAndBirthday(
+ "SELECT mName, mBirthday FROM user LIMIT 1");
+ assertThat(result.name, is(user.getName()));
+ assertThat(result.birthday, is(user.getBirthday()));
+ }
+
+ @Test
+ public void embedded_nullField() {
+ User user = TestUtil.createUser(3);
+ Pet[] pets = TestUtil.createPetsForUser(3, 1, 1);
+ mUserDao.insert(user);
+ mPetDao.insertAll(pets);
+ UserAndPet received = mRawDao.getUserAndPet("SELECT * FROM User LIMIT 1");
+ assertThat(received.getUser(), is(user));
+ assertThat(received.getPet(), is(nullValue()));
+ }
+
+ @Test
+ public void embedded_list() {
+ User[] users = TestUtil.createUsersArray(3, 5);
+ Pet[] pets = TestUtil.createPetsForUser(3, 1, 2);
+ mUserDao.insertAll(users);
+ mPetDao.insertAll(pets);
+ List<UserAndPet> received = mRawDao.getUserAndPetList(
+ "SELECT * FROM User LEFT JOIN Pet ON (User.mId = Pet.mUserId)"
+ + " ORDER BY mId ASC, mPetId ASC");
+ assertThat(received.size(), is(3));
+ // row 0
+ assertThat(received.get(0).getUser(), is(users[0]));
+ assertThat(received.get(0).getPet(), is(pets[0]));
+ // row 1
+ assertThat(received.get(1).getUser(), is(users[0]));
+ assertThat(received.get(1).getPet(), is(pets[1]));
+ // row 2
+ assertThat(received.get(2).getUser(), is(users[1]));
+ assertThat(received.get(2).getPet(), is(nullValue()));
+ }
+
+ @Test
+ public void count() {
+ mUserDao.insertAll(TestUtil.createUsersArray(3, 5, 7, 10));
+ int count = mRawDao.count("SELECT COUNT(*) FROM User");
+ assertThat(count, is(4));
+ }
+
+ private void drain() throws TimeoutException, InterruptedException {
+ mExecutorRule.drainTasks(1, TimeUnit.MINUTES);
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java
index ec77561..e2525c4 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java
@@ -21,6 +21,7 @@
import android.arch.persistence.room.integration.testapp.dao.FunnyNamedDao;
import android.arch.persistence.room.integration.testapp.dao.PetCoupleDao;
import android.arch.persistence.room.integration.testapp.dao.PetDao;
+import android.arch.persistence.room.integration.testapp.dao.RawDao;
import android.arch.persistence.room.integration.testapp.dao.SchoolDao;
import android.arch.persistence.room.integration.testapp.dao.SpecificDogDao;
import android.arch.persistence.room.integration.testapp.dao.ToyDao;
@@ -44,6 +45,7 @@
protected SpecificDogDao mSpecificDogDao;
protected WithClauseDao mWithClauseDao;
protected FunnyNamedDao mFunnyNamedDao;
+ protected RawDao mRawDao;
@Before
public void createDb() {
@@ -58,5 +60,6 @@
mSpecificDogDao = mDatabase.getSpecificDogDao();
mWithClauseDao = mDatabase.getWithClauseDao();
mFunnyNamedDao = mDatabase.getFunnyNamedDao();
+ mRawDao = mDatabase.getRawDao();
}
}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java
index 29e2554..a6e8223 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java
@@ -33,4 +33,23 @@
public String getLastName() {
return mLastName;
}
+
+ @SuppressWarnings("SimplifiableIfStatement")
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ NameAndLastName that = (NameAndLastName) o;
+
+ if (mName != null ? !mName.equals(that.mName) : that.mName != null) return false;
+ return mLastName != null ? mLastName.equals(that.mLastName) : that.mLastName == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mName != null ? mName.hashCode() : 0;
+ result = 31 * result + (mLastName != null ? mLastName.hashCode() : 0);
+ return result;
+ }
}
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/DatabaseBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/DatabaseBundle.java
index 4ac9029..f131838 100644
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/DatabaseBundle.java
+++ b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/DatabaseBundle.java
@@ -32,7 +32,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class DatabaseBundle {
+public class DatabaseBundle implements SchemaEquality<DatabaseBundle> {
@SerializedName("version")
private int mVersion;
@SerializedName("identityHash")
@@ -104,4 +104,10 @@
result.addAll(mSetupQueries);
return result;
}
+
+ @Override
+ public boolean isSchemaEqual(DatabaseBundle other) {
+ return SchemaEqualityUtil.checkSchemaEquality(getEntitiesByTableName(),
+ other.getEntitiesByTableName());
+ }
}
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/EntityBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/EntityBundle.java
index 8980a3b..d78ac35 100644
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/EntityBundle.java
+++ b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/EntityBundle.java
@@ -16,6 +16,8 @@
package android.arch.persistence.room.migration.bundle;
+import static android.arch.persistence.room.migration.bundle.SchemaEqualityUtil.checkSchemaEquality;
+
import android.support.annotation.RestrictTo;
import com.google.gson.annotations.SerializedName;
@@ -35,7 +37,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class EntityBundle {
+public class EntityBundle implements SchemaEquality<EntityBundle> {
static final String NEW_TABLE_PREFIX = "_new_";
@@ -176,4 +178,15 @@
}
return result;
}
+
+ @Override
+ public boolean isSchemaEqual(EntityBundle other) {
+ if (!mTableName.equals(other.mTableName)) {
+ return false;
+ }
+ return checkSchemaEquality(getFieldsByColumnName(), other.getFieldsByColumnName())
+ && checkSchemaEquality(mPrimaryKey, other.mPrimaryKey)
+ && checkSchemaEquality(mIndices, other.mIndices)
+ && checkSchemaEquality(mForeignKeys, other.mForeignKeys);
+ }
}
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/FieldBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/FieldBundle.java
index eb73d81..5f74087 100644
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/FieldBundle.java
+++ b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/FieldBundle.java
@@ -27,7 +27,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class FieldBundle {
+public class FieldBundle implements SchemaEquality<FieldBundle> {
@SerializedName("fieldPath")
private String mFieldPath;
@SerializedName("columnName")
@@ -59,4 +59,14 @@
public boolean isNonNull() {
return mNonNull;
}
+
+ @Override
+ public boolean isSchemaEqual(FieldBundle other) {
+ if (mNonNull != other.mNonNull) return false;
+ if (mColumnName != null ? !mColumnName.equals(other.mColumnName)
+ : other.mColumnName != null) {
+ return false;
+ }
+ return mAffinity != null ? mAffinity.equals(other.mAffinity) : other.mAffinity == null;
+ }
}
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java
index d72cf8c..367dd74 100644
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java
+++ b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java
@@ -28,7 +28,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class ForeignKeyBundle {
+public class ForeignKeyBundle implements SchemaEquality<ForeignKeyBundle> {
@SerializedName("table")
private String mTable;
@SerializedName("onDelete")
@@ -43,10 +43,10 @@
/**
* Creates a foreign key bundle with the given parameters.
*
- * @param table The target table
- * @param onDelete OnDelete action
- * @param onUpdate OnUpdate action
- * @param columns The list of columns in the current table
+ * @param table The target table
+ * @param onDelete OnDelete action
+ * @param onUpdate OnUpdate action
+ * @param columns The list of columns in the current table
* @param referencedColumns The list of columns in the referenced table
*/
public ForeignKeyBundle(String table, String onDelete, String onUpdate,
@@ -102,4 +102,18 @@
public List<String> getReferencedColumns() {
return mReferencedColumns;
}
+
+ @Override
+ public boolean isSchemaEqual(ForeignKeyBundle other) {
+ if (mTable != null ? !mTable.equals(other.mTable) : other.mTable != null) return false;
+ if (mOnDelete != null ? !mOnDelete.equals(other.mOnDelete) : other.mOnDelete != null) {
+ return false;
+ }
+ if (mOnUpdate != null ? !mOnUpdate.equals(other.mOnUpdate) : other.mOnUpdate != null) {
+ return false;
+ }
+ // order matters
+ return mColumns.equals(other.mColumns) && mReferencedColumns.equals(
+ other.mReferencedColumns);
+ }
}
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/IndexBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/IndexBundle.java
index ba40618..e991316 100644
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/IndexBundle.java
+++ b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/IndexBundle.java
@@ -28,7 +28,9 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class IndexBundle {
+public class IndexBundle implements SchemaEquality<IndexBundle> {
+ // should match Index.kt
+ public static final String DEFAULT_PREFIX = "index_";
@SerializedName("name")
private String mName;
@SerializedName("unique")
@@ -65,4 +67,25 @@
public String create(String tableName) {
return BundleUtil.replaceTableName(mCreateSql, tableName);
}
+
+ @Override
+ public boolean isSchemaEqual(IndexBundle other) {
+ if (mUnique != other.mUnique) return false;
+ if (mName.startsWith(DEFAULT_PREFIX)) {
+ if (!other.mName.startsWith(DEFAULT_PREFIX)) {
+ return false;
+ }
+ } else if (other.mName.startsWith(DEFAULT_PREFIX)) {
+ return false;
+ } else if (!mName.equals(other.mName)) {
+ return false;
+ }
+
+ // order matters
+ if (mColumnNames != null ? !mColumnNames.equals(other.mColumnNames)
+ : other.mColumnNames != null) {
+ return false;
+ }
+ return true;
+ }
}
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/PrimaryKeyBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/PrimaryKeyBundle.java
index c16f967..820aa7e 100644
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/PrimaryKeyBundle.java
+++ b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/PrimaryKeyBundle.java
@@ -28,7 +28,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class PrimaryKeyBundle {
+public class PrimaryKeyBundle implements SchemaEquality<PrimaryKeyBundle> {
@SerializedName("columnNames")
private List<String> mColumnNames;
@SerializedName("autoGenerate")
@@ -46,4 +46,9 @@
public boolean isAutoGenerate() {
return mAutoGenerate;
}
+
+ @Override
+ public boolean isSchemaEqual(PrimaryKeyBundle other) {
+ return mColumnNames.equals(other.mColumnNames) && mAutoGenerate == other.mAutoGenerate;
+ }
}
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaBundle.java
index d6171aa..af35e6f 100644
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaBundle.java
+++ b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaBundle.java
@@ -37,7 +37,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class SchemaBundle {
+public class SchemaBundle implements SchemaEquality<SchemaBundle> {
@SerializedName("formatVersion")
private int mFormatVersion;
@@ -47,6 +47,7 @@
private static final Gson GSON;
private static final String CHARSET = "UTF-8";
public static final int LATEST_FORMAT = 1;
+
static {
GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
}
@@ -104,4 +105,9 @@
}
}
+ @Override
+ public boolean isSchemaEqual(SchemaBundle other) {
+ return SchemaEqualityUtil.checkSchemaEquality(mDatabase, other.mDatabase)
+ && mFormatVersion == other.mFormatVersion;
+ }
}
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaEquality.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaEquality.java
new file mode 100644
index 0000000..59ea4b0
--- /dev/null
+++ b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaEquality.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.migration.bundle;
+
+import android.support.annotation.RestrictTo;
+
+/**
+ * A loose equals check which checks schema equality instead of 100% equality (e.g. order of
+ * columns in an entity does not have to match)
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+interface SchemaEquality<T> {
+ boolean isSchemaEqual(T other);
+}
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaEqualityUtil.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaEqualityUtil.java
new file mode 100644
index 0000000..65a7572
--- /dev/null
+++ b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaEqualityUtil.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.migration.bundle;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * utility class to run schema equality on collections.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class SchemaEqualityUtil {
+ static <T, K extends SchemaEquality<K>> boolean checkSchemaEquality(
+ @Nullable Map<T, K> map1, @Nullable Map<T, K> map2) {
+ if (map1 == null) {
+ return map2 == null;
+ }
+ if (map2 == null) {
+ return false;
+ }
+ if (map1.size() != map2.size()) {
+ return false;
+ }
+ for (Map.Entry<T, K> pair : map1.entrySet()) {
+ if (!checkSchemaEquality(pair.getValue(), map2.get(pair.getKey()))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ static <K extends SchemaEquality<K>> boolean checkSchemaEquality(
+ @Nullable List<K> list1, @Nullable List<K> list2) {
+ if (list1 == null) {
+ return list2 == null;
+ }
+ if (list2 == null) {
+ return false;
+ }
+ if (list1.size() != list2.size()) {
+ return false;
+ }
+ // we don't care this is n^2, small list + only used for testing.
+ for (K item1 : list1) {
+ // find matching item
+ boolean matched = false;
+ for (K item2 : list2) {
+ if (checkSchemaEquality(item1, item2)) {
+ matched = true;
+ break;
+ }
+ }
+ if (!matched) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @SuppressWarnings("SimplifiableIfStatement")
+ static <K extends SchemaEquality<K>> boolean checkSchemaEquality(
+ @Nullable K item1, @Nullable K item2) {
+ if (item1 == null) {
+ return item2 == null;
+ }
+ if (item2 == null) {
+ return false;
+ }
+ return item1.isSchemaEqual(item2);
+ }
+}
diff --git a/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/EntityBundleTest.java b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/EntityBundleTest.java
new file mode 100644
index 0000000..4b4df8b
--- /dev/null
+++ b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/EntityBundleTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import static java.util.Arrays.asList;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Collections;
+
+@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
+@RunWith(JUnit4.class)
+public class EntityBundleTest {
+ @Test
+ public void schemaEquality_same_equal() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("foo"), createFieldBundle("bar")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("foo")),
+ asList(createForeignKeyBundle("bar", "foo")));
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("foo"), createFieldBundle("bar")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("foo")),
+ asList(createForeignKeyBundle("bar", "foo")));
+
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_reorderedFields_equal() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("foo"), createFieldBundle("bar")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("bar"), createFieldBundle("foo")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffFields_notEqual() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("foo"), createFieldBundle("bar")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("foo2"), createFieldBundle("bar")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_reorderedForeignKeys_equal() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ asList(createForeignKeyBundle("x", "y"),
+ createForeignKeyBundle("bar", "foo")));
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ asList(createForeignKeyBundle("bar", "foo"),
+ createForeignKeyBundle("x", "y")));
+
+
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffForeignKeys_notEqual() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ asList(createForeignKeyBundle("bar", "foo")));
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ asList(createForeignKeyBundle("bar2", "foo")));
+
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_reorderedIndices_equal() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("foo"), createIndexBundle("baz")),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("baz"), createIndexBundle("foo")),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffIndices_notEqual() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("foo")),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("foo2")),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ private FieldBundle createFieldBundle(String name) {
+ return new FieldBundle("foo", name, "text", false);
+ }
+
+ private IndexBundle createIndexBundle(String colName) {
+ return new IndexBundle("ind_" + colName, false,
+ asList(colName), "create");
+ }
+
+ private ForeignKeyBundle createForeignKeyBundle(String targetTable, String column) {
+ return new ForeignKeyBundle(targetTable, "CASCADE", "CASCADE",
+ asList(column), asList(column));
+ }
+}
diff --git a/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/FieldBundleTest.java b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/FieldBundleTest.java
new file mode 100644
index 0000000..eac4477
--- /dev/null
+++ b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/FieldBundleTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class FieldBundleTest {
+ @Test
+ public void schemaEquality_same_equal() {
+ FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+ FieldBundle copy = new FieldBundle("foo", "foo", "text", false);
+ assertThat(bundle.isSchemaEqual(copy), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffNonNull_notEqual() {
+ FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+ FieldBundle copy = new FieldBundle("foo", "foo", "text", true);
+ assertThat(bundle.isSchemaEqual(copy), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffColumnName_notEqual() {
+ FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+ FieldBundle copy = new FieldBundle("foo", "foo2", "text", true);
+ assertThat(bundle.isSchemaEqual(copy), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffAffinity_notEqual() {
+ FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+ FieldBundle copy = new FieldBundle("foo", "foo2", "int", false);
+ assertThat(bundle.isSchemaEqual(copy), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffPath_equal() {
+ FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+ FieldBundle copy = new FieldBundle("foo>bar", "foo", "text", false);
+ assertThat(bundle.isSchemaEqual(copy), is(true));
+ }
+}
diff --git a/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/ForeignKeyBundleTest.java b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/ForeignKeyBundleTest.java
new file mode 100644
index 0000000..be1b81e
--- /dev/null
+++ b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/ForeignKeyBundleTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+@RunWith(JUnit4.class)
+public class ForeignKeyBundleTest {
+ @Test
+ public void schemaEquality_same_equal() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffTable_notEqual() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table2", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffOnDelete_notEqual() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete2",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffOnUpdate_notEqual() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate2", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffSrcOrder_notEqual() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col2", "col1"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffTargetOrder_notEqual() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target2", "target1"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+}
diff --git a/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/IndexBundleTest.java b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/IndexBundleTest.java
new file mode 100644
index 0000000..aa7230f
--- /dev/null
+++ b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/IndexBundleTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+@RunWith(JUnit4.class)
+public class IndexBundleTest {
+ @Test
+ public void schemaEquality_same_equal() {
+ IndexBundle bundle = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffName_notEqual() {
+ IndexBundle bundle = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle("index3", false,
+ Arrays.asList("col1", "col2"), "sql");
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffGenericName_equal() {
+ IndexBundle bundle = new IndexBundle(IndexBundle.DEFAULT_PREFIX + "x", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle(IndexBundle.DEFAULT_PREFIX + "y", false,
+ Arrays.asList("col1", "col2"), "sql");
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffUnique_notEqual() {
+ IndexBundle bundle = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle("index1", true,
+ Arrays.asList("col1", "col2"), "sql");
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffColumns_notEqual() {
+ IndexBundle bundle = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle("index1", false,
+ Arrays.asList("col2", "col1"), "sql");
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffSql_equal() {
+ IndexBundle bundle = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql22");
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+}
diff --git a/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/PrimaryKeyBundleTest.java b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/PrimaryKeyBundleTest.java
new file mode 100644
index 0000000..3b9e464
--- /dev/null
+++ b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/PrimaryKeyBundleTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+@RunWith(JUnit4.class)
+public class PrimaryKeyBundleTest {
+ @Test
+ public void schemaEquality_same_equal() {
+ PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "bar"));
+ PrimaryKeyBundle other = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "bar"));
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffAutoGen_notEqual() {
+ PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "bar"));
+ PrimaryKeyBundle other = new PrimaryKeyBundle(false,
+ Arrays.asList("foo", "bar"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffColumns_notEqual() {
+ PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "baz"));
+ PrimaryKeyBundle other = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "bar"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffColumnOrder_notEqual() {
+ PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "bar"));
+ PrimaryKeyBundle other = new PrimaryKeyBundle(true,
+ Arrays.asList("bar", "foo"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+}
diff --git a/room/runtime/api/current.txt b/room/runtime/api/current.txt
new file mode 100644
index 0000000..29cfa85
--- /dev/null
+++ b/room/runtime/api/current.txt
@@ -0,0 +1,90 @@
+package android.arch.persistence.room {
+
+ public class DatabaseConfiguration {
+ method public boolean isMigrationRequiredFrom(int);
+ field public final boolean allowMainThreadQueries;
+ field public final java.util.List<android.arch.persistence.room.RoomDatabase.Callback> callbacks;
+ field public final android.content.Context context;
+ field public final android.arch.persistence.room.RoomDatabase.MigrationContainer migrationContainer;
+ field public final java.lang.String name;
+ field public final boolean requireMigration;
+ field public final android.arch.persistence.db.SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory;
+ }
+
+ public class InvalidationTracker {
+ method public void addObserver(android.arch.persistence.room.InvalidationTracker.Observer);
+ method public void refreshVersionsAsync();
+ method public void removeObserver(android.arch.persistence.room.InvalidationTracker.Observer);
+ }
+
+ public static abstract class InvalidationTracker.Observer {
+ ctor protected InvalidationTracker.Observer(java.lang.String, java.lang.String...);
+ ctor public InvalidationTracker.Observer(java.lang.String[]);
+ method public abstract void onInvalidated(java.util.Set<java.lang.String>);
+ }
+
+ public class Room {
+ ctor public Room();
+ method public static <T extends android.arch.persistence.room.RoomDatabase> android.arch.persistence.room.RoomDatabase.Builder<T> databaseBuilder(android.content.Context, java.lang.Class<T>, java.lang.String);
+ method public static <T extends android.arch.persistence.room.RoomDatabase> android.arch.persistence.room.RoomDatabase.Builder<T> inMemoryDatabaseBuilder(android.content.Context, java.lang.Class<T>);
+ field public static final java.lang.String MASTER_TABLE_NAME = "room_master_table";
+ }
+
+ public abstract class RoomDatabase {
+ ctor public RoomDatabase();
+ method public void beginTransaction();
+ method public void close();
+ method public android.arch.persistence.db.SupportSQLiteStatement compileStatement(java.lang.String);
+ method protected abstract android.arch.persistence.room.InvalidationTracker createInvalidationTracker();
+ method protected abstract android.arch.persistence.db.SupportSQLiteOpenHelper createOpenHelper(android.arch.persistence.room.DatabaseConfiguration);
+ method public void endTransaction();
+ method public android.arch.persistence.room.InvalidationTracker getInvalidationTracker();
+ method public android.arch.persistence.db.SupportSQLiteOpenHelper getOpenHelper();
+ method public boolean inTransaction();
+ method public void init(android.arch.persistence.room.DatabaseConfiguration);
+ method protected void internalInitInvalidationTracker(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public boolean isOpen();
+ method public android.database.Cursor query(java.lang.String, java.lang.Object[]);
+ method public android.database.Cursor query(android.arch.persistence.db.SupportSQLiteQuery);
+ method public void runInTransaction(java.lang.Runnable);
+ method public <V> V runInTransaction(java.util.concurrent.Callable<V>);
+ method public void setTransactionSuccessful();
+ field protected java.util.List<android.arch.persistence.room.RoomDatabase.Callback> mCallbacks;
+ field protected volatile android.arch.persistence.db.SupportSQLiteDatabase mDatabase;
+ }
+
+ public static class RoomDatabase.Builder<T extends android.arch.persistence.room.RoomDatabase> {
+ method public android.arch.persistence.room.RoomDatabase.Builder<T> addCallback(android.arch.persistence.room.RoomDatabase.Callback);
+ method public android.arch.persistence.room.RoomDatabase.Builder<T> addMigrations(android.arch.persistence.room.migration.Migration...);
+ method public android.arch.persistence.room.RoomDatabase.Builder<T> allowMainThreadQueries();
+ method public T build();
+ method public android.arch.persistence.room.RoomDatabase.Builder<T> fallbackToDestructiveMigration();
+ method public android.arch.persistence.room.RoomDatabase.Builder<T> fallbackToDestructiveMigrationFrom(java.lang.Integer...);
+ method public android.arch.persistence.room.RoomDatabase.Builder<T> openHelperFactory(android.arch.persistence.db.SupportSQLiteOpenHelper.Factory);
+ }
+
+ public static abstract class RoomDatabase.Callback {
+ ctor public RoomDatabase.Callback();
+ method public void onCreate(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public void onOpen(android.arch.persistence.db.SupportSQLiteDatabase);
+ }
+
+ public static class RoomDatabase.MigrationContainer {
+ ctor public RoomDatabase.MigrationContainer();
+ method public void addMigrations(android.arch.persistence.room.migration.Migration...);
+ method public java.util.List<android.arch.persistence.room.migration.Migration> findMigrationPath(int, int);
+ }
+
+}
+
+package android.arch.persistence.room.migration {
+
+ public abstract class Migration {
+ ctor public Migration(int, int);
+ method public abstract void migrate(android.arch.persistence.db.SupportSQLiteDatabase);
+ field public final int endVersion;
+ field public final int startVersion;
+ }
+
+}
+
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/DatabaseConfiguration.java b/room/runtime/src/main/java/android/arch/persistence/room/DatabaseConfiguration.java
index adf5d4d..42acc1d 100644
--- a/room/runtime/src/main/java/android/arch/persistence/room/DatabaseConfiguration.java
+++ b/room/runtime/src/main/java/android/arch/persistence/room/DatabaseConfiguration.java
@@ -23,6 +23,7 @@
import android.support.annotation.RestrictTo;
import java.util.List;
+import java.util.Set;
/**
* Configuration class for a {@link RoomDatabase}.
@@ -65,6 +66,11 @@
public final boolean requireMigration;
/**
+ * The collection of schema versions from which migrations aren't required.
+ */
+ private final Set<Integer> mMigrationNotRequiredFrom;
+
+ /**
* Creates a database configuration with the given values.
*
* @param context The application context.
@@ -75,6 +81,8 @@
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param requireMigration True if Room should require a valid migration if version changes,
* instead of recreating the tables.
+ * @param migrationNotRequiredFrom The collection of schema versions from which migrations
+ * aren't required.
*
* @hide
*/
@@ -84,7 +92,8 @@
@NonNull RoomDatabase.MigrationContainer migrationContainer,
@Nullable List<RoomDatabase.Callback> callbacks,
boolean allowMainThreadQueries,
- boolean requireMigration) {
+ boolean requireMigration,
+ @Nullable Set<Integer> migrationNotRequiredFrom) {
this.sqliteOpenHelperFactory = sqliteOpenHelperFactory;
this.context = context;
this.name = name;
@@ -92,5 +101,21 @@
this.callbacks = callbacks;
this.allowMainThreadQueries = allowMainThreadQueries;
this.requireMigration = requireMigration;
+ this.mMigrationNotRequiredFrom = migrationNotRequiredFrom;
+ }
+
+ /**
+ * Returns whether a migration is required from the specified version.
+ *
+ * @param version The schema version.
+ * @return True if a valid migration is required, false otherwise.
+ */
+ public boolean isMigrationRequiredFrom(int version) {
+ // Migrations are required from this version if we generally require migrations AND EITHER
+ // there are no exceptions OR the supplied version is not one of the exceptions.
+ return requireMigration
+ && (mMigrationNotRequiredFrom == null
+ || !mMigrationNotRequiredFrom.contains(version));
+
}
}
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/RoomDatabase.java b/room/runtime/src/main/java/android/arch/persistence/room/RoomDatabase.java
index 2a108f9..2f6b13d 100644
--- a/room/runtime/src/main/java/android/arch/persistence/room/RoomDatabase.java
+++ b/room/runtime/src/main/java/android/arch/persistence/room/RoomDatabase.java
@@ -35,7 +35,9 @@
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@@ -52,6 +54,13 @@
//@SuppressWarnings({"unused", "WeakerAccess"})
public abstract class RoomDatabase {
private static final String DB_IMPL_SUFFIX = "_Impl";
+ /**
+ * Unfortunately, we cannot read this value so we are only setting it to the SQLite default.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final int MAX_BIND_PARAMETER_CNT = 999;
// set by the generated open helper.
protected volatile SupportSQLiteDatabase mDatabase;
private SupportSQLiteOpenHelper mOpenHelper;
@@ -326,7 +335,14 @@
/**
* Migrations, mapped by from-to pairs.
*/
- private MigrationContainer mMigrationContainer;
+ private final MigrationContainer mMigrationContainer;
+ private Set<Integer> mMigrationsNotRequiredFrom;
+ /**
+ * Keeps track of {@link Migration#startVersion}s and {@link Migration#endVersion}s added in
+ * {@link #addMigrations(Migration...)} for later validation that makes those versions don't
+ * match any versions passed to {@link #fallbackToDestructiveMigrationFrom(Integer...)}.
+ */
+ private Set<Integer> mMigrationStartAndEndVersions;
Builder(@NonNull Context context, @NonNull Class<T> klass, @Nullable String name) {
mContext = context;
@@ -369,7 +385,15 @@
* @return this
*/
@NonNull
- public Builder<T> addMigrations(@NonNull Migration... migrations) {
+ public Builder<T> addMigrations(@NonNull Migration... migrations) {
+ if (mMigrationStartAndEndVersions == null) {
+ mMigrationStartAndEndVersions = new HashSet<>();
+ }
+ for (Migration migration: migrations) {
+ mMigrationStartAndEndVersions.add(migration.startVersion);
+ mMigrationStartAndEndVersions.add(migration.endVersion);
+ }
+
mMigrationContainer.addMigrations(migrations);
return this;
}
@@ -416,6 +440,36 @@
}
/**
+ * Informs Room that it is allowed to destructively recreate database tables from specific
+ * starting schema versions.
+ * <p>
+ * This functionality is the same as that provided by
+ * {@link #fallbackToDestructiveMigration()}, except that this method allows the
+ * specification of a set of schema versions for which destructive recreation is allowed.
+ * <p>
+ * Using this method is preferable to {@link #fallbackToDestructiveMigration()} if you want
+ * to allow destructive migrations from some schema versions while still taking advantage
+ * of exceptions being thrown due to unintentionally missing migrations.
+ * <p>
+ * Note: No versions passed to this method may also exist as either starting or ending
+ * versions in the {@link Migration}s provided to {@link #addMigrations(Migration...)}. If a
+ * version passed to this method is found as a starting or ending version in a Migration, an
+ * exception will be thrown.
+ *
+ * @param startVersions The set of schema versions from which Room should use a destructive
+ * migration.
+ * @return this
+ */
+ @NonNull
+ public Builder<T> fallbackToDestructiveMigrationFrom(Integer... startVersions) {
+ if (mMigrationsNotRequiredFrom == null) {
+ mMigrationsNotRequiredFrom = new HashSet<>();
+ }
+ Collections.addAll(mMigrationsNotRequiredFrom, startVersions);
+ return this;
+ }
+
+ /**
* Adds a {@link Callback} to this database.
*
* @param callback The callback.
@@ -449,12 +503,28 @@
throw new IllegalArgumentException("Must provide an abstract class that"
+ " extends RoomDatabase");
}
+
+ if (mMigrationStartAndEndVersions != null && mMigrationsNotRequiredFrom != null) {
+ for (Integer version : mMigrationStartAndEndVersions) {
+ if (mMigrationsNotRequiredFrom.contains(version)) {
+ throw new IllegalArgumentException(
+ "Inconsistency detected. A Migration was supplied to "
+ + "addMigration(Migration... migrations) that has a start "
+ + "or end version equal to a start version supplied to "
+ + "fallbackToDestructiveMigrationFrom(Integer ... "
+ + "startVersions). Start version: "
+ + version);
+ }
+ }
+ }
+
if (mFactory == null) {
mFactory = new FrameworkSQLiteOpenHelperFactory();
}
DatabaseConfiguration configuration =
new DatabaseConfiguration(mContext, mName, mFactory, mMigrationContainer,
- mCallbacks, mAllowMainThreadQueries, mRequireMigration);
+ mCallbacks, mAllowMainThreadQueries, mRequireMigration,
+ mMigrationsNotRequiredFrom);
T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
db.init(configuration);
return db;
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/RoomOpenHelper.java b/room/runtime/src/main/java/android/arch/persistence/room/RoomOpenHelper.java
index 47279d6..aad6895 100644
--- a/room/runtime/src/main/java/android/arch/persistence/room/RoomOpenHelper.java
+++ b/room/runtime/src/main/java/android/arch/persistence/room/RoomOpenHelper.java
@@ -41,13 +41,20 @@
private final Delegate mDelegate;
@NonNull
private final String mIdentityHash;
+ /**
+ * Room v1 had a bug where the hash was not consistent if fields are reordered.
+ * The new has fixes it but we still need to accept the legacy hash.
+ */
+ @NonNull // b/64290754
+ private final String mLegacyHash;
public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
- @NonNull String identityHash) {
+ @NonNull String identityHash, @NonNull String legacyHash) {
super(delegate.version);
mConfiguration = configuration;
mDelegate = delegate;
mIdentityHash = identityHash;
+ mLegacyHash = legacyHash;
}
@Override
@@ -78,14 +85,17 @@
}
}
if (!migrated) {
- if (mConfiguration == null || mConfiguration.requireMigration) {
+ if (mConfiguration != null && !mConfiguration.isMigrationRequiredFrom(oldVersion)) {
+ mDelegate.dropAllTables(db);
+ mDelegate.createAllTables(db);
+ } else {
throw new IllegalStateException("A migration from " + oldVersion + " to "
- + newVersion + " is necessary. Please provide a Migration in the builder or call"
- + " fallbackToDestructiveMigration in the builder in which case Room will"
- + " re-create all of the tables.");
+ + newVersion + " was required but not found. Please provide the "
+ + "necessary Migration path via "
+ + "RoomDatabase.Builder.addMigration(Migration ...) or allow for "
+ + "destructive migrations via one of the "
+ + "RoomDatabase.Builder.fallbackToDestructiveMigration* methods.");
}
- mDelegate.dropAllTables(db);
- mDelegate.createAllTables(db);
}
}
@@ -115,7 +125,7 @@
} finally {
cursor.close();
}
- if (!mIdentityHash.equals(identityHash)) {
+ if (!mIdentityHash.equals(identityHash) && !mLegacyHash.equals(identityHash)) {
throw new IllegalStateException("Room cannot verify the data integrity. Looks like"
+ " you've changed schema but forgot to update the version number. You can"
+ " simply fix this by increasing the version number.");
diff --git a/room/runtime/src/test/java/android/arch/persistence/room/BuilderTest.java b/room/runtime/src/test/java/android/arch/persistence/room/BuilderTest.java
index 0728cca..33d96d8 100644
--- a/room/runtime/src/test/java/android/arch/persistence/room/BuilderTest.java
+++ b/room/runtime/src/test/java/android/arch/persistence/room/BuilderTest.java
@@ -30,6 +30,7 @@
import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory;
import android.arch.persistence.room.migration.Migration;
import android.content.Context;
+import android.support.annotation.NonNull;
import org.hamcrest.CoreMatchers;
import org.junit.Test;
@@ -110,13 +111,102 @@
@Test
public void skipMigration() {
Context context = mock(Context.class);
+
TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
- .fallbackToDestructiveMigration().build();
+ .fallbackToDestructiveMigration()
+ .build();
+
DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
assertThat(config.requireMigration, is(false));
}
@Test
+ public void fallbackToDestructiveMigrationFrom_calledOnce_migrationsNotRequiredForValues() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigrationFrom(1, 2).build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.isMigrationRequiredFrom(1), is(false));
+ assertThat(config.isMigrationRequiredFrom(2), is(false));
+ }
+
+ @Test
+ public void fallbackToDestructiveMigrationFrom_calledTwice_migrationsNotRequiredForValues() {
+ Context context = mock(Context.class);
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigrationFrom(1, 2)
+ .fallbackToDestructiveMigrationFrom(3, 4)
+ .build();
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+
+ assertThat(config.isMigrationRequiredFrom(1), is(false));
+ assertThat(config.isMigrationRequiredFrom(2), is(false));
+ assertThat(config.isMigrationRequiredFrom(3), is(false));
+ assertThat(config.isMigrationRequiredFrom(4), is(false));
+ }
+
+ @Test
+ public void isMigrationRequiredFrom_fallBackToDestructiveCalled_alwaysReturnsFalse() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigration()
+ .build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.isMigrationRequiredFrom(0), is(false));
+ assertThat(config.isMigrationRequiredFrom(1), is(false));
+ assertThat(config.isMigrationRequiredFrom(5), is(false));
+ assertThat(config.isMigrationRequiredFrom(12), is(false));
+ assertThat(config.isMigrationRequiredFrom(132), is(false));
+ }
+
+ @Test
+ public void isMigrationRequiredFrom_byDefault_alwaysReturnsTrue() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.isMigrationRequiredFrom(0), is(true));
+ assertThat(config.isMigrationRequiredFrom(1), is(true));
+ assertThat(config.isMigrationRequiredFrom(5), is(true));
+ assertThat(config.isMigrationRequiredFrom(12), is(true));
+ assertThat(config.isMigrationRequiredFrom(132), is(true));
+ }
+
+ @Test
+ public void isMigrationRequiredFrom_fallBackToDestFromCalled_falseForProvidedValues() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigrationFrom(1, 4, 81)
+ .build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.isMigrationRequiredFrom(1), is(false));
+ assertThat(config.isMigrationRequiredFrom(4), is(false));
+ assertThat(config.isMigrationRequiredFrom(81), is(false));
+ }
+
+ @Test
+ public void isMigrationRequiredFrom_fallBackToDestFromCalled_trueForNonProvidedValues() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigrationFrom(1, 4, 81)
+ .build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.isMigrationRequiredFrom(2), is(true));
+ assertThat(config.isMigrationRequiredFrom(3), is(true));
+ assertThat(config.isMigrationRequiredFrom(73), is(true));
+ }
+
+ @Test
public void createBasic() {
Context context = mock(Context.class);
TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
@@ -163,7 +253,7 @@
}
@Override
- public void migrate(SupportSQLiteDatabase database) {
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
}
}
diff --git a/room/rxjava2/api/current.txt b/room/rxjava2/api/current.txt
new file mode 100644
index 0000000..a7cffd3
--- /dev/null
+++ b/room/rxjava2/api/current.txt
@@ -0,0 +1,14 @@
+package android.arch.persistence.room {
+
+ public class EmptyResultSetException extends java.lang.RuntimeException {
+ ctor public EmptyResultSetException(java.lang.String);
+ }
+
+ public class RxRoom {
+ ctor public RxRoom();
+ method public static io.reactivex.Flowable<java.lang.Object> createFlowable(android.arch.persistence.room.RoomDatabase, java.lang.String...);
+ field public static final java.lang.Object NOTHING;
+ }
+
+}
+
diff --git a/room/testing/api/current.txt b/room/testing/api/current.txt
new file mode 100644
index 0000000..e93487f
--- /dev/null
+++ b/room/testing/api/current.txt
@@ -0,0 +1,13 @@
+package android.arch.persistence.room.testing {
+
+ public class MigrationTestHelper extends org.junit.rules.TestWatcher {
+ ctor public MigrationTestHelper(android.app.Instrumentation, java.lang.String);
+ ctor public MigrationTestHelper(android.app.Instrumentation, java.lang.String, android.arch.persistence.db.SupportSQLiteOpenHelper.Factory);
+ method public void closeWhenFinished(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public void closeWhenFinished(android.arch.persistence.room.RoomDatabase);
+ method public android.arch.persistence.db.SupportSQLiteDatabase createDatabase(java.lang.String, int) throws java.io.IOException;
+ method public android.arch.persistence.db.SupportSQLiteDatabase runMigrationsAndValidate(java.lang.String, int, boolean, android.arch.persistence.room.migration.Migration...) throws java.io.IOException;
+ }
+
+}
+
diff --git a/room/testing/src/main/java/android/arch/persistence/room/testing/MigrationTestHelper.java b/room/testing/src/main/java/android/arch/persistence/room/testing/MigrationTestHelper.java
index 2e93bbe..013dd37 100644
--- a/room/testing/src/main/java/android/arch/persistence/room/testing/MigrationTestHelper.java
+++ b/room/testing/src/main/java/android/arch/persistence/room/testing/MigrationTestHelper.java
@@ -143,9 +143,12 @@
RoomDatabase.MigrationContainer container = new RoomDatabase.MigrationContainer();
DatabaseConfiguration configuration = new DatabaseConfiguration(
mInstrumentation.getTargetContext(), name, mOpenFactory, container, null, true,
- true);
+ true, Collections.<Integer>emptySet());
RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
new CreatingDelegate(schemaBundle.getDatabase()),
+ schemaBundle.getDatabase().getIdentityHash(),
+ // we pass the same hash twice since an old schema does not necessarily have
+ // a legacy hash and we would not even persist it.
schemaBundle.getDatabase().getIdentityHash());
return openDatabase(name, roomOpenHelper);
}
@@ -186,9 +189,12 @@
container.addMigrations(migrations);
DatabaseConfiguration configuration = new DatabaseConfiguration(
mInstrumentation.getTargetContext(), name, mOpenFactory, container, null, true,
- true);
+ true, Collections.<Integer>emptySet());
RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
new MigratingDelegate(schemaBundle.getDatabase(), validateDroppedTables),
+ // we pass the same hash twice since an old schema does not necessarily have
+ // a legacy hash and we would not even persist it.
+ schemaBundle.getDatabase().getIdentityHash(),
schemaBundle.getDatabase().getIdentityHash());
return openDatabase(name, roomOpenHelper);
}
diff --git a/samples/Support13Demos/src/main/AndroidManifest.xml b/samples/Support13Demos/src/main/AndroidManifest.xml
index af7fad2..6be99b6 100644
--- a/samples/Support13Demos/src/main/AndroidManifest.xml
+++ b/samples/Support13Demos/src/main/AndroidManifest.xml
@@ -77,13 +77,5 @@
</intent-filter>
</activity>
- <activity android:name=".view.inputmethod.CommitContentSupport"
- android:label="@string/commit_content_support">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="com.example.android.supportv13.SUPPORT13_SAMPLE_CODE" />
- </intent-filter>
- </activity>
-
</application>
</manifest>
diff --git a/samples/Support13Demos/src/main/java/com/example/android/supportv13/Shakespeare.java b/samples/Support13Demos/src/main/java/com/example/android/supportv13/Shakespeare.java
deleted file mode 100644
index 01f8dc8..0000000
--- a/samples/Support13Demos/src/main/java/com/example/android/supportv13/Shakespeare.java
+++ /dev/null
@@ -1,223 +0,0 @@
-package com.example.android.supportv13;
-
-public final class Shakespeare {
- /**
- * Our data, part 1.
- */
- public static final String[] TITLES =
- {
- "Henry IV (1)",
- "Henry V",
- "Henry VIII",
- "Richard II",
- "Richard III",
- "Merchant of Venice",
- "Othello",
- "King Lear"
- };
-
- /**
- * Our data, part 2.
- */
- public static final String[] DIALOGUE =
- {
- "So shaken as we are, so wan with care," +
- "Find we a time for frighted peace to pant," +
- "And breathe short-winded accents of new broils" +
- "To be commenced in strands afar remote." +
- "No more the thirsty entrance of this soil" +
- "Shall daub her lips with her own children's blood;" +
- "Nor more shall trenching war channel her fields," +
- "Nor bruise her flowerets with the armed hoofs" +
- "Of hostile paces: those opposed eyes," +
- "Which, like the meteors of a troubled heaven," +
- "All of one nature, of one substance bred," +
- "Did lately meet in the intestine shock" +
- "And furious close of civil butchery" +
- "Shall now, in mutual well-beseeming ranks," +
- "March all one way and be no more opposed" +
- "Against acquaintance, kindred and allies:" +
- "The edge of war, like an ill-sheathed knife," +
- "No more shall cut his master. Therefore, friends," +
- "As far as to the sepulchre of Christ," +
- "Whose soldier now, under whose blessed cross" +
- "We are impressed and engaged to fight," +
- "Forthwith a power of English shall we levy;" +
- "Whose arms were moulded in their mothers' womb" +
- "To chase these pagans in those holy fields" +
- "Over whose acres walk'd those blessed feet" +
- "Which fourteen hundred years ago were nail'd" +
- "For our advantage on the bitter cross." +
- "But this our purpose now is twelve month old," +
- "And bootless 'tis to tell you we will go:" +
- "Therefore we meet not now. Then let me hear" +
- "Of you, my gentle cousin Westmoreland," +
- "What yesternight our council did decree" +
- "In forwarding this dear expedience.",
-
- "Hear him but reason in divinity," +
- "And all-admiring with an inward wish" +
- "You would desire the king were made a prelate:" +
- "Hear him debate of commonwealth affairs," +
- "You would say it hath been all in all his study:" +
- "List his discourse of war, and you shall hear" +
- "A fearful battle render'd you in music:" +
- "Turn him to any cause of policy," +
- "The Gordian knot of it he will unloose," +
- "Familiar as his garter: that, when he speaks," +
- "The air, a charter'd libertine, is still," +
- "And the mute wonder lurketh in men's ears," +
- "To steal his sweet and honey'd sentences;" +
- "So that the art and practic part of life" +
- "Must be the mistress to this theoric:" +
- "Which is a wonder how his grace should glean it," +
- "Since his addiction was to courses vain," +
- "His companies unletter'd, rude and shallow," +
- "His hours fill'd up with riots, banquets, sports," +
- "And never noted in him any study," +
- "Any retirement, any sequestration" +
- "From open haunts and popularity.",
-
- "I come no more to make you laugh: things now," +
- "That bear a weighty and a serious brow," +
- "Sad, high, and working, full of state and woe," +
- "Such noble scenes as draw the eye to flow," +
- "We now present. Those that can pity, here" +
- "May, if they think it well, let fall a tear;" +
- "The subject will deserve it. Such as give" +
- "Their money out of hope they may believe," +
- "May here find truth too. Those that come to see" +
- "Only a show or two, and so agree" +
- "The play may pass, if they be still and willing," +
- "I'll undertake may see away their shilling" +
- "Richly in two short hours. Only they" +
- "That come to hear a merry bawdy play," +
- "A noise of targets, or to see a fellow" +
- "In a long motley coat guarded with yellow," +
- "Will be deceived; for, gentle hearers, know," +
- "To rank our chosen truth with such a show" +
- "As fool and fight is, beside forfeiting" +
- "Our own brains, and the opinion that we bring," +
- "To make that only true we now intend," +
- "Will leave us never an understanding friend." +
- "Therefore, for goodness' sake, and as you are known" +
- "The first and happiest hearers of the town," +
- "Be sad, as we would make ye: think ye see" +
- "The very persons of our noble story" +
- "As they were living; think you see them great," +
- "And follow'd with the general throng and sweat" +
- "Of thousand friends; then in a moment, see" +
- "How soon this mightiness meets misery:" +
- "And, if you can be merry then, I'll say" +
- "A man may weep upon his wedding-day.",
-
- "First, heaven be the record to my speech!" +
- "In the devotion of a subject's love," +
- "Tendering the precious safety of my prince," +
- "And free from other misbegotten hate," +
- "Come I appellant to this princely presence." +
- "Now, Thomas Mowbray, do I turn to thee," +
- "And mark my greeting well; for what I speak" +
- "My body shall make good upon this earth," +
- "Or my divine soul answer it in heaven." +
- "Thou art a traitor and a miscreant," +
- "Too good to be so and too bad to live," +
- "Since the more fair and crystal is the sky," +
- "The uglier seem the clouds that in it fly." +
- "Once more, the more to aggravate the note," +
- "With a foul traitor's name stuff I thy throat;" +
- "And wish, so please my sovereign, ere I move," +
- "What my tongue speaks my right drawn sword may prove.",
-
- "Now is the winter of our discontent" +
- "Made glorious summer by this sun of York;" +
- "And all the clouds that lour'd upon our house" +
- "In the deep bosom of the ocean buried." +
- "Now are our brows bound with victorious wreaths;" +
- "Our bruised arms hung up for monuments;" +
- "Our stern alarums changed to merry meetings," +
- "Our dreadful marches to delightful measures." +
- "Grim-visaged war hath smooth'd his wrinkled front;" +
- "And now, instead of mounting barded steeds" +
- "To fright the souls of fearful adversaries," +
- "He capers nimbly in a lady's chamber" +
- "To the lascivious pleasing of a lute." +
- "But I, that am not shaped for sportive tricks," +
- "Nor made to court an amorous looking-glass;" +
- "I, that am rudely stamp'd, and want love's majesty" +
- "To strut before a wanton ambling nymph;" +
- "I, that am curtail'd of this fair proportion," +
- "Cheated of feature by dissembling nature," +
- "Deformed, unfinish'd, sent before my time" +
- "Into this breathing world, scarce half made up," +
- "And that so lamely and unfashionable" +
- "That dogs bark at me as I halt by them;" +
- "Why, I, in this weak piping time of peace," +
- "Have no delight to pass away the time," +
- "Unless to spy my shadow in the sun" +
- "And descant on mine own deformity:" +
- "And therefore, since I cannot prove a lover," +
- "To entertain these fair well-spoken days," +
- "I am determined to prove a villain" +
- "And hate the idle pleasures of these days." +
- "Plots have I laid, inductions dangerous," +
- "By drunken prophecies, libels and dreams," +
- "To set my brother Clarence and the king" +
- "In deadly hate the one against the other:" +
- "And if King Edward be as true and just" +
- "As I am subtle, false and treacherous," +
- "This day should Clarence closely be mew'd up," +
- "About a prophecy, which says that 'G'" +
- "Of Edward's heirs the murderer shall be." +
- "Dive, thoughts, down to my soul: here" +
- "Clarence comes.",
-
- "To bait fish withal: if it will feed nothing else," +
- "it will feed my revenge. He hath disgraced me, and" +
- "hindered me half a million; laughed at my losses," +
- "mocked at my gains, scorned my nation, thwarted my" +
- "bargains, cooled my friends, heated mine" +
- "enemies; and what's his reason? I am a Jew. Hath" +
- "not a Jew eyes? hath not a Jew hands, organs," +
- "dimensions, senses, affections, passions? fed with" +
- "the same food, hurt with the same weapons, subject" +
- "to the same diseases, healed by the same means," +
- "warmed and cooled by the same winter and summer, as" +
- "a Christian is? If you prick us, do we not bleed?" +
- "if you tickle us, do we not laugh? if you poison" +
- "us, do we not die? and if you wrong us, shall we not" +
- "revenge? If we are like you in the rest, we will" +
- "resemble you in that. If a Jew wrong a Christian," +
- "what is his humility? Revenge. If a Christian" +
- "wrong a Jew, what should his sufferance be by" +
- "Christian example? Why, revenge. The villany you" +
- "teach me, I will execute, and it shall go hard but I" +
- "will better the instruction.",
-
- "Virtue! a fig! 'tis in ourselves that we are thus" +
- "or thus. Our bodies are our gardens, to the which" +
- "our wills are gardeners: so that if we will plant" +
- "nettles, or sow lettuce, set hyssop and weed up" +
- "thyme, supply it with one gender of herbs, or" +
- "distract it with many, either to have it sterile" +
- "with idleness, or manured with industry, why, the" +
- "power and corrigible authority of this lies in our" +
- "wills. If the balance of our lives had not one" +
- "scale of reason to poise another of sensuality, the" +
- "blood and baseness of our natures would conduct us" +
- "to most preposterous conclusions: but we have" +
- "reason to cool our raging motions, our carnal" +
- "stings, our unbitted lusts, whereof I take this that" +
- "you call love to be a sect or scion.",
-
- "Blow, winds, and crack your cheeks! rage! blow!" +
- "You cataracts and hurricanoes, spout" +
- "Till you have drench'd our steeples, drown'd the cocks!" +
- "You sulphurous and thought-executing fires," +
- "Vaunt-couriers to oak-cleaving thunderbolts," +
- "Singe my white head! And thou, all-shaking thunder," +
- "Smite flat the thick rotundity o' the world!" +
- "Crack nature's moulds, an germens spill at once," +
- "That make ingrateful man!"
- };
-}
diff --git a/samples/Support13Demos/src/main/java/com/example/android/supportv13/view/CheckableFrameLayout.java b/samples/Support13Demos/src/main/java/com/example/android/supportv13/view/CheckableFrameLayout.java
deleted file mode 100644
index e2383de..0000000
--- a/samples/Support13Demos/src/main/java/com/example/android/supportv13/view/CheckableFrameLayout.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.example.android.supportv13.view;
-
-import android.content.Context;
-import android.graphics.drawable.ColorDrawable;
-import android.util.AttributeSet;
-import android.widget.Checkable;
-import android.widget.FrameLayout;
-
-public class CheckableFrameLayout extends FrameLayout implements Checkable {
- private boolean mChecked;
-
- public CheckableFrameLayout(Context context) {
- super(context);
- }
-
- public CheckableFrameLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public void setChecked(boolean checked) {
- mChecked = checked;
- setBackgroundDrawable(checked ? new ColorDrawable(0xff0000a0) : null);
- }
-
- @Override
- public boolean isChecked() {
- return mChecked;
- }
-
- @Override
- public void toggle() {
- setChecked(!mChecked);
- }
-
-}
diff --git a/samples/Support13Demos/src/main/res/drawable-hdpi/alert_dialog_icon.png b/samples/Support13Demos/src/main/res/drawable-hdpi/alert_dialog_icon.png
deleted file mode 100755
index fe54477..0000000
--- a/samples/Support13Demos/src/main/res/drawable-hdpi/alert_dialog_icon.png
+++ /dev/null
Binary files differ
diff --git a/samples/Support13Demos/src/main/res/drawable-mdpi/alert_dialog_icon.png b/samples/Support13Demos/src/main/res/drawable-mdpi/alert_dialog_icon.png
deleted file mode 100644
index 0a7de04..0000000
--- a/samples/Support13Demos/src/main/res/drawable-mdpi/alert_dialog_icon.png
+++ /dev/null
Binary files differ
diff --git a/samples/Support13Demos/src/main/res/layout/simple_list_item_checkable_1.xml b/samples/Support13Demos/src/main/res/layout/simple_list_item_checkable_1.xml
deleted file mode 100644
index 84017a6..0000000
--- a/samples/Support13Demos/src/main/res/layout/simple_list_item_checkable_1.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2011 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<com.example.android.supportv4.view.CheckableFrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
- <TextView
- android:id="@android:id/text1"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textAppearance="?android:attr/textAppearanceLarge"
- android:minHeight="?android:attr/listPreferredItemHeight"
- android:gravity="center_vertical"
- />
-</com.example.android.supportv4.view.CheckableFrameLayout>
diff --git a/samples/Support13Demos/src/main/res/values/colors.xml b/samples/Support13Demos/src/main/res/values/colors.xml
deleted file mode 100644
index e50c7a0..0000000
--- a/samples/Support13Demos/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2007 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT 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>
- <drawable name="red">#7f00</drawable>
- <drawable name="blue">#770000ff</drawable>
- <drawable name="green">#7700ff00</drawable>
- <drawable name="yellow">#77ffff00</drawable>
-</resources>
diff --git a/samples/Support13Demos/src/main/res/values/strings.xml b/samples/Support13Demos/src/main/res/values/strings.xml
index b0950b2..7c621e9 100644
--- a/samples/Support13Demos/src/main/res/values/strings.xml
+++ b/samples/Support13Demos/src/main/res/values/strings.xml
@@ -20,11 +20,6 @@
<string name="hello_world"><b>Hello, <i>World!</i></b></string>
<string name="retained">Retained state</string>
- <string name="alert_dialog_two_buttons_title">
- Lorem ipsum dolor sit aie consectetur adipiscing\nPlloaso mako nuto
- siwuf cakso dodtos anr koop.
- </string>
-
<string name="fragment_nesting_pager_support">Fragment/Nesting Pager</string>
<string name="fragment_nesting_state_pager_support">Fragment/Nesting State Pager</string>
@@ -34,9 +29,4 @@
<string name="last">Last</string>
<string name="fragment_state_pager_support">Fragment/State Pager</string>
-
- <string name="action_bar_tabs_pager">Fragment/Action Bar Tabs Pager</string>
-
- <string name="commit_content_support">View/Input Method/Commit Content</string>
-
</resources>
diff --git a/samples/Support4Demos/src/main/AndroidManifest.xml b/samples/Support4Demos/src/main/AndroidManifest.xml
index 812d4e8..66f34dc 100644
--- a/samples/Support4Demos/src/main/AndroidManifest.xml
+++ b/samples/Support4Demos/src/main/AndroidManifest.xml
@@ -424,6 +424,14 @@
</provider>
<!-- END_INCLUDE(file_provider_declaration) -->
+ <activity android:name=".view.inputmethod.CommitContentSupport"
+ android:label="@string/commit_content_support">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="com.example.android.supportv13.SUPPORT13_SAMPLE_CODE" />
+ </intent-filter>
+ </activity>
+
<!-- MediaBrowserCompat Sample -->
<activity android:name=".media.MediaBrowserSupport"
android:label="@string/media_browser_support">
diff --git a/samples/Support13Demos/src/main/java/com/example/android/supportv13/view/inputmethod/CommitContentSupport.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/inputmethod/CommitContentSupport.java
similarity index 93%
rename from samples/Support13Demos/src/main/java/com/example/android/supportv13/view/inputmethod/CommitContentSupport.java
rename to samples/Support4Demos/src/main/java/com/example/android/supportv4/view/inputmethod/CommitContentSupport.java
index 78d64cd..335fa76 100644
--- a/samples/Support13Demos/src/main/java/com/example/android/supportv13/view/inputmethod/CommitContentSupport.java
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/inputmethod/CommitContentSupport.java
@@ -11,22 +11,19 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
*/
-package com.example.android.supportv13.view.inputmethod;
-
-import com.example.android.supportv13.R;
-
-import android.support.v13.view.inputmethod.EditorInfoCompat;
-import android.support.v13.view.inputmethod.InputConnectionCompat;
-import android.support.v13.view.inputmethod.InputContentInfoCompat;
+package com.example.android.supportv4.view.inputmethod;
import android.app.Activity;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
+import android.support.v13.view.inputmethod.EditorInfoCompat;
+import android.support.v13.view.inputmethod.InputConnectionCompat;
+import android.support.v13.view.inputmethod.InputContentInfoCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.inputmethod.EditorInfo;
@@ -36,14 +33,18 @@
import android.widget.LinearLayout;
import android.widget.TextView;
+import com.example.android.supportv4.R;
+
import java.util.ArrayList;
import java.util.Arrays;
+/**
+ * Demo activity for using {@link InputConnectionCompat}.
+ */
public class CommitContentSupport extends Activity {
private static final String INPUT_CONTENT_INFO_KEY = "COMMIT_CONTENT_INPUT_CONTENT_INFO";
private static final String COMMIT_CONTENT_FLAGS_KEY = "COMMIT_CONTENT_FLAGS";
-
- private static String TAG = "CommitContentSupport";
+ private static final String TAG = "CommitContentSupport";
private WebView mWebView;
private TextView mLabel;
@@ -108,7 +109,7 @@
}
private boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags,
- Bundle opts, String[] contentMimeTypes) {
+ String[] contentMimeTypes) {
// Clear the temporary permission (if any). See below about why we do this here.
try {
if (mCurrentInputContentInfo != null) {
@@ -211,14 +212,9 @@
final InputConnection ic = super.onCreateInputConnection(editorInfo);
EditorInfoCompat.setContentMimeTypes(editorInfo, mimeTypes);
final InputConnectionCompat.OnCommitContentListener callback =
- new InputConnectionCompat.OnCommitContentListener() {
- @Override
- public boolean onCommitContent(InputContentInfoCompat inputContentInfo,
- int flags, Bundle opts) {
- return CommitContentSupport.this.onCommitContent(
- inputContentInfo, flags, opts, mimeTypes);
- }
- };
+ (inputContentInfo, flags, opts) ->
+ CommitContentSupport.this.onCommitContent(
+ inputContentInfo, flags, mimeTypes);
return InputConnectionCompat.createWrapper(ic, editorInfo, callback);
}
};
diff --git a/samples/Support13Demos/src/main/res/layout/commit_content.xml b/samples/Support4Demos/src/main/res/layout/commit_content.xml
similarity index 100%
rename from samples/Support13Demos/src/main/res/layout/commit_content.xml
rename to samples/Support4Demos/src/main/res/layout/commit_content.xml
diff --git a/samples/Support4Demos/src/main/res/values/strings.xml b/samples/Support4Demos/src/main/res/values/strings.xml
index 83404e8..86adb94 100644
--- a/samples/Support4Demos/src/main/res/values/strings.xml
+++ b/samples/Support4Demos/src/main/res/values/strings.xml
@@ -241,4 +241,6 @@
<string name="drawable_compat_no_tint">Not tint</string>
<string name="drawable_compat_color_tint">Color tint</string>
<string name="drawable_compat_color_list_tint">Color state list</string>
+
+ <string name="commit_content_support">View/Input Method/Commit Content</string>
</resources>
diff --git a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/ListItemActivity.java b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/ListItemActivity.java
index 043a836..6594928 100644
--- a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/ListItemActivity.java
+++ b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/ListItemActivity.java
@@ -57,6 +57,7 @@
new SampleProvider(this), ListItemAdapter.BackgroundStyle.PANEL);
mPagedListView.setAdapter(adapter);
mPagedListView.setMaxPages(PagedListView.UNLIMITED_PAGES);
+ mPagedListView.setDividerVisibilityManager(adapter);
}
private static class SampleProvider extends ListItemProvider {
@@ -101,6 +102,12 @@
.build());
mItems.add(new ListItem.Builder(mContext)
+ .withPrimaryActionIcon(android.R.drawable.sym_def_app_icon, false)
+ .withTitle("single line without a list divider")
+ .withDividerHidden()
+ .build());
+
+ mItems.add(new ListItem.Builder(mContext)
.withOnClickListener(mOnClickListener)
.withPrimaryActionEmptyIcon()
.withTitle("clickable single line with empty icon and end icon no divider")
@@ -114,6 +121,13 @@
.build());
mItems.add(new ListItem.Builder(mContext)
+ .withTitle("Subtitle-like line without a list divider")
+ .withDividerHidden()
+ .withViewBinder(viewHolder ->
+ viewHolder.getTitle().setTextAppearance(R.style.CarListSubtitle))
+ .build());
+
+ mItems.add(new ListItem.Builder(mContext)
.withPrimaryActionNoIcon()
.withTitle("single line with two actions and no divider")
.withActions("action 1", false,
@@ -213,6 +227,14 @@
.withBody("Only body - no title. " + mContext.getString(R.string.long_text))
.build());
+ mItems.add(new ListItem.Builder(mContext)
+ .withTitle("Switch - initially unchecked")
+ .withSwitch(false, true, (button, isChecked) -> {
+ Toast.makeText(mContext,
+ isChecked ? "checked" : "unchecked", Toast.LENGTH_SHORT).show();
+ })
+ .build());
+
mListProvider = new ListItemProvider.ListProvider(mItems);
}
diff --git a/samples/SupportCarDemos/src/main/res/values/styles.xml b/samples/SupportCarDemos/src/main/res/values/styles.xml
new file mode 100644
index 0000000..80074d8
--- /dev/null
+++ b/samples/SupportCarDemos/src/main/res/values/styles.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="CarListSubtitle" parent="CarTitle">
+ <item name="android:textColor">@color/car_highlight</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/samples/SupportCarDemos/src/main/res/values/themes.xml b/samples/SupportCarDemos/src/main/res/values/themes.xml
index 4b82ecd..068da60 100644
--- a/samples/SupportCarDemos/src/main/res/values/themes.xml
+++ b/samples/SupportCarDemos/src/main/res/values/themes.xml
@@ -18,14 +18,15 @@
<!-- The main theme for all activities within the Car Demo. -->
<style name="CarTheme" parent="android:Theme.Material.Light">
<item name="android:windowBackground">@color/car_grey_50</item>
+ <item name="android:windowLightStatusBar">true</item>
<item name="android:colorAccent">@color/car_yellow_500</item>
<item name="android:colorPrimary">@color/car_highlight</item>
<item name="android:colorPrimaryDark">@color/car_grey_300</item>
- <item name="android:buttonStyle">@style/CarButton</item>
- <item name="android:borderlessButtonStyle">@style/CarButton.Borderless</item>
+ <item name="android:buttonStyle">@style/Widget.Car.Button</item>
+ <item name="android:borderlessButtonStyle">@style/Widget.Car.Button.Borderless.Colored</item>
<item name="android:progressBarStyleHorizontal">
- @style/CarProgressBar.Horizontal
+ @style/Widget.Car.ProgressBar.Horizontal
</item>
- <item name="android:windowLightStatusBar">true</item>
+ <item name="android:colorControlActivated">@color/car_accent</item>
</style>
</resources>
diff --git a/tv-provider/src/main/java/android/support/media/tv/BasePreviewProgram.java b/tv-provider/src/main/java/android/support/media/tv/BasePreviewProgram.java
index eeaa5ea..816b1a1 100644
--- a/tv-provider/src/main/java/android/support/media/tv/BasePreviewProgram.java
+++ b/tv-provider/src/main/java/android/support/media/tv/BasePreviewProgram.java
@@ -15,6 +15,7 @@
*/
package android.support.media.tv;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.content.ContentValues;
@@ -39,6 +40,7 @@
*
* @hide
*/
+@RestrictTo(LIBRARY)
public abstract class BasePreviewProgram extends BaseProgram {
/**
* @hide
diff --git a/tv-provider/src/main/java/android/support/media/tv/BaseProgram.java b/tv-provider/src/main/java/android/support/media/tv/BaseProgram.java
index 23b5cf9..4c7882d 100644
--- a/tv-provider/src/main/java/android/support/media/tv/BaseProgram.java
+++ b/tv-provider/src/main/java/android/support/media/tv/BaseProgram.java
@@ -15,6 +15,7 @@
*/
package android.support.media.tv;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.content.ContentValues;
@@ -37,6 +38,7 @@
* {@link TvContractCompat}.
* @hide
*/
+@RestrictTo(LIBRARY)
public abstract class BaseProgram {
/**
* @hide
diff --git a/tv-provider/src/main/java/android/support/media/tv/TvContractCompat.java b/tv-provider/src/main/java/android/support/media/tv/TvContractCompat.java
index de4fd04..bd03bf1 100644
--- a/tv-provider/src/main/java/android/support/media/tv/TvContractCompat.java
+++ b/tv-provider/src/main/java/android/support/media/tv/TvContractCompat.java
@@ -2422,6 +2422,7 @@
/** Canonical genres for TV programs. */
public static final class Genres {
/** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@StringDef({
FAMILY_KIDS,
SPORTS,
diff --git a/v13/api/27.0.0.ignore b/v13/api/27.0.0.ignore
new file mode 100644
index 0000000..c578d34
--- /dev/null
+++ b/v13/api/27.0.0.ignore
@@ -0,0 +1,5 @@
+05913a0
+0d6069f
+ddf372b
+e6a9e7f
+454dbc1
diff --git a/v13/api/current.txt b/v13/api/current.txt
index 1d9fdbf..fd72d5c 100644
--- a/v13/api/current.txt
+++ b/v13/api/current.txt
@@ -1,8 +1,7 @@
package android.support.v13.app {
- public class ActivityCompat extends android.support.v4.app.ActivityCompat {
- ctor protected ActivityCompat();
- method public static android.support.v13.view.DragAndDropPermissionsCompat requestDragAndDropPermissions(android.app.Activity, android.view.DragEvent);
+ public deprecated class ActivityCompat extends android.support.v4.app.ActivityCompat {
+ ctor protected deprecated ActivityCompat();
}
public deprecated class FragmentCompat {
@@ -68,59 +67,8 @@
package android.support.v13.view {
- public final class DragAndDropPermissionsCompat {
- method public void release();
- }
-
- public class DragStartHelper {
- ctor public DragStartHelper(android.view.View, android.support.v13.view.DragStartHelper.OnDragStartListener);
- method public void attach();
- method public void detach();
- method public void getTouchPosition(android.graphics.Point);
- method public boolean onLongClick(android.view.View);
- method public boolean onTouch(android.view.View, android.view.MotionEvent);
- }
-
- public static abstract interface DragStartHelper.OnDragStartListener {
- method public abstract boolean onDragStart(android.view.View, android.support.v13.view.DragStartHelper);
- }
-
public deprecated class ViewCompat extends android.support.v4.view.ViewCompat {
}
}
-package android.support.v13.view.inputmethod {
-
- public final class EditorInfoCompat {
- ctor public EditorInfoCompat();
- method public static java.lang.String[] getContentMimeTypes(android.view.inputmethod.EditorInfo);
- method public static void setContentMimeTypes(android.view.inputmethod.EditorInfo, java.lang.String[]);
- field public static final int IME_FLAG_FORCE_ASCII = -2147483648; // 0x80000000
- field public static final int IME_FLAG_NO_PERSONALIZED_LEARNING = 16777216; // 0x1000000
- }
-
- public final class InputConnectionCompat {
- ctor public InputConnectionCompat();
- method public static boolean commitContent(android.view.inputmethod.InputConnection, android.view.inputmethod.EditorInfo, android.support.v13.view.inputmethod.InputContentInfoCompat, int, android.os.Bundle);
- method public static android.view.inputmethod.InputConnection createWrapper(android.view.inputmethod.InputConnection, android.view.inputmethod.EditorInfo, android.support.v13.view.inputmethod.InputConnectionCompat.OnCommitContentListener);
- field public static int INPUT_CONTENT_GRANT_READ_URI_PERMISSION;
- }
-
- public static abstract interface InputConnectionCompat.OnCommitContentListener {
- method public abstract boolean onCommitContent(android.support.v13.view.inputmethod.InputContentInfoCompat, int, android.os.Bundle);
- }
-
- public final class InputContentInfoCompat {
- ctor public InputContentInfoCompat(android.net.Uri, android.content.ClipDescription, android.net.Uri);
- method public android.net.Uri getContentUri();
- method public android.content.ClipDescription getDescription();
- method public android.net.Uri getLinkUri();
- method public void releasePermission();
- method public void requestPermission();
- method public java.lang.Object unwrap();
- method public static android.support.v13.view.inputmethod.InputContentInfoCompat wrap(java.lang.Object);
- }
-
-}
-
diff --git a/v13/build.gradle b/v13/build.gradle
index 88e0bc0..9965d1d 100644
--- a/v13/build.gradle
+++ b/v13/build.gradle
@@ -1,4 +1,3 @@
-import static android.support.dependencies.DependenciesKt.*
import android.support.LibraryGroups
import android.support.LibraryVersions
@@ -9,11 +8,6 @@
dependencies {
api(project(":support-annotations"))
api(project(":support-v4"))
-
- androidTestImplementation(TEST_RUNNER)
- androidTestImplementation(ESPRESSO_CORE)
- androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
}
android {
diff --git a/v13/java/android/support/v13/app/ActivityCompat.java b/v13/java/android/support/v13/app/ActivityCompat.java
index b0c3c30..7c1546a 100644
--- a/v13/java/android/support/v13/app/ActivityCompat.java
+++ b/v13/java/android/support/v13/app/ActivityCompat.java
@@ -16,32 +16,22 @@
package android.support.v13.app;
-import android.app.Activity;
-import android.support.v13.view.DragAndDropPermissionsCompat;
-import android.view.DragEvent;
-
/**
* Helper for accessing features in {@link android.app.Activity} in a backwards compatible fashion.
+ *
+ * @deprecated Use {@link android.support.v4.app.ActivityCompat
+ * android.support.v4.app.ActivityCompat}.
*/
+@Deprecated
public class ActivityCompat extends android.support.v4.app.ActivityCompat {
-
- /**
- * Create {@link DragAndDropPermissionsCompat} object bound to this activity and controlling
- * the access permissions for content URIs associated with the {@link android.view.DragEvent}.
- * @param dragEvent Drag event to request permission for
- * @return The {@link DragAndDropPermissionsCompat} object used to control access to the content
- * URIs. {@code null} if no content URIs are associated with the event or if permissions could
- * not be granted.
- */
- public static DragAndDropPermissionsCompat requestDragAndDropPermissions(Activity activity,
- DragEvent dragEvent) {
- return DragAndDropPermissionsCompat.request(activity, dragEvent);
- }
-
/**
* This class should not be instantiated, but the constructor must be
* visible for the class to be extended.
+ *
+ * @deprecated Use {@link android.support.v4.app.ActivityCompat
+ * android.support.v4.app.ActivityCompat}.
*/
+ @Deprecated
protected ActivityCompat() {
// Not publicly instantiable, but may be extended.
}
diff --git a/v13/tests/AndroidManifest.xml b/v13/tests/AndroidManifest.xml
deleted file mode 100644
index 3097555..0000000
--- a/v13/tests/AndroidManifest.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2016 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.support.v13.test">
- <uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
-
- <application>
- <activity android:name="android.support.v13.app.FragmentCompatTestActivity" />
- <activity android:name="android.support.v13.view.DragStartHelperTestActivity"/>
- </application>
-
-</manifest>
diff --git a/v13/tests/NO_DOCS b/v13/tests/NO_DOCS
deleted file mode 100644
index 092a39c..0000000
--- a/v13/tests/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/v13/tests/java/android/support/v13/app/FragmentCompatTest.java b/v13/tests/java/android/support/v13/app/FragmentCompatTest.java
deleted file mode 100644
index 34b01f1..0000000
--- a/v13/tests/java/android/support/v13/app/FragmentCompatTest.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.v13.app;
-
-import static org.mockito.AdditionalMatchers.aryEq;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.same;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-
-import android.Manifest;
-import android.app.Activity;
-import android.app.Fragment;
-import android.os.Build;
-import android.support.annotation.NonNull;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SdkSuppress;
-import android.support.test.filters.SmallTest;
-import android.support.test.rule.ActivityTestRule;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.v13.app.FragmentCompat.PermissionCompatDelegate;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.HONEYCOMB)
-public class FragmentCompatTest {
- @Rule
- public ActivityTestRule<FragmentCompatTestActivity> mActivityRule =
- new ActivityTestRule<>(FragmentCompatTestActivity.class);
-
- private Activity mActivity;
- private TestFragment mFragment;
-
- @Before
- public void setup() throws Throwable {
- mActivity = mActivityRule.getActivity();
- mFragment = attachTestFragment();
- }
-
- @SmallTest
- @Test
- public void testFragmentCompatDelegate() {
- FragmentCompat.PermissionCompatDelegate delegate = mock(PermissionCompatDelegate.class);
-
- // First test setting the delegate
- FragmentCompat.setPermissionCompatDelegate(delegate);
-
- FragmentCompat.requestPermissions(mFragment, new String[]{
- Manifest.permission.ACCESS_FINE_LOCATION}, 42);
- verify(delegate).requestPermissions(same(mFragment),
- aryEq(new String[]{Manifest.permission.ACCESS_FINE_LOCATION}), eq(42));
-
- // Now test clearing the delegate
- FragmentCompat.setPermissionCompatDelegate(null);
-
- FragmentCompat.requestPermissions(mFragment, new String[]{
- Manifest.permission.ACCESS_FINE_LOCATION}, 42);
-
- verifyNoMoreInteractions(delegate);
- }
-
- private TestFragment attachTestFragment() throws Throwable {
- final TestFragment fragment = new TestFragment();
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.getFragmentManager().beginTransaction()
- .add(fragment, null)
- .addToBackStack(null)
- .commitAllowingStateLoss();
- mActivity.getFragmentManager().executePendingTransactions();
- }
- });
- InstrumentationRegistry.getInstrumentation().waitForIdleSync();
- return fragment;
- }
-
- /**
- * Empty class to satisfy java class dependency.
- */
- public static class TestFragment extends Fragment implements
- FragmentCompat.OnRequestPermissionsResultCallback {
- @Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
- @NonNull int[] grantResults) {}
- }
-}
diff --git a/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatButton.java b/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatButton.java
index 478d42d..fa35e21 100644
--- a/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatButton.java
+++ b/v7/appcompat/src/main/java/android/support/v7/widget/AppCompatButton.java
@@ -32,6 +32,7 @@
import android.support.v4.widget.TextViewCompat;
import android.support.v7.appcompat.R;
import android.util.AttributeSet;
+import android.view.ActionMode;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Button;
@@ -358,4 +359,13 @@
mTextHelper.setAllCaps(allCaps);
}
}
+
+ /**
+ * See
+ * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
+ */
+ @Override
+ public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+ TextViewCompat.setCustomSelectionActionModeCallback(this, actionModeCallback);
+ }
}
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 dca409c..415453d 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
@@ -18,8 +18,10 @@
import android.content.Context;
import android.support.annotation.DrawableRes;
+import android.support.v4.widget.TextViewCompat;
import android.support.v7.content.res.AppCompatResources;
import android.util.AttributeSet;
+import android.view.ActionMode;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.CheckedTextView;
@@ -87,4 +89,13 @@
return AppCompatHintHelper.onCreateInputConnection(super.onCreateInputConnection(outAttrs),
outAttrs, this);
}
+
+ /**
+ * See
+ * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
+ */
+ @Override
+ public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+ TextViewCompat.setCustomSelectionActionModeCallback(this, actionModeCallback);
+ }
}
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 6831fcb..c4f8c20 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
@@ -26,8 +26,10 @@
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.support.v4.view.TintableBackgroundView;
+import android.support.v4.widget.TextViewCompat;
import android.support.v7.appcompat.R;
import android.util.AttributeSet;
+import android.view.ActionMode;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.EditText;
@@ -167,4 +169,13 @@
return AppCompatHintHelper.onCreateInputConnection(super.onCreateInputConnection(outAttrs),
outAttrs, this);
}
+
+ /**
+ * See
+ * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
+ */
+ @Override
+ public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+ TextViewCompat.setCustomSelectionActionModeCallback(this, actionModeCallback);
+ }
}
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 d813277..40ca778 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,7 @@
import android.support.v4.widget.TextViewCompat;
import android.support.v7.appcompat.R;
import android.util.AttributeSet;
+import android.view.ActionMode;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.TextView;
@@ -369,4 +370,13 @@
return AppCompatHintHelper.onCreateInputConnection(super.onCreateInputConnection(outAttrs),
outAttrs, this);
}
+
+ /**
+ * See
+ * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
+ */
+ @Override
+ public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+ TextViewCompat.setCustomSelectionActionModeCallback(this, actionModeCallback);
+ }
}
diff --git a/v7/appcompat/src/main/java/android/support/v7/widget/ButtonBarLayout.java b/v7/appcompat/src/main/java/android/support/v7/widget/ButtonBarLayout.java
index b47a568..f4bbc6c 100644
--- a/v7/appcompat/src/main/java/android/support/v7/widget/ButtonBarLayout.java
+++ b/v7/appcompat/src/main/java/android/support/v7/widget/ButtonBarLayout.java
@@ -35,9 +35,6 @@
*/
@RestrictTo(LIBRARY_GROUP)
public class ButtonBarLayout extends LinearLayout {
- /** Minimum screen height required for button stacking. */
- private static final int ALLOW_STACKING_MIN_HEIGHT_DP = 320;
-
/** Amount of the second button to "peek" above the fold when stacked. */
private static final int PEEK_BUTTON_DP = 16;
@@ -50,11 +47,8 @@
public ButtonBarLayout(Context context, AttributeSet attrs) {
super(context, attrs);
- final boolean allowStackingDefault =
- getResources().getConfiguration().screenHeightDp >= ALLOW_STACKING_MIN_HEIGHT_DP;
final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ButtonBarLayout);
- mAllowStacking = ta.getBoolean(R.styleable.ButtonBarLayout_allowStacking,
- allowStackingDefault);
+ mAllowStacking = ta.getBoolean(R.styleable.ButtonBarLayout_allowStacking, true);
ta.recycle();
}
diff --git a/v7/appcompat/src/main/java/android/support/v7/widget/ContentFrameLayout.java b/v7/appcompat/src/main/java/android/support/v7/widget/ContentFrameLayout.java
index 1100280..f777901 100644
--- a/v7/appcompat/src/main/java/android/support/v7/widget/ContentFrameLayout.java
+++ b/v7/appcompat/src/main/java/android/support/v7/widget/ContentFrameLayout.java
@@ -16,6 +16,7 @@
package android.support.v7.widget;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static android.view.View.MeasureSpec.AT_MOST;
import static android.view.View.MeasureSpec.EXACTLY;
@@ -33,6 +34,7 @@
/**
* @hide
*/
+@RestrictTo(LIBRARY)
public class ContentFrameLayout extends FrameLayout {
public interface OnAttachListener {
diff --git a/v7/appcompat/src/main/java/android/support/v7/widget/DialogTitle.java b/v7/appcompat/src/main/java/android/support/v7/widget/DialogTitle.java
index 313d748..99e5924 100644
--- a/v7/appcompat/src/main/java/android/support/v7/widget/DialogTitle.java
+++ b/v7/appcompat/src/main/java/android/support/v7/widget/DialogTitle.java
@@ -21,10 +21,12 @@
import android.content.Context;
import android.content.res.TypedArray;
import android.support.annotation.RestrictTo;
+import android.support.v4.widget.TextViewCompat;
import android.support.v7.appcompat.R;
import android.text.Layout;
import android.util.AttributeSet;
import android.util.TypedValue;
+import android.view.ActionMode;
import android.widget.TextView;
/**
@@ -78,4 +80,13 @@
}
}
}
+
+ /**
+ * See
+ * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
+ */
+ @Override
+ public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+ TextViewCompat.setCustomSelectionActionModeCallback(this, actionModeCallback);
+ }
}
\ No newline at end of file
diff --git a/v7/appcompat/src/main/java/android/support/v7/widget/LinearLayoutCompat.java b/v7/appcompat/src/main/java/android/support/v7/widget/LinearLayoutCompat.java
index f071ae4..ef68896 100644
--- a/v7/appcompat/src/main/java/android/support/v7/widget/LinearLayoutCompat.java
+++ b/v7/appcompat/src/main/java/android/support/v7/widget/LinearLayoutCompat.java
@@ -16,6 +16,7 @@
package android.support.v7.widget;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.content.Context;
@@ -559,6 +560,7 @@
* @return true if there should be a divider before the child at childIndex
* @hide Pending API consideration. Currently only used internally by the system.
*/
+ @RestrictTo(LIBRARY)
protected boolean hasDividerBeforeChildAt(int childIndex) {
if (childIndex == 0) {
return (mShowDividers & SHOW_DIVIDER_BEGINNING) != 0;
diff --git a/v7/mediarouter/tests/src/android/support/v7/app/MediaRouteChooserDialogTest.java b/v7/mediarouter/tests/src/android/support/v7/app/MediaRouteChooserDialogTest.java
index 7288968..7354037 100644
--- a/v7/mediarouter/tests/src/android/support/v7/app/MediaRouteChooserDialogTest.java
+++ b/v7/mediarouter/tests/src/android/support/v7/app/MediaRouteChooserDialogTest.java
@@ -21,6 +21,7 @@
import android.content.Context;
import android.content.res.TypedArray;
import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.LargeTest;
import android.support.test.filters.SmallTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
@@ -33,6 +34,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+@LargeTest
@RunWith(AndroidJUnit4.class)
public class MediaRouteChooserDialogTest {
diff --git a/v7/preference/lint-baseline.xml b/v7/preference/lint-baseline.xml
index 2b0d510..1f3ddab 100644
--- a/v7/preference/lint-baseline.xml
+++ b/v7/preference/lint-baseline.xml
@@ -1,5 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="4" by="lint 3.0.0-alpha9">
+<issues format="4" by="lint 3.0.0">
+
+ <issue
+ id="NewApi"
+ message="`@android:id/icon_frame` requires API level 24 (current min is 14)"
+ errorLine1=" android:id="@android:id/icon_frame""
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="res/layout-v7/expand_button.xml"
+ line="31"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="`?android:attr/textAppearanceListItemSecondary` requires API level 21 (current min is 14)"
+ errorLine1=" android:textAppearance="?android:attr/textAppearanceListItemSecondary""
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="res/layout-v7/expand_button.xml"
+ line="69"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Using theme references in XML drawables requires API level 21 (current min is 14)"
+ errorLine1=" android:tint="?android:attr/colorAccent">"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="res/drawable/ic_arrow_down_24dp.xml"
+ line="22"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="`@android:id/list_container` requires API level 24 (current min is 14)"
+ errorLine1=" android:id="@android:id/list_container""
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="res/layout/preference_list_fragment.xml"
+ line="24"
+ column="9"/>
+ </issue>
<issue
id="Suspicious0dp"
diff --git a/v7/preference/src/main/java/android/support/v7/preference/Preference.java b/v7/preference/src/main/java/android/support/v7/preference/Preference.java
index fa8461d..88262cd 100644
--- a/v7/preference/src/main/java/android/support/v7/preference/Preference.java
+++ b/v7/preference/src/main/java/android/support/v7/preference/Preference.java
@@ -16,6 +16,7 @@
package android.support.v7.preference;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.content.Context;
@@ -1297,6 +1298,7 @@
* preference was removed, modified, and re-added to a {@link PreferenceGroup}
* @hide
*/
+ @RestrictTo(LIBRARY)
public final boolean wasDetached() {
return mWasDetached;
}
@@ -1305,6 +1307,7 @@
* Clears the {@link #wasDetached()} status
* @hide
*/
+ @RestrictTo(LIBRARY)
public final void clearWasDetached() {
mWasDetached = false;
}
diff --git a/v7/recyclerview/src/main/java/android/support/v7/widget/StaggeredGridLayoutManager.java b/v7/recyclerview/src/main/java/android/support/v7/widget/StaggeredGridLayoutManager.java
index 55fb14e..4e560b4 100644
--- a/v7/recyclerview/src/main/java/android/support/v7/widget/StaggeredGridLayoutManager.java
+++ b/v7/recyclerview/src/main/java/android/support/v7/widget/StaggeredGridLayoutManager.java
@@ -16,6 +16,7 @@
package android.support.v7.widget;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_HEAD;
import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_TAIL;
@@ -2069,6 +2070,7 @@
/** @hide */
@Override
+ @RestrictTo(LIBRARY)
public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
LayoutPrefetchRegistry layoutPrefetchRegistry) {
/* This method uses the simplifying assumption that the next N items (where N = span count)
diff --git a/viewpager2/src/androidTest/java/androidx/widget/viewpager2/tests/ViewPager2Tests.java b/viewpager2/src/androidTest/java/androidx/widget/viewpager2/tests/ViewPager2Tests.java
index 7f5432b..45b42aa 100644
--- a/viewpager2/src/androidTest/java/androidx/widget/viewpager2/tests/ViewPager2Tests.java
+++ b/viewpager2/src/androidTest/java/androidx/widget/viewpager2/tests/ViewPager2Tests.java
@@ -23,6 +23,7 @@
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE;
import static android.support.v7.widget.RecyclerView.SCROLL_STATE_SETTLING;
+import static android.view.View.OVER_SCROLL_NEVER;
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.equalTo;
@@ -30,6 +31,7 @@
import android.content.Context;
import android.graphics.Color;
+import android.os.Build;
import android.support.annotation.NonNull;
import android.support.test.InstrumentationRegistry;
import android.support.test.espresso.IdlingRegistry;
@@ -105,6 +107,11 @@
public void rendersAndHandlesSwiping() throws Throwable {
final int pageCount = sColors.length;
+ if (Build.VERSION.SDK_INT < 16) { // TODO(b/71500143): remove temporary workaround
+ RecyclerView mRecyclerView = (RecyclerView) mViewPager.getChildAt(0);
+ mRecyclerView.setOverScrollMode(OVER_SCROLL_NEVER);
+ }
+
onView(withId(mViewPager.getId())).check(matches(isDisplayed()));
mActivityTestRule.runOnUiThread(new Runnable() {
@Override