Merge "Move compiler-xprocessing to compiler-processing" into androidx-master-dev
diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt
index 1a011b5..1c47c0b 100644
--- a/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt
@@ -24,6 +24,7 @@
import androidx.build.gradle.isRoot
import androidx.build.jacoco.Jacoco
import androidx.build.license.CheckExternalDependencyLicensesTask
+import androidx.build.playground.VerifyPlaygroundGradlePropertiesTask
import androidx.build.studio.StudioTask.Companion.registerStudioTask
import androidx.build.uptodatedness.TaskUpToDateValidator
import com.android.build.gradle.api.AndroidBasePlugin
@@ -76,6 +77,10 @@
tasks.register(AndroidXPlugin.CREATE_LIBRARY_BUILD_INFO_FILES_TASK)
)
+ VerifyPlaygroundGradlePropertiesTask.createIfNecessary(project)?.let {
+ buildOnServerTask.dependsOn(it)
+ }
+
extra.set("versionChecker", GMavenVersionChecker(logger))
val createArchiveTask = Release.getGlobalFullZipTask(this)
buildOnServerTask.dependsOn(createArchiveTask)
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
index 6ab319f..933f9da 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
@@ -84,7 +84,7 @@
const val MOCKITO_KOTLIN = "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0"
const val MULTIDEX = "androidx.multidex:multidex:2.0.0"
const val NULLAWAY = "com.uber.nullaway:nullaway:0.3.7"
-const val PLAY_CORE = "com.google.android.play:core:1.7.2"
+const val PLAY_CORE = "com.google.android.play:core:1.8.0"
const val REACTIVE_STREAMS = "org.reactivestreams:reactive-streams:1.0.0"
const val RX_JAVA = "io.reactivex.rxjava2:rxjava:2.2.9"
const val RX_JAVA3 = "io.reactivex.rxjava3:rxjava:3.0.0"
diff --git a/buildSrc/src/main/kotlin/androidx/build/playground/VerifyPlaygroundGradlePropertiesTask.kt b/buildSrc/src/main/kotlin/androidx/build/playground/VerifyPlaygroundGradlePropertiesTask.kt
new file mode 100644
index 0000000..8721a8f0
--- /dev/null
+++ b/buildSrc/src/main/kotlin/androidx/build/playground/VerifyPlaygroundGradlePropertiesTask.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.playground
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.Project
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.TaskAction
+import org.gradle.api.tasks.TaskProvider
+import java.io.File
+import java.util.Properties
+
+/**
+ * Compares the properties file for playground projects with the main androidx properties file
+ * to ensure playgrounds do not define any property in their own build that conflicts with the
+ * main build.
+ */
+@Suppress("UnstableApiUsage") // for fileProperty
+abstract class VerifyPlaygroundGradlePropertiesTask : DefaultTask() {
+ @get:InputFile
+ abstract val androidxProperties: RegularFileProperty
+
+ @get:InputFile
+ abstract val playgroundProperties: RegularFileProperty
+
+ @get:OutputFile
+ abstract val outputFile: RegularFileProperty
+
+ @TaskAction
+ fun compareProperties() {
+ val rootProperties = loadPropertiesFile(androidxProperties.get().asFile)
+ val playgroundProperties = loadPropertiesFile(playgroundProperties.get().asFile)
+ validateProperties(rootProperties, playgroundProperties)
+ }
+
+ private fun validateProperties(
+ rootProperties: Properties,
+ playgroundProperties: Properties
+ ) {
+ // ensure we don't define properties that do not match the root file
+ // this includes properties that are not defined in the root androidx build as they might
+ // be properties which can alter the build output. We might consider whitelisting certain
+ // properties in the future if necessary.
+ playgroundProperties.forEach {
+ val rootValue = rootProperties[it.key]
+ if (rootValue != it.value) {
+ throw GradleException(
+ """
+ ${it.key} is defined as ${it.value} in playground properties but
+ it does not match the value defined in root properties file ($rootValue).
+ Having inconsistent properties in playground projects might trigger wrong
+ compilation output in the main AndroidX build, thus not allowed.
+ """.trimIndent()
+ )
+ }
+ }
+ // put the success into an output so that task can be up to date.
+ outputFile.get().asFile.writeText("valid", Charsets.UTF_8)
+ }
+
+ private fun loadPropertiesFile(file: File) = file.inputStream().use { inputStream ->
+ Properties().apply {
+ load(inputStream)
+ }
+ }
+
+ companion object {
+ private const val TASK_NAME = "verifyPlaygroundGradleProperties"
+
+ /**
+ * Creates the task to verify playground properties if an only if we have the
+ * playground-common folder to check against.
+ */
+ fun createIfNecessary(
+ project: Project
+ ): TaskProvider<VerifyPlaygroundGradlePropertiesTask>? {
+ return if (project.projectDir.resolve("playground-common").exists()) {
+ project.tasks.register(
+ TASK_NAME,
+ VerifyPlaygroundGradlePropertiesTask::class.java
+ ) {
+ it.androidxProperties.set(
+ project.layout.projectDirectory.file("gradle.properties")
+ )
+ it.playgroundProperties.set(
+ project.layout.projectDirectory.file(
+ "playground-common/androidx-shared.properties"
+ )
+ )
+ it.outputFile.set(
+ project.layout.buildDirectory.file("playgroundPropertiesValidation.out")
+ )
+ }
+ } else {
+ null
+ }
+ }
+ }
+}
diff --git a/core/core/api/1.5.0-alpha01.txt b/core/core/api/1.5.0-alpha01.txt
index 713d200..2f34606 100644
--- a/core/core/api/1.5.0-alpha01.txt
+++ b/core/core/api/1.5.0-alpha01.txt
@@ -2678,6 +2678,7 @@
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! focusSearch(int);
method public java.util.List<androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat!>! getActionList();
method public int getActions();
+ method public java.util.List<java.lang.String!>? getAvailableExtraData();
method @Deprecated public void getBoundsInParent(android.graphics.Rect!);
method public void getBoundsInScreen(android.graphics.Rect!);
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! getChild(int);
@@ -2688,6 +2689,7 @@
method public CharSequence! getContentDescription();
method public int getDrawingOrder();
method public CharSequence! getError();
+ method public android.view.accessibility.AccessibilityNodeInfo.ExtraRenderingInfo? getExtraRenderingInfo();
method public android.os.Bundle! getExtras();
method public CharSequence? getHintText();
method @Deprecated public Object! getInfo();
@@ -2743,10 +2745,12 @@
method public boolean performAction(int, android.os.Bundle!);
method public void recycle();
method public boolean refresh();
+ method public boolean refreshWithExtraData(String?, android.os.Bundle);
method public boolean removeAction(androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat!);
method public boolean removeChild(android.view.View!);
method public boolean removeChild(android.view.View!, int);
method public void setAccessibilityFocused(boolean);
+ method public void setAvailableExtraData(java.util.List<java.lang.String!>);
method @Deprecated public void setBoundsInParent(android.graphics.Rect!);
method public void setBoundsInScreen(android.graphics.Rect!);
method public void setCanOpenPopup(boolean);
@@ -2840,6 +2844,10 @@
field public static final int ACTION_SELECT = 4; // 0x4
field public static final int ACTION_SET_SELECTION = 131072; // 0x20000
field public static final int ACTION_SET_TEXT = 2097152; // 0x200000
+ field public static final String EXTRA_DATA_RENDERING_INFO_KEY = "android.view.accessibility.extra.DATA_RENDERING_INFO_KEY";
+ field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH";
+ field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX";
+ field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_KEY";
field public static final int FOCUS_ACCESSIBILITY = 2; // 0x2
field public static final int FOCUS_INPUT = 1; // 0x1
field public static final int MOVEMENT_GRANULARITY_CHARACTER = 1; // 0x1
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index 4742c40..34d2fa0 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -2697,6 +2697,7 @@
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! focusSearch(int);
method public java.util.List<androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat!>! getActionList();
method public int getActions();
+ method public java.util.List<java.lang.String!>? getAvailableExtraData();
method @Deprecated public void getBoundsInParent(android.graphics.Rect!);
method public void getBoundsInScreen(android.graphics.Rect!);
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! getChild(int);
@@ -2707,6 +2708,7 @@
method public CharSequence! getContentDescription();
method public int getDrawingOrder();
method public CharSequence! getError();
+ method public android.view.accessibility.AccessibilityNodeInfo.ExtraRenderingInfo? getExtraRenderingInfo();
method public android.os.Bundle! getExtras();
method public CharSequence? getHintText();
method @Deprecated public Object! getInfo();
@@ -2762,10 +2764,12 @@
method public boolean performAction(int, android.os.Bundle!);
method public void recycle();
method public boolean refresh();
+ method public boolean refreshWithExtraData(String?, android.os.Bundle);
method public boolean removeAction(androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat!);
method public boolean removeChild(android.view.View!);
method public boolean removeChild(android.view.View!, int);
method public void setAccessibilityFocused(boolean);
+ method public void setAvailableExtraData(java.util.List<java.lang.String!>);
method @Deprecated public void setBoundsInParent(android.graphics.Rect!);
method public void setBoundsInScreen(android.graphics.Rect!);
method public void setCanOpenPopup(boolean);
@@ -2859,6 +2863,10 @@
field public static final int ACTION_SELECT = 4; // 0x4
field public static final int ACTION_SET_SELECTION = 131072; // 0x20000
field public static final int ACTION_SET_TEXT = 2097152; // 0x200000
+ field public static final String EXTRA_DATA_RENDERING_INFO_KEY = "android.view.accessibility.extra.DATA_RENDERING_INFO_KEY";
+ field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH";
+ field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX";
+ field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_KEY";
field public static final int FOCUS_ACCESSIBILITY = 2; // 0x2
field public static final int FOCUS_INPUT = 1; // 0x1
field public static final int MOVEMENT_GRANULARITY_CHARACTER = 1; // 0x1
diff --git a/core/core/api/public_plus_experimental_1.5.0-alpha01.txt b/core/core/api/public_plus_experimental_1.5.0-alpha01.txt
index 2e0be49..0140bc1 100644
--- a/core/core/api/public_plus_experimental_1.5.0-alpha01.txt
+++ b/core/core/api/public_plus_experimental_1.5.0-alpha01.txt
@@ -2676,6 +2676,7 @@
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! focusSearch(int);
method public java.util.List<androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat!>! getActionList();
method public int getActions();
+ method public java.util.List<java.lang.String!>? getAvailableExtraData();
method @Deprecated public void getBoundsInParent(android.graphics.Rect!);
method public void getBoundsInScreen(android.graphics.Rect!);
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! getChild(int);
@@ -2686,6 +2687,7 @@
method public CharSequence! getContentDescription();
method public int getDrawingOrder();
method public CharSequence! getError();
+ method public android.view.accessibility.AccessibilityNodeInfo.ExtraRenderingInfo? getExtraRenderingInfo();
method public android.os.Bundle! getExtras();
method public CharSequence? getHintText();
method @Deprecated public Object! getInfo();
@@ -2741,10 +2743,12 @@
method public boolean performAction(int, android.os.Bundle!);
method public void recycle();
method public boolean refresh();
+ method public boolean refreshWithExtraData(String?, android.os.Bundle);
method public boolean removeAction(androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat!);
method public boolean removeChild(android.view.View!);
method public boolean removeChild(android.view.View!, int);
method public void setAccessibilityFocused(boolean);
+ method public void setAvailableExtraData(java.util.List<java.lang.String!>);
method @Deprecated public void setBoundsInParent(android.graphics.Rect!);
method public void setBoundsInScreen(android.graphics.Rect!);
method public void setCanOpenPopup(boolean);
@@ -2838,6 +2842,10 @@
field public static final int ACTION_SELECT = 4; // 0x4
field public static final int ACTION_SET_SELECTION = 131072; // 0x20000
field public static final int ACTION_SET_TEXT = 2097152; // 0x200000
+ field public static final String EXTRA_DATA_RENDERING_INFO_KEY = "android.view.accessibility.extra.DATA_RENDERING_INFO_KEY";
+ field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH";
+ field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX";
+ field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_KEY";
field public static final int FOCUS_ACCESSIBILITY = 2; // 0x2
field public static final int FOCUS_INPUT = 1; // 0x1
field public static final int MOVEMENT_GRANULARITY_CHARACTER = 1; // 0x1
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index f33927a..42c8f98 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -2695,6 +2695,7 @@
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! focusSearch(int);
method public java.util.List<androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat!>! getActionList();
method public int getActions();
+ method public java.util.List<java.lang.String!>? getAvailableExtraData();
method @Deprecated public void getBoundsInParent(android.graphics.Rect!);
method public void getBoundsInScreen(android.graphics.Rect!);
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! getChild(int);
@@ -2705,6 +2706,7 @@
method public CharSequence! getContentDescription();
method public int getDrawingOrder();
method public CharSequence! getError();
+ method public android.view.accessibility.AccessibilityNodeInfo.ExtraRenderingInfo? getExtraRenderingInfo();
method public android.os.Bundle! getExtras();
method public CharSequence? getHintText();
method @Deprecated public Object! getInfo();
@@ -2760,10 +2762,12 @@
method public boolean performAction(int, android.os.Bundle!);
method public void recycle();
method public boolean refresh();
+ method public boolean refreshWithExtraData(String?, android.os.Bundle);
method public boolean removeAction(androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat!);
method public boolean removeChild(android.view.View!);
method public boolean removeChild(android.view.View!, int);
method public void setAccessibilityFocused(boolean);
+ method public void setAvailableExtraData(java.util.List<java.lang.String!>);
method @Deprecated public void setBoundsInParent(android.graphics.Rect!);
method public void setBoundsInScreen(android.graphics.Rect!);
method public void setCanOpenPopup(boolean);
@@ -2857,6 +2861,10 @@
field public static final int ACTION_SELECT = 4; // 0x4
field public static final int ACTION_SET_SELECTION = 131072; // 0x20000
field public static final int ACTION_SET_TEXT = 2097152; // 0x200000
+ field public static final String EXTRA_DATA_RENDERING_INFO_KEY = "android.view.accessibility.extra.DATA_RENDERING_INFO_KEY";
+ field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH";
+ field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX";
+ field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_KEY";
field public static final int FOCUS_ACCESSIBILITY = 2; // 0x2
field public static final int FOCUS_INPUT = 1; // 0x1
field public static final int MOVEMENT_GRANULARITY_CHARACTER = 1; // 0x1
diff --git a/core/core/api/restricted_1.5.0-alpha01.txt b/core/core/api/restricted_1.5.0-alpha01.txt
index 84edbd8..0dafa25 100644
--- a/core/core/api/restricted_1.5.0-alpha01.txt
+++ b/core/core/api/restricted_1.5.0-alpha01.txt
@@ -3093,6 +3093,7 @@
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! focusSearch(int);
method public java.util.List<androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat!>! getActionList();
method public int getActions();
+ method public java.util.List<java.lang.String!>? getAvailableExtraData();
method @Deprecated public void getBoundsInParent(android.graphics.Rect!);
method public void getBoundsInScreen(android.graphics.Rect!);
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! getChild(int);
@@ -3104,6 +3105,7 @@
method public CharSequence! getContentDescription();
method public int getDrawingOrder();
method public CharSequence! getError();
+ method public android.view.accessibility.AccessibilityNodeInfo.ExtraRenderingInfo? getExtraRenderingInfo();
method public android.os.Bundle! getExtras();
method public CharSequence? getHintText();
method @Deprecated public Object! getInfo();
@@ -3159,10 +3161,12 @@
method public boolean performAction(int, android.os.Bundle!);
method public void recycle();
method public boolean refresh();
+ method public boolean refreshWithExtraData(String?, android.os.Bundle);
method public boolean removeAction(androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat!);
method public boolean removeChild(android.view.View!);
method public boolean removeChild(android.view.View!, int);
method public void setAccessibilityFocused(boolean);
+ method public void setAvailableExtraData(java.util.List<java.lang.String!>);
method @Deprecated public void setBoundsInParent(android.graphics.Rect!);
method public void setBoundsInScreen(android.graphics.Rect!);
method public void setCanOpenPopup(boolean);
@@ -3256,6 +3260,10 @@
field public static final int ACTION_SELECT = 4; // 0x4
field public static final int ACTION_SET_SELECTION = 131072; // 0x20000
field public static final int ACTION_SET_TEXT = 2097152; // 0x200000
+ field public static final String EXTRA_DATA_RENDERING_INFO_KEY = "android.view.accessibility.extra.DATA_RENDERING_INFO_KEY";
+ field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH";
+ field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX";
+ field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_KEY";
field public static final int FOCUS_ACCESSIBILITY = 2; // 0x2
field public static final int FOCUS_INPUT = 1; // 0x1
field public static final int MOVEMENT_GRANULARITY_CHARACTER = 1; // 0x1
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 66fed5c..b5197212 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -3115,6 +3115,7 @@
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! focusSearch(int);
method public java.util.List<androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat!>! getActionList();
method public int getActions();
+ method public java.util.List<java.lang.String!>? getAvailableExtraData();
method @Deprecated public void getBoundsInParent(android.graphics.Rect!);
method public void getBoundsInScreen(android.graphics.Rect!);
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! getChild(int);
@@ -3126,6 +3127,7 @@
method public CharSequence! getContentDescription();
method public int getDrawingOrder();
method public CharSequence! getError();
+ method public android.view.accessibility.AccessibilityNodeInfo.ExtraRenderingInfo? getExtraRenderingInfo();
method public android.os.Bundle! getExtras();
method public CharSequence? getHintText();
method @Deprecated public Object! getInfo();
@@ -3181,10 +3183,12 @@
method public boolean performAction(int, android.os.Bundle!);
method public void recycle();
method public boolean refresh();
+ method public boolean refreshWithExtraData(String?, android.os.Bundle);
method public boolean removeAction(androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat!);
method public boolean removeChild(android.view.View!);
method public boolean removeChild(android.view.View!, int);
method public void setAccessibilityFocused(boolean);
+ method public void setAvailableExtraData(java.util.List<java.lang.String!>);
method @Deprecated public void setBoundsInParent(android.graphics.Rect!);
method public void setBoundsInScreen(android.graphics.Rect!);
method public void setCanOpenPopup(boolean);
@@ -3278,6 +3282,10 @@
field public static final int ACTION_SELECT = 4; // 0x4
field public static final int ACTION_SET_SELECTION = 131072; // 0x20000
field public static final int ACTION_SET_TEXT = 2097152; // 0x200000
+ field public static final String EXTRA_DATA_RENDERING_INFO_KEY = "android.view.accessibility.extra.DATA_RENDERING_INFO_KEY";
+ field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH";
+ field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX";
+ field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_KEY";
field public static final int FOCUS_ACCESSIBILITY = 2; // 0x2
field public static final int FOCUS_INPUT = 1; // 0x1
field public static final int MOVEMENT_GRANULARITY_CHARACTER = 1; // 0x1
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
index 6b35ae0..77b849b 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
@@ -35,6 +35,7 @@
import android.util.SparseArray;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.ExtraRenderingInfo;
import android.view.accessibility.AccessibilityNodeInfo.TouchDelegateInfo;
import androidx.annotation.IntRange;
@@ -1668,6 +1669,64 @@
*/
public static final int MOVEMENT_GRANULARITY_PAGE = 0x00000010;
+ /**
+ * Key used to request and locate extra data for text character location. This key requests that
+ * an array of {@link android.graphics.RectF}s be added to the extras. This request is made with
+ * {@link #refreshWithExtraData(String, Bundle)}. The arguments taken by this request are two
+ * integers: {@link #EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX} and
+ * {@link #EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH}. The starting index must be valid
+ * inside the CharSequence returned by {@link #getText()}, and the length must be positive.
+ * <p>
+ * The data can be retrieved from the {@code Bundle} returned by {@link #getExtras()} using this
+ * string as a key for {@link Bundle#getParcelableArray(String)}. The
+ * {@link android.graphics.RectF} will be null for characters that either do not exist or are
+ * off the screen.
+ *
+ * {@see #refreshWithExtraData(String, Bundle)}
+ */
+ @SuppressLint("ActionValue")
+ public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY =
+ "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_KEY";
+
+ /**
+ * Integer argument specifying the start index of the requested text location data. Must be
+ * valid inside the CharSequence returned by {@link #getText()}.
+ *
+ * @see #EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY
+ */
+ @SuppressLint("ActionValue")
+ public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX =
+ "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX";
+
+ /**
+ * Integer argument specifying the end index of the requested text location data. Must be
+ * positive.
+ *
+ * @see #EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY
+ */
+ @SuppressLint("ActionValue")
+ public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH =
+ "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH";
+
+ /**
+ * Key used to request extra data for the rendering information.
+ * The key requests that a {@link AccessibilityNodeInfo.ExtraRenderingInfo} be added to this
+ * info. This request is made with {@link #refreshWithExtraData(String, Bundle)} without
+ * argument.
+ * <p>
+ * The data can be retrieved from the {@link ExtraRenderingInfo} returned by
+ * {@link #getExtraRenderingInfo()} using {@link ExtraRenderingInfo#getLayoutSize},
+ * {@link ExtraRenderingInfo#getTextSizeInPx()} and
+ * {@link ExtraRenderingInfo#getTextSizeUnit()}. For layout params, it is supported by both
+ * {@link android.widget.TextView} and {@link android.view.ViewGroup}. For text size and
+ * unit, it is only supported by {@link android.widget.TextView}.
+ *
+ * @see #refreshWithExtraData(String, Bundle)
+ */
+ @SuppressLint("ActionValue")
+ public static final String EXTRA_DATA_RENDERING_INFO_KEY =
+ "android.view.accessibility.extra.DATA_RENDERING_INFO_KEY";
+
private static int sClickableSpanId = 0;
/**
@@ -3060,6 +3119,23 @@
}
/**
+ * Gets the {@link ExtraRenderingInfo extra rendering info} if the node is meant to be
+ * refreshed with extra data to examine rendering related accessibility issues.
+ *
+ * @return The {@link ExtraRenderingInfo extra rendering info}.
+ *
+ * @see #EXTRA_DATA_RENDERING_INFO_KEY
+ * @see #refreshWithExtraData(String, Bundle)
+ */
+ @Nullable
+ public ExtraRenderingInfo getExtraRenderingInfo() {
+ if (Build.VERSION.SDK_INT >= 30) {
+ return mInfo.getExtraRenderingInfo();
+ }
+ return null;
+ }
+
+ /**
* Gets the actions that can be performed on the node.
*
* @return A list of AccessibilityActions.
@@ -3649,6 +3725,47 @@
}
/**
+ * Get the extra data available for this node.
+ * <p>
+ * Some data that is useful for some accessibility services is expensive to compute, and would
+ * place undue overhead on apps to compute all the time. That data can be requested with
+ * {@link #refreshWithExtraData(String, Bundle)}.
+ *
+ * @return An unmodifiable list of keys corresponding to extra data that can be requested.
+ * @see #EXTRA_DATA_RENDERING_INFO_KEY
+ * @see #EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY
+ */
+ @Nullable
+ public List<String> getAvailableExtraData() {
+ if (Build.VERSION.SDK_INT >= 26) {
+ return mInfo.getAvailableExtraData();
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Set the extra data available for this node.
+ * <p>
+ * <strong>Note:</strong> When a {@code View} passes in a non-empty list, it promises that
+ * it will populate the node's extras with corresponding pieces of information in
+ * {@link View#addExtraDataToAccessibilityNodeInfo(AccessibilityNodeInfo, String, Bundle)}.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ *
+ * @param extraDataKeys A list of types of extra data that are available.
+ * @see #getAvailableExtraData()
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setAvailableExtraData(@NonNull List<String> extraDataKeys) {
+ if (Build.VERSION.SDK_INT >= 26) {
+ mInfo.setAvailableExtraData(extraDataKeys);
+ }
+ }
+
+ /**
* Gets the window to which this node belongs.
*
* @return The window.
@@ -3981,6 +4098,30 @@
}
/**
+ * Refreshes this info with the latest state of the view it represents, and request new
+ * data be added by the View.
+ *
+ * @param extraDataKey The extra data requested. Data that must be requested
+ * with this mechanism is generally expensive to retrieve, so should only be
+ * requested when needed. See
+ * {@link #EXTRA_DATA_RENDERING_INFO_KEY},
+ * {@link #EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY},
+ * {@link #getAvailableExtraData()} and {@link #getExtraRenderingInfo()}.
+ * @param args A bundle of arguments for the request. These depend on the particular request.
+ *
+ * @return {@code true} if the refresh succeeded. {@code false} if the {@link View} represented
+ * by this node is no longer in the view tree (and thus this node is obsolete and should be
+ * recycled).
+ */
+ public boolean refreshWithExtraData(@Nullable String extraDataKey, @NonNull Bundle args) {
+ if (Build.VERSION.SDK_INT >= 26) {
+ return mInfo.refreshWithExtraData(extraDataKey, args);
+ } else {
+ return false;
+ }
+ }
+
+ /**
* Gets the custom role description.
* @return The role description.
*/
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeProviderCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeProviderCompat.java
index 477a4fe..5e51fd1 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeProviderCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeProviderCompat.java
@@ -240,7 +240,7 @@
* the info's {@link AccessibilityNodeInfoCompat#getExtras} method.
* @param arguments A {@link Bundle} holding any arguments relevant for this request.
*
- * @see AccessibilityNodeInfo#setAvailableExtraData(List)
+ * @see AccessibilityNodeInfoCompat#setAvailableExtraData(List)
*/
public void addExtraDataToAccessibilityNodeInfo(int virtualViewId,
@NonNull AccessibilityNodeInfoCompat info, @NonNull String extraDataKey,
diff --git a/datastore/datastore-core/src/test/kotlin/androidx/datastore/DataMigrationInitializerTest.kt b/datastore/datastore-core/src/test/java/androidx/datastore/DataMigrationInitializerTest.kt
similarity index 100%
rename from datastore/datastore-core/src/test/kotlin/androidx/datastore/DataMigrationInitializerTest.kt
rename to datastore/datastore-core/src/test/java/androidx/datastore/DataMigrationInitializerTest.kt
diff --git a/datastore/datastore-core/src/test/kotlin/androidx/datastore/DataStoreFactoryTest.kt b/datastore/datastore-core/src/test/java/androidx/datastore/DataStoreFactoryTest.kt
similarity index 100%
rename from datastore/datastore-core/src/test/kotlin/androidx/datastore/DataStoreFactoryTest.kt
rename to datastore/datastore-core/src/test/java/androidx/datastore/DataStoreFactoryTest.kt
diff --git a/datastore/datastore-core/src/test/kotlin/androidx/datastore/SingleProcessDataStoreTest.kt b/datastore/datastore-core/src/test/java/androidx/datastore/SingleProcessDataStoreTest.kt
similarity index 100%
rename from datastore/datastore-core/src/test/kotlin/androidx/datastore/SingleProcessDataStoreTest.kt
rename to datastore/datastore-core/src/test/java/androidx/datastore/SingleProcessDataStoreTest.kt
diff --git a/datastore/datastore-core/src/test/kotlin/androidx/datastore/TestingSerializer.kt b/datastore/datastore-core/src/test/java/androidx/datastore/TestingSerializer.kt
similarity index 100%
rename from datastore/datastore-core/src/test/kotlin/androidx/datastore/TestingSerializer.kt
rename to datastore/datastore-core/src/test/java/androidx/datastore/TestingSerializer.kt
diff --git a/datastore/datastore-core/src/test/kotlin/androidx/datastore/handlers/ReplaceFileCorruptionHandlerTest.kt b/datastore/datastore-core/src/test/java/androidx/datastore/handlers/ReplaceFileCorruptionHandlerTest.kt
similarity index 100%
rename from datastore/datastore-core/src/test/kotlin/androidx/datastore/handlers/ReplaceFileCorruptionHandlerTest.kt
rename to datastore/datastore-core/src/test/java/androidx/datastore/handlers/ReplaceFileCorruptionHandlerTest.kt
diff --git a/datastore/datastore-sampleapp/build.gradle b/datastore/datastore-sampleapp/build.gradle
index 0072845..ff065b1 100644
--- a/datastore/datastore-sampleapp/build.gradle
+++ b/datastore/datastore-sampleapp/build.gradle
@@ -52,11 +52,13 @@
}
dependencies {
- //For DataStore with Preferences
+ // For DataStore with Preferences
implementation(project(":datastore:datastore-preferences"))
// For DataStore with protos
implementation(project(":datastore:datastore-core"))
+ api("androidx.preference:preference:1.1.0")
+
implementation(PROTOBUF_LITE)
implementation(KOTLIN_STDLIB)
diff --git a/datastore/datastore-sampleapp/src/main/AndroidManifest.xml b/datastore/datastore-sampleapp/src/main/AndroidManifest.xml
index e46b51a..28673cf 100644
--- a/datastore/datastore-sampleapp/src/main/AndroidManifest.xml
+++ b/datastore/datastore-sampleapp/src/main/AndroidManifest.xml
@@ -34,5 +34,12 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
+
+ <activity android:name=".SettingsFragmentActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
</application>
</manifest>
\ No newline at end of file
diff --git a/datastore/datastore-sampleapp/src/main/java/com/example/datastoresampleapp/SettingsFragment.kt b/datastore/datastore-sampleapp/src/main/java/com/example/datastoresampleapp/SettingsFragment.kt
new file mode 100644
index 0000000..cd4966f
--- /dev/null
+++ b/datastore/datastore-sampleapp/src/main/java/com/example/datastoresampleapp/SettingsFragment.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.datastoresampleapp
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import androidx.appcompat.app.AppCompatActivity
+import androidx.datastore.CorruptionException
+import androidx.datastore.DataStore
+import androidx.datastore.DataStoreFactory
+import androidx.datastore.Serializer
+import androidx.preference.Preference
+import androidx.preference.SwitchPreference
+import androidx.preference.PreferenceFragmentCompat
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.TwoStatePreference
+import com.google.protobuf.InvalidProtocolBufferException
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.launch
+import java.io.File
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+
+private val TAG = "SettingsActivity"
+
+class SettingsFragmentActivity() : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ supportFragmentManager.beginTransaction()
+ .replace(android.R.id.content, SettingsFragment()).commit()
+ }
+}
+
+/**
+ * Toggle States:
+ * 1) Value not read from disk. Toggle is disabled in default position.
+ * 2) Value read from disk and no pending updates. Toggle is enabled in latest persisted position.
+ * 3) Value read from disk but with pending updates. Toggle is disabled in pending position.
+ */
+class SettingsFragment() : PreferenceFragmentCompat() {
+ private val fooToggle: TwoStatePreference by lazy {
+ createFooPreference(preferenceManager.context)
+ }
+
+ private val PROTO_STORE_FILE_NAME = "datastore_test_app.pb"
+
+ private val settingsStore: DataStore<Settings> by lazy {
+ DataStoreFactory().create(
+ { File(requireActivity().applicationContext.filesDir, PROTO_STORE_FILE_NAME) },
+ SettingsSerializer
+ )
+ }
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ val preferences = preferenceManager.createPreferenceScreen(preferenceManager.context)
+ preferences.addPreference(fooToggle)
+ preferenceScreen = preferences
+ }
+
+ @SuppressLint("SyntheticAccessor")
+ @ExperimentalCoroutinesApi
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ viewLifecycleOwner.lifecycleScope.launchWhenStarted {
+ // Read the initial value from disk
+ val settings: Settings = try {
+ settingsStore.data.first()
+ } catch (ex: IOException) {
+ Log.e(TAG, "Could not read settings.", ex)
+ // Show error to user here, or try re-reading.
+ return@launchWhenStarted
+ }
+
+ // Set the toggle to the value read from disk and enable the toggle.
+ fooToggle.isChecked = settings.foo
+ fooToggle.isEnabled = true
+
+ fooToggle.changeFlow.flatMapLatest { (_: Preference?, newValue: Any?) ->
+ val isChecked = newValue as Boolean
+
+ fooToggle.isEnabled = false // Disable the toggle until the write is completed
+ fooToggle.isChecked = isChecked // Set the disabled toggle to the pending value
+
+ try {
+ settingsStore.setFoo(isChecked)
+ } catch (ex: IOException) { // setFoo can only throw IOExceptions
+ Log.e(TAG, "Could not write settings", ex)
+ // Show error to user here
+ }
+ settingsStore.data // Switch to data flow since it is the source of truth.
+ }.collect {
+ // We update the toggle to the latest persisted value - whether or not the
+ // update succeeded. If the write failed, this will reset to original state.
+ fooToggle.isChecked = it.foo
+ fooToggle.isEnabled = true
+ }
+ }
+ }
+
+ private suspend fun DataStore<Settings>.setFoo(foo: Boolean) = updateData {
+ it.toBuilder().setFoo(foo).build()
+ }
+
+ private fun createFooPreference(context: Context) = SwitchPreference(context).apply {
+ isEnabled = false // Start out disabled
+ isPersistent = false // Disable SharedPreferences
+ title = "Foo title"
+ summary = "Summary of Foo toggle"
+ }
+}
+
+@ExperimentalCoroutinesApi
+private val Preference.changeFlow: Flow<Pair<Preference?, Any?>>
+ get() = callbackFlow {
+ this@changeFlow.setOnPreferenceChangeListener { preference: Preference?, newValue: Any? ->
+ this@callbackFlow.launch {
+ send(Pair(preference, newValue))
+ }
+ false // Do not update the state of the toggle.
+ }
+
+ awaitClose { this@changeFlow.onPreferenceChangeListener = null }
+ }
+
+private object SettingsSerializer : Serializer<Settings> {
+ override fun readFrom(input: InputStream): Settings {
+ try {
+ return Settings.parseFrom(input)
+ } catch (ipbe: InvalidProtocolBufferException) {
+ throw CorruptionException("Cannot read proto.", ipbe)
+ }
+ }
+
+ override fun writeTo(t: Settings, output: OutputStream) = t.writeTo(output)
+}
\ No newline at end of file
diff --git a/datastore/datastore-sampleapp/src/main/proto/settings.proto b/datastore/datastore-sampleapp/src/main/proto/settings.proto
index 38fd074..1fb4e0a 100644
--- a/datastore/datastore-sampleapp/src/main/proto/settings.proto
+++ b/datastore/datastore-sampleapp/src/main/proto/settings.proto
@@ -5,4 +5,5 @@
message Settings {
int32 counter = 1;
+ bool foo = 2;
}
diff --git a/datastore/datastore-sampleapp/src/main/res/layout/settings_fragment.xml b/datastore/datastore-sampleapp/src/main/res/layout/settings_fragment.xml
new file mode 100644
index 0000000..7a9bcbf
--- /dev/null
+++ b/datastore/datastore-sampleapp/src/main/res/layout/settings_fragment.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <fragment
+ android:id="@+id/fragment"
+ android:name="com.example.datastoresampleapp.SettingsFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ tools:layout_editor_absoluteY="197dp" />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/inspection/inspection-testing/src/main/java/androidx/inspection/testing/InspectorTester.kt b/inspection/inspection-testing/src/main/java/androidx/inspection/testing/InspectorTester.kt
index fd56c1e..18265b8 100644
--- a/inspection/inspection-testing/src/main/java/androidx/inspection/testing/InspectorTester.kt
+++ b/inspection/inspection-testing/src/main/java/androidx/inspection/testing/InspectorTester.kt
@@ -139,7 +139,6 @@
open class DefaultTestInspectorEnvironment(
private val testInspectorExecutors: InspectorExecutors
) : InspectorEnvironment {
- constructor(parent: Job) : this(TestInspectorExecutors(parent))
override fun registerEntryHook(
originClass: Class<*>,
diff --git a/inspection/inspection-testing/src/main/java/androidx/inspection/testing/TestInspectorExecutors.kt b/inspection/inspection-testing/src/main/java/androidx/inspection/testing/TestInspectorExecutors.kt
index bdd680a..96adc63 100644
--- a/inspection/inspection-testing/src/main/java/androidx/inspection/testing/TestInspectorExecutors.kt
+++ b/inspection/inspection-testing/src/main/java/androidx/inspection/testing/TestInspectorExecutors.kt
@@ -21,6 +21,7 @@
import androidx.inspection.InspectorExecutors
import kotlinx.coroutines.Job
import java.util.concurrent.Executor
+import java.util.concurrent.Executors
import java.util.concurrent.RejectedExecutionException
/**
@@ -29,10 +30,12 @@
* HandlerThread created for inspector will quit once parent job completes.
*/
class TestInspectorExecutors(
- parentJob: Job
+ parentJob: Job,
+ ioExecutor: Executor? = null
) : InspectorExecutors {
private val handlerThread = HandlerThread("Test Inspector Handler Thread")
private val handler: Handler
+ private val ioExecutor: Executor
init {
handlerThread.start()
@@ -40,6 +43,11 @@
parentJob.invokeOnCompletion {
handlerThread.looper.quitSafely()
}
+ this.ioExecutor = ioExecutor ?: Executors.newFixedThreadPool(4).also { executor ->
+ parentJob.invokeOnCompletion {
+ executor.shutdown()
+ }
+ }
}
override fun handler() = handler
@@ -50,7 +58,5 @@
}
}
- override fun io(): Executor {
- TODO()
- }
+ override fun io() = ioExecutor
}
\ No newline at end of file
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/PlaybackFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/PlaybackFragmentTest.java
index 531670b..37b9400 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/PlaybackFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/PlaybackFragmentTest.java
@@ -540,7 +540,7 @@
final ControlsOverlayAutoHideDisabledFragment fragment =
(ControlsOverlayAutoHideDisabledFragment) activity.getTestFragment();
- // Sanity check that onViewCreated has made the controls invisible
+ // Validate that onViewCreated has made the controls invisible
assertFalse(fragment.mControlVisible);
activityTestRule.runOnUiThread(new Runnable() {
public void run() {
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/PlaybackSupportFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/PlaybackSupportFragmentTest.java
index 913c660..9e930c4 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/PlaybackSupportFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/PlaybackSupportFragmentTest.java
@@ -537,7 +537,7 @@
final ControlsOverlayAutoHideDisabledFragment fragment =
(ControlsOverlayAutoHideDisabledFragment) activity.getTestFragment();
- // Sanity check that onViewCreated has made the controls invisible
+ // Validate that onViewCreated has made the controls invisible
assertFalse(fragment.mControlVisible);
activityTestRule.runOnUiThread(new Runnable() {
public void run() {
diff --git a/navigation/navigation-dynamic-features-fragment/src/androidTest/java/androidx/navigation/dynamicfeatures/fragment/DynamicNavHostFragmentTest.kt b/navigation/navigation-dynamic-features-fragment/src/androidTest/java/androidx/navigation/dynamicfeatures/fragment/DynamicNavHostFragmentTest.kt
index e12afd8..59e061f 100644
--- a/navigation/navigation-dynamic-features-fragment/src/androidTest/java/androidx/navigation/dynamicfeatures/fragment/DynamicNavHostFragmentTest.kt
+++ b/navigation/navigation-dynamic-features-fragment/src/androidTest/java/androidx/navigation/dynamicfeatures/fragment/DynamicNavHostFragmentTest.kt
@@ -23,7 +23,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.testutils.withActivity
-import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -47,7 +47,7 @@
.commitNow()
}
}
- assertNotEquals(fragment.createSplitInstallManager(), fragment.createSplitInstallManager())
+ assertEquals(fragment.createSplitInstallManager(), fragment.createSplitInstallManager())
}
}
diff --git a/navigation/navigation-dynamic-features-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment.kt b/navigation/navigation-dynamic-features-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment.kt
index 7223b1a..aee34b8 100644
--- a/navigation/navigation-dynamic-features-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment.kt
+++ b/navigation/navigation-dynamic-features-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment.kt
@@ -29,6 +29,7 @@
import androidx.navigation.dynamicfeatures.DynamicExtras
import androidx.navigation.dynamicfeatures.DynamicInstallMonitor
import androidx.navigation.fragment.findNavController
+import com.google.android.play.core.common.IntentSenderForResultStarter
import com.google.android.play.core.splitinstall.SplitInstallSessionState
import com.google.android.play.core.splitinstall.model.SplitInstallErrorCode
import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus
@@ -128,11 +129,30 @@
navigate()
}
SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION -> try {
- @Suppress("DEPRECATION")
- // TODO replace once PlayCore ships with code landed in b/145276704.
- startIntentSenderForResult(
- sessionState.resolutionIntent().intentSender,
- INSTALL_REQUEST_CODE, null, 0, 0, 0, null
+ val splitInstallManager = monitor.splitInstallManager
+ if (splitInstallManager == null) {
+ onFailed(SplitInstallErrorCode.INTERNAL_ERROR)
+ return
+ }
+ splitInstallManager.startConfirmationDialogForResult(
+ sessionState,
+ IntentSenderForResultStarter { intent,
+ requestCode,
+ fillInIntent,
+ flagsMask,
+ flagsValues,
+ extraFlags,
+ options ->
+ startIntentSenderForResult(
+ intent,
+ requestCode,
+ fillInIntent,
+ flagsMask,
+ flagsValues,
+ extraFlags,
+ options
+ )
+ }, INSTALL_REQUEST_CODE
)
} catch (e: IntentSender.SendIntentException) {
onFailed(SplitInstallErrorCode.INTERNAL_ERROR)
diff --git a/navigation/navigation-dynamic-features-runtime/src/main/java/androidx/navigation/dynamicfeatures/DynamicInstallMonitor.kt b/navigation/navigation-dynamic-features-runtime/src/main/java/androidx/navigation/dynamicfeatures/DynamicInstallMonitor.kt
index e912d75..6e53c5f 100644
--- a/navigation/navigation-dynamic-features-runtime/src/main/java/androidx/navigation/dynamicfeatures/DynamicInstallMonitor.kt
+++ b/navigation/navigation-dynamic-features-runtime/src/main/java/androidx/navigation/dynamicfeatures/DynamicInstallMonitor.kt
@@ -69,8 +69,10 @@
/**
* The [SplitInstallManager] used to monitor the installation if any was set.
+ * @hide
*/
- internal var splitInstallManager: SplitInstallManager? = null
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY)
+ var splitInstallManager: SplitInstallManager? = null
/**
* `true` if the monitor has been used to request an install, else
diff --git a/playground-common/README.md b/playground-common/README.md
index 59bd21f..99fb098 100644
--- a/playground-common/README.md
+++ b/playground-common/README.md
@@ -41,6 +41,21 @@
method or filter projects from the main AndroidX settings gradle file using the
`selectProjectsFromAndroidX` method.
+### Properties
+When a `gradle.properties` file shows up under a sub project, main AndroidX build ends up
+reading it. For this reason, we can only keep a minimal `gradle.properties` file in these
+sub modules that also support playground setup.
+
+We cannot avoid creating `gradle.properties` as certain properties (e.g. `useAndroidX`) are
+read at configuration time and we cannot set it dynamically.
+
+Properties that will be set dynamically are kept in `playground.properties` file while
+shared properties are kept in `androidx-shared.properties` file.
+The dynamic properties are read in the `playground-include-settings.gradle` file and set
+on each project.
+
+There is a `VerifyPlaygroundGradlePropertiesTask` task that validates the contents of
+`androidx-shared.properties` file as part of the main AndroidX build.
### Optional Dependencies
Even though sub-projects usually declare exact coordinates for their dependencies,
for tests, it is a common practice to declare `project` dependencies. To avoid needing
diff --git a/playground-common/androidx-shared.properties b/playground-common/androidx-shared.properties
new file mode 100644
index 0000000..606adab
--- /dev/null
+++ b/playground-common/androidx-shared.properties
@@ -0,0 +1,33 @@
+#
+# Copyright 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Properties that are copied from main properties file
+# We set playground properties in two steps:
+# * This file is linked into gradle.properties under the project and limited to
+# just copying properties from the androidx properties file without any change.
+# Its integrity is validated as part of the buildOnServer task in AndroidX.
+# (validatePlaygroundGradleProperties task)
+# * Additional settings are in playground.properties which are loaded dynamically
+# This separation is necessary to ensure gradle can read certain properties
+# at configuration time.
+
+android.useAndroidX=true
+# Disable features we do not use
+android.defaults.buildfeatures.aidl=false
+android.defaults.buildfeatures.buildconfig=false
+android.defaults.buildfeatures.renderscript=false
+android.defaults.buildfeatures.resvalues=false
+android.defaults.buildfeatures.shaders=false
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index 86cffc9..d02bbef 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -14,15 +14,6 @@
# limitations under the License.
#
-# Project-wide Gradle settings.
-# IDE (e.g. Android Studio) users:
-# Gradle settings configured through the IDE *will override*
-# any settings specified in this file.
-# For more details on how to configure your build environment visit
-# http://www.gradle.org/docs/current/userguide/build_environment.html
-# Specifies the JVM arguments used for the daemon process.
-# The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs=-Xmx2048m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
@@ -30,10 +21,8 @@
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
-android.useAndroidX=true
-# Automatically convert third-party libraries to use AndroidX
-android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
+org.gradle.jvmargs=-Xmx2048m
kotlin.code.style=official
# Disable docs
androidx.enableDocumentation=false
@@ -43,10 +32,3 @@
androidx.playground.metalavaBuildId=6604778
androidx.playground.dokkaBuildId=6656073
androidx.studio.type=playground
-
-# Disable features we do not use
-android.defaults.buildfeatures.aidl=false
-android.defaults.buildfeatures.buildconfig=false
-android.defaults.buildfeatures.renderscript=false
-android.defaults.buildfeatures.resvalues=false
-android.defaults.buildfeatures.shaders=false
diff --git a/playground-common/setup-playground.sh b/playground-common/setup-playground.sh
index 650ae3d..ee41940 100755
--- a/playground-common/setup-playground.sh
+++ b/playground-common/setup-playground.sh
@@ -17,6 +17,8 @@
ln -s "${PLAYGROUND_REL_PATH}/gradlew" gradlew
rm -rf gradlew.bat
ln -s "${PLAYGROUND_REL_PATH}/gradlew.bat" gradlew.bat
+rm -rf gradle.properties
+ln -s "${PLAYGROUND_REL_PATH}/androidx-shared.properties" gradle.properties
ANDROIDX_IDEA_DIR="${PLAYGROUND_REL_PATH}/../.idea"
diff --git a/playground/gradle.properties b/playground/gradle.properties
new file mode 120000
index 0000000..d952fb0
--- /dev/null
+++ b/playground/gradle.properties
@@ -0,0 +1 @@
+../playground-common/androidx-shared.properties
\ No newline at end of file
diff --git a/room/gradle.properties b/room/gradle.properties
new file mode 120000
index 0000000..d952fb0
--- /dev/null
+++ b/room/gradle.properties
@@ -0,0 +1 @@
+../playground-common/androidx-shared.properties
\ No newline at end of file
diff --git a/slices/view/src/main/java/androidx/slice/widget/SliceStyle.java b/slices/view/src/main/java/androidx/slice/widget/SliceStyle.java
index 6772cf3..4f45a45 100644
--- a/slices/view/src/main/java/androidx/slice/widget/SliceStyle.java
+++ b/slices/view/src/main/java/androidx/slice/widget/SliceStyle.java
@@ -78,6 +78,8 @@
private int mListMinScrollHeight;
private int mListLargeHeight;
+ private boolean mExpandToAvailableHeight;
+
private RowStyle mRowStyle;
public SliceStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
@@ -142,6 +144,9 @@
R.dimen.abc_slice_row_range_inline_height);
mRowInlineRangeHeight = (int) a.getDimension(
R.styleable.SliceView_rowInlineRangeHeight, defaultRowInlineRangeHeight);
+
+ mExpandToAvailableHeight = a.getBoolean(
+ R.styleable.SliceView_expandToAvailableHeight, false);
} finally {
a.recycle();
}
@@ -253,6 +258,10 @@
return mRowSelectionHeight;
}
+ public boolean getExpandToAvailableHeight() {
+ return mExpandToAvailableHeight;
+ }
+
public int getRowHeight(RowContent row, SliceViewPolicy policy) {
int maxHeight = policy.getMaxSmallHeight() > 0 ? policy.getMaxSmallHeight() : mRowMaxHeight;
@@ -335,7 +344,7 @@
boolean bigEnoughToScroll = desiredHeight - maxLargeHeight >= mListMinScrollHeight;
// Adjust for scrolling
- int height = bigEnoughToScroll ? maxLargeHeight
+ int height = bigEnoughToScroll && !getExpandToAvailableHeight() ? maxLargeHeight
: maxHeight <= 0 ? desiredHeight
: Math.min(maxLargeHeight, desiredHeight);
if (!scrollable) {
diff --git a/slices/view/src/main/java/androidx/slice/widget/SliceView.java b/slices/view/src/main/java/androidx/slice/widget/SliceView.java
index 6e3e26a..df6eabe 100644
--- a/slices/view/src/main/java/androidx/slice/widget/SliceView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/SliceView.java
@@ -395,7 +395,10 @@
childrenHeight = requiredHeight;
} else {
// Not enough space available for slice in current mode
- if (getMode() == MODE_LARGE
+ if (mSliceStyle.getExpandToAvailableHeight()) {
+ // Don't request more space than we're allowed to have.
+ requiredHeight = childrenHeight;
+ } else if (getMode() == MODE_LARGE
&& childrenHeight >= mLargeHeight + actionHeight) {
childrenHeight = mLargeHeight + actionHeight;
} else if (childrenHeight <= mMinTemplateHeight) {
diff --git a/slices/view/src/main/res-public/values/public_attrs.xml b/slices/view/src/main/res-public/values/public_attrs.xml
index 5e8825d..599f927f 100644
--- a/slices/view/src/main/res-public/values/public_attrs.xml
+++ b/slices/view/src/main/res-public/values/public_attrs.xml
@@ -58,4 +58,5 @@
<public type="attr" name="iconSize" />
<public type="attr" name="imageSize" />
<public type="attr" name="disableRecyclerViewItemAnimator" />
+ <public type="attr" name="expandToAvailableHeight" />
</resources>
diff --git a/slices/view/src/main/res/values/attrs.xml b/slices/view/src/main/res/values/attrs.xml
index 8bb15c2..e440ce2 100644
--- a/slices/view/src/main/res/values/attrs.xml
+++ b/slices/view/src/main/res/values/attrs.xml
@@ -69,6 +69,14 @@
<attr name="rowRangeSingleTextHeight" format="dimension" />
<!-- Size of row view when range is inline -->
<attr name="rowInlineRangeHeight" format="dimension" />
+
+ <!-- Removes the height restriction of slices in MODE_LARGE. If the slice
+ is smaller than the available height, wrap_content decides whether the slice takes
+ up the entire height or only the required height. If the slice is bigger than
+ the available height, the height mode decides whether the slice fills the height
+ (height mode AT_MOST/EXACTLY), or expands to fit all items (height mode UNSPECIFIED).
+ -->
+ <attr name="expandToAvailableHeight" format="boolean" />
</declare-styleable>
<!-- To apply a style for all slices shown within an activity or app you
diff --git a/sqlite/integration-tests/inspection-room-testapp/src/androidTest/java/androidx/sqlite/inspection/RoomInvalidationHookTest.kt b/sqlite/integration-tests/inspection-room-testapp/src/androidTest/java/androidx/sqlite/inspection/RoomInvalidationHookTest.kt
index 54c108d..7152e8e 100644
--- a/sqlite/integration-tests/inspection-room-testapp/src/androidTest/java/androidx/sqlite/inspection/RoomInvalidationHookTest.kt
+++ b/sqlite/integration-tests/inspection-room-testapp/src/androidTest/java/androidx/sqlite/inspection/RoomInvalidationHookTest.kt
@@ -17,9 +17,7 @@
package androidx.sqlite.inspection
import android.database.sqlite.SQLiteDatabase
-import androidx.inspection.Connection
import androidx.inspection.InspectorEnvironment
-import androidx.inspection.InspectorFactory
import androidx.inspection.testing.DefaultTestInspectorEnvironment
import androidx.inspection.testing.InspectorTester
import androidx.inspection.testing.TestInspectorExecutors
@@ -45,8 +43,10 @@
@RunWith(AndroidJUnit4::class)
class RoomInvalidationHookTest {
private lateinit var db: TestDatabase
- private val inspectionExecutors =
- Pair(Executors.newSingleThreadExecutor(), Executors.newSingleThreadScheduledExecutor())
+
+ private val testJob = Job()
+ private val ioExecutor = Executors.newSingleThreadExecutor()
+ private val testInspectorExecutors = TestInspectorExecutors(testJob, ioExecutor)
@Before
fun initDb() {
@@ -62,14 +62,14 @@
@After
fun closeDb() {
- listOf(inspectionExecutors.first, inspectionExecutors.second)
- .forEach { inspectionExecutor ->
- inspectionExecutor.shutdown()
- assertWithMessage("inspector should not have any leaking tasks")
- .that(inspectionExecutor.awaitTermination(10, TimeUnit.SECONDS))
- .isTrue()
- db.close()
- }
+ testJob.complete()
+ ioExecutor.shutdown()
+ assertWithMessage("inspector should not have any leaking tasks")
+ .that(ioExecutor.awaitTermination(10, TimeUnit.SECONDS))
+ .isTrue()
+
+ testInspectorExecutors.handler().looper.thread.join(10_000)
+ db.close()
}
/**
@@ -77,28 +77,15 @@
* invalidation observer on the Room side is invoked.
*/
@Test
- fun invalidationHook() = runBlocking<Unit> {
+ fun invalidationHook() = runBlocking<Unit>(testJob) {
val testEnv = TestInspectorEnvironment(
roomDatabase = db,
sqliteDb = db.getSqliteDb(),
- parentJob = this.coroutineContext[Job]!!
+ inspectorExecutors = testInspectorExecutors
)
val tester = InspectorTester(
- inspectorId = "test",
- environment = testEnv,
- factoryOverride = object : InspectorFactory<SqliteInspector>("test") {
- override fun createInspector(
- connection: Connection,
- environment: InspectorEnvironment
- ): SqliteInspector {
- return SqliteInspector(
- connection,
- environment,
- inspectionExecutors.first,
- inspectionExecutors.second
- )
- }
- }
+ inspectorId = "androidx.sqlite.inspection",
+ environment = testEnv
)
val invalidatedTables = CompletableDeferred<List<String>>()
db.invalidationTracker.addObserver(object : InvalidationTracker.Observer("TestEntity") {
@@ -151,8 +138,8 @@
class TestInspectorEnvironment(
private val roomDatabase: RoomDatabase,
private val sqliteDb: SQLiteDatabase,
- parentJob: Job
-) : DefaultTestInspectorEnvironment(TestInspectorExecutors(parentJob)) {
+ inspectorExecutors: TestInspectorExecutors
+) : DefaultTestInspectorEnvironment(inspectorExecutors) {
override fun registerEntryHook(
originClass: Class<*>,
originMethod: String,
diff --git a/sqlite/integration-tests/inspection-sqldelight-testapp/src/androidTest/java/androidx/sqlite/inspection/SqlDelightInvalidationTest.kt b/sqlite/integration-tests/inspection-sqldelight-testapp/src/androidTest/java/androidx/sqlite/inspection/SqlDelightInvalidationTest.kt
index 9de4bb9..5313cbe 100644
--- a/sqlite/integration-tests/inspection-sqldelight-testapp/src/androidTest/java/androidx/sqlite/inspection/SqlDelightInvalidationTest.kt
+++ b/sqlite/integration-tests/inspection-sqldelight-testapp/src/androidTest/java/androidx/sqlite/inspection/SqlDelightInvalidationTest.kt
@@ -17,9 +17,7 @@
package androidx.sqlite.inspection
import android.database.sqlite.SQLiteDatabase
-import androidx.inspection.Connection
import androidx.inspection.InspectorEnvironment
-import androidx.inspection.InspectorFactory
import androidx.inspection.testing.DefaultTestInspectorEnvironment
import androidx.inspection.testing.InspectorTester
import androidx.inspection.testing.TestInspectorExecutors
@@ -48,7 +46,6 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import java.util.concurrent.Executors
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@@ -79,8 +76,11 @@
val sqliteDb = openedDb.getSqliteDb()
val query = dao.selectAll()
val job = this.coroutineContext[Job]!!
- val tester = sqliteInspectorTester(TestInspectorEnvironment(sqliteDb, listOf(query),
- job))
+ val tester = InspectorTester(
+ inspectorId = "androidx.sqlite.inspection",
+ environment = TestInspectorEnvironment(sqliteDb, listOf(query),
+ job)
+ )
val updates = query.asFlow().mapToList().take(2).produceIn(this)
val firstExpected = TestEntity.Impl(1, "one")
@@ -133,24 +133,6 @@
} as SQLiteDatabase
}
-suspend fun sqliteInspectorTester(environment: InspectorEnvironment) = InspectorTester(
- inspectorId = "test",
- environment = environment,
- factoryOverride = object : InspectorFactory<SqliteInspector>("test") {
- override fun createInspector(
- connection: Connection,
- environment: InspectorEnvironment
- ): SqliteInspector {
- return SqliteInspector(
- connection,
- environment,
- Executors.newSingleThreadExecutor(),
- Executors.newSingleThreadScheduledExecutor()
- )
- }
- }
-)
-
@Suppress("UNCHECKED_CAST")
class TestInspectorEnvironment(
private val sqliteDb: SQLiteDatabase,
diff --git a/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/CancellationQueryTest.kt b/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/CancellationQueryTest.kt
index db4ee0c..873ccbd 100644
--- a/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/CancellationQueryTest.kt
+++ b/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/CancellationQueryTest.kt
@@ -16,7 +16,6 @@
package androidx.sqlite.inspection.test
-import androidx.sqlite.inspection.SqliteInspectorFactory
import androidx.sqlite.inspection.test.CountingDelegatingExecutorService.Event.FINISHED
import androidx.sqlite.inspection.test.CountingDelegatingExecutorService.Event.STARTED
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -35,16 +34,14 @@
import org.junit.runner.RunWith
import java.util.concurrent.Executor
import java.util.concurrent.Executors.newCachedThreadPool
-import java.util.concurrent.Executors.newSingleThreadScheduledExecutor
@MediumTest
@RunWith(AndroidJUnit4::class)
class CancellationQueryTest {
private val countingExecutorService = CountingDelegatingExecutorService(newCachedThreadPool())
@get:Rule
- val environment = SqliteInspectorTestEnvironment(
- SqliteInspectorFactory(countingExecutorService, newSingleThreadScheduledExecutor())
- )
+ val environment = SqliteInspectorTestEnvironment(countingExecutorService)
+
@get:Rule
val temporaryFolder = TemporaryFolder(getInstrumentation().context.cacheDir)
diff --git a/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/SqliteInspectorTestEnvironment.kt b/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/SqliteInspectorTestEnvironment.kt
index 17e8b3d..022b3d5 100644
--- a/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/SqliteInspectorTestEnvironment.kt
+++ b/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/SqliteInspectorTestEnvironment.kt
@@ -22,7 +22,6 @@
import androidx.inspection.testing.InspectorTester
import androidx.inspection.testing.DefaultTestInspectorEnvironment
import androidx.inspection.testing.TestInspectorExecutors
-import androidx.sqlite.inspection.SqliteInspectorFactory
import androidx.sqlite.inspection.SqliteInspectorProtocol
import androidx.sqlite.inspection.SqliteInspectorProtocol.Command
import androidx.sqlite.inspection.SqliteInspectorProtocol.DatabaseOpenedEvent
@@ -34,23 +33,23 @@
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.runBlocking
import org.junit.rules.ExternalResource
+import java.util.concurrent.Executor
private const val SQLITE_INSPECTOR_ID = "androidx.sqlite.inspection"
class SqliteInspectorTestEnvironment(
- val factoryOverride: SqliteInspectorFactory? = null
+ val ioExecutorOverride: Executor? = null
) : ExternalResource() {
private lateinit var inspectorTester: InspectorTester
private lateinit var environment: FakeInspectorEnvironment
private val job = Job()
override fun before() {
- environment = FakeInspectorEnvironment(job)
+ environment = FakeInspectorEnvironment(job, TestInspectorExecutors(job, ioExecutorOverride))
inspectorTester = runBlocking {
InspectorTester(
inspectorId = SQLITE_INSPECTOR_ID,
- environment = environment,
- factoryOverride = factoryOverride
+ environment = environment
)
}
}
@@ -132,8 +131,9 @@
* retrieved in [consumeRegisteredHooks].
*/
private class FakeInspectorEnvironment(
- job: Job
-) : DefaultTestInspectorEnvironment(TestInspectorExecutors(job)) {
+ job: Job,
+ executors: TestInspectorExecutors = TestInspectorExecutors(job)
+) : DefaultTestInspectorEnvironment(executors) {
private val instancesToFind = mutableListOf<Any>()
private val registeredHooks = mutableListOf<Hook>()
diff --git a/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspectionExecutors.java b/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspectionExecutors.java
index ceae951..2f66151 100644
--- a/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspectionExecutors.java
+++ b/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspectionExecutors.java
@@ -16,67 +16,14 @@
package androidx.sqlite.inspection;
-import androidx.annotation.NonNull;
-
-import java.util.Locale;
import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.atomic.AtomicLong;
class SqliteInspectionExecutors {
private SqliteInspectionExecutors() {
}
- private static final ThreadFactory sThreadFactory = new ThreadFactory() {
- AtomicLong mNextId = new AtomicLong(0);
-
- @Override
- public Thread newThread(@NonNull Runnable target) {
- Thread thread = new Thread(target, generateThreadName());
- thread.setDaemon(true); // Don't prevent JVM from exiting
- return thread;
- }
-
- private String generateThreadName() {
- return String.format(Locale.ROOT, "Studio:SqlIns%d",
- mNextId.getAndIncrement());
- }
- };
-
- private static final Executor sDirectExecutor = new Executor() {
- @Override
- public void execute(@NonNull Runnable command) {
- command.run();
- }
- };
-
- private static final Executor sIOExecutor = Executors.newCachedThreadPool(sThreadFactory);
-
- private static final ScheduledExecutorService sScheduledExecutorService =
- Executors.newSingleThreadScheduledExecutor(sThreadFactory);
-
- static Executor directExecutor() {
- return sDirectExecutor;
- }
-
- static Executor ioExecutor() {
- return sIOExecutor;
- }
-
- /**
- * Single threaded ScheduledExecutor.
- * <p>
- * Since single threaded, only use for short tasks, e.g.
- * scheduling tasks to be executed on another thread.
- */
- static ScheduledExecutorService scheduledExecutor() {
- return sScheduledExecutorService;
- }
-
static Future<Void> submit(Executor executor, Runnable runnable) {
FutureTask<Void> task = new FutureTask<>(runnable, null);
executor.execute(task);
diff --git a/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspector.java b/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspector.java
index 497e580..13c0bc3 100644
--- a/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspector.java
+++ b/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspector.java
@@ -19,7 +19,6 @@
import static android.database.DatabaseUtils.getSqlStatementType;
import static androidx.sqlite.inspection.DatabaseExtensions.isAttemptAtUsingClosedDatabase;
-import static androidx.sqlite.inspection.SqliteInspectionExecutors.directExecutor;
import static androidx.sqlite.inspection.SqliteInspectorProtocol.ErrorContent.ErrorCode.ERROR_DB_CLOSED_DURING_OPERATION;
import static androidx.sqlite.inspection.SqliteInspectorProtocol.ErrorContent.ErrorCode.ERROR_ISSUE_WITH_PROCESSING_NEW_DATABASE_CONNECTION;
import static androidx.sqlite.inspection.SqliteInspectorProtocol.ErrorContent.ErrorCode.ERROR_ISSUE_WITH_PROCESSING_QUERY;
@@ -87,8 +86,6 @@
import java.util.WeakHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
/**
* Inspector to work with SQLite databases
@@ -171,7 +168,7 @@
private final DatabaseRegistry mDatabaseRegistry;
private final InspectorEnvironment mEnvironment;
private final Executor mIOExecutor;
- private final ScheduledExecutorService mScheduledExecutor;
+
/**
* Utility instance that handles communication with Room's InvalidationTracker instances.
*/
@@ -180,12 +177,10 @@
@NonNull
private final SqlDelightInvalidation mSqlDelightInvalidation;
- SqliteInspector(@NonNull Connection connection, InspectorEnvironment environment,
- Executor ioExecutor, ScheduledExecutorService scheduledExecutor) {
+ SqliteInspector(@NonNull Connection connection, @NonNull InspectorEnvironment environment) {
super(connection);
mEnvironment = environment;
- mIOExecutor = ioExecutor;
- mScheduledExecutor = scheduledExecutor;
+ mIOExecutor = environment.executors().io();
mRoomInvalidationRegistry = new RoomInvalidationRegistry(mEnvironment);
mSqlDelightInvalidation = SqlDelightInvalidation.create(mEnvironment);
@@ -349,12 +344,12 @@
@Override
@SuppressWarnings("FutureReturnValueIgnored") // TODO: handle errors from Future
public void schedule(final Runnable command, final long delayMs) {
- mScheduledExecutor.schedule(new Runnable() {
+ mEnvironment.executors().handler().postDelayed(new Runnable() {
@Override
public void run() {
mIOExecutor.execute(command);
}
- }, delayMs, TimeUnit.MILLISECONDS);
+ }, delayMs);
}
};
final RequestCollapsingThrottler throttler = new RequestCollapsingThrottler(
@@ -553,7 +548,7 @@
}
}
});
- callback.addCancellationListener(directExecutor(), new Runnable() {
+ callback.addCancellationListener(mEnvironment.executors().primary(), new Runnable() {
@Override
public void run() {
cancellationSignal.cancel();
diff --git a/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspectorFactory.java b/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspectorFactory.java
index abd17a8..db69c72 100644
--- a/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspectorFactory.java
+++ b/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspectorFactory.java
@@ -17,39 +17,25 @@
package androidx.sqlite.inspection;
import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
import androidx.inspection.Connection;
import androidx.inspection.InspectorEnvironment;
import androidx.inspection.InspectorFactory;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ScheduledExecutorService;
-
/**
* Factory for SqliteInspector
*/
public final class SqliteInspectorFactory extends InspectorFactory<SqliteInspector> {
private static final String SQLITE_INSPECTOR_ID = "androidx.sqlite.inspection";
- private final Executor mIOExecutor;
- private final ScheduledExecutorService mScheduledExecutorService;
-
- @VisibleForTesting
- public SqliteInspectorFactory(@NonNull Executor ioExecutor,
- @NonNull ScheduledExecutorService scheduledExecutorService) {
- super(SQLITE_INSPECTOR_ID);
- mIOExecutor = ioExecutor;
- mScheduledExecutorService = scheduledExecutorService;
- }
@SuppressWarnings("unused") // called by ServiceLoader
public SqliteInspectorFactory() {
- this(SqliteInspectionExecutors.ioExecutor(), SqliteInspectionExecutors.scheduledExecutor());
+ super(SQLITE_INSPECTOR_ID);
}
@NonNull
@Override
public SqliteInspector createInspector(@NonNull Connection connection,
@NonNull InspectorEnvironment environment) {
- return new SqliteInspector(connection, environment, mIOExecutor, mScheduledExecutorService);
+ return new SqliteInspector(connection, environment);
}
}
diff --git a/ui/ui-core/src/androidMain/kotlin/androidx/compose/ui/platform/DisposableUiSavedStateRegistry.kt b/ui/ui-core/src/androidMain/kotlin/androidx/compose/ui/platform/DisposableUiSavedStateRegistry.kt
index a21f1db..d549c4d 100644
--- a/ui/ui-core/src/androidMain/kotlin/androidx/compose/ui/platform/DisposableUiSavedStateRegistry.kt
+++ b/ui/ui-core/src/androidMain/kotlin/androidx/compose/ui/platform/DisposableUiSavedStateRegistry.kt
@@ -67,7 +67,7 @@
val androidxRegistry = savedStateRegistryOwner.savedStateRegistry
val bundle = androidxRegistry.consumeRestoredStateForKey(key)
- val restored: Map<String, Any>? = bundle?.toMap()
+ val restored: Map<String, List<Any?>>? = bundle?.toMap()
val uiSavedStateRegistry = UiSavedStateRegistry(restored) {
canBeSavedToBundle(it)
@@ -142,33 +142,23 @@
SizeF::class.java
)
-private val ValuesKey = "values"
-private val KeysKey = "keys"
-
-private fun Bundle.toMap(): Map<String, Any>? {
- val keys = getStringArrayList(KeysKey)
- @Suppress("UNCHECKED_CAST")
- val values = getParcelableArrayList<Parcelable>(ValuesKey) as ArrayList<Any>?
- check(keys != null && values != null && keys.size == values.size) {
- "Invalid bundle passed as restored state"
- }
- val map = mutableMapOf<String, Any>()
- for (i in keys.indices) {
- map[keys[i]] = values[i]
+private fun Bundle.toMap(): Map<String, List<Any?>>? {
+ val map = mutableMapOf<String, List<Any?>>()
+ this.keySet().forEach { key ->
+ map[key] = getParcelableArrayList<Parcelable?>(key) as List<Any?>
}
return map
}
-private fun Map<String, Any>.toBundle(): Bundle {
- val keys = ArrayList<String>(size)
- val values = ArrayList<Any>(size)
- forEach { (key, value) ->
- keys.add(key)
- values.add(value)
- }
+private fun Map<String, List<Any?>>.toBundle(): Bundle {
val bundle = Bundle()
- bundle.putStringArrayList(KeysKey, keys)
- @Suppress("UNCHECKED_CAST")
- bundle.putParcelableArrayList(ValuesKey, values as ArrayList<Parcelable>)
+ forEach { (key, list) ->
+ val arrayList = if (list is ArrayList<*>) list else ArrayList(list)
+ @Suppress("UNCHECKED_CAST")
+ bundle.putParcelableArrayList(
+ key,
+ arrayList as ArrayList<Parcelable?>
+ )
+ }
return bundle
}
diff --git a/ui/ui-saved-instance-state/api/current.txt b/ui/ui-saved-instance-state/api/current.txt
index bcbdb06..7e0a56e 100644
--- a/ui/ui-saved-instance-state/api/current.txt
+++ b/ui/ui-saved-instance-state/api/current.txt
@@ -34,13 +34,13 @@
public interface UiSavedStateRegistry {
method public boolean canBeSaved(Object value);
method public Object? consumeRestored(String key);
- method public java.util.Map<java.lang.String,java.lang.Object> performSave();
+ method public java.util.Map<java.lang.String,java.util.List<java.lang.Object>> performSave();
method public void registerProvider(String key, kotlin.jvm.functions.Function0<?> valueProvider);
- method public void unregisterProvider(String key);
+ method public void unregisterProvider(String key, kotlin.jvm.functions.Function0<?> valueProvider);
}
public final class UiSavedStateRegistryKt {
- method public static androidx.compose.runtime.savedinstancestate.UiSavedStateRegistry UiSavedStateRegistry(java.util.Map<java.lang.String,?>? restoredValues, kotlin.jvm.functions.Function1<java.lang.Object,java.lang.Boolean> canBeSaved);
+ method public static androidx.compose.runtime.savedinstancestate.UiSavedStateRegistry UiSavedStateRegistry(java.util.Map<java.lang.String,? extends java.util.List<?>>? restoredValues, kotlin.jvm.functions.Function1<java.lang.Object,java.lang.Boolean> canBeSaved);
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.runtime.savedinstancestate.UiSavedStateRegistry> getUiSavedStateRegistryAmbient();
}
diff --git a/ui/ui-saved-instance-state/api/public_plus_experimental_current.txt b/ui/ui-saved-instance-state/api/public_plus_experimental_current.txt
index bcbdb06..7e0a56e 100644
--- a/ui/ui-saved-instance-state/api/public_plus_experimental_current.txt
+++ b/ui/ui-saved-instance-state/api/public_plus_experimental_current.txt
@@ -34,13 +34,13 @@
public interface UiSavedStateRegistry {
method public boolean canBeSaved(Object value);
method public Object? consumeRestored(String key);
- method public java.util.Map<java.lang.String,java.lang.Object> performSave();
+ method public java.util.Map<java.lang.String,java.util.List<java.lang.Object>> performSave();
method public void registerProvider(String key, kotlin.jvm.functions.Function0<?> valueProvider);
- method public void unregisterProvider(String key);
+ method public void unregisterProvider(String key, kotlin.jvm.functions.Function0<?> valueProvider);
}
public final class UiSavedStateRegistryKt {
- method public static androidx.compose.runtime.savedinstancestate.UiSavedStateRegistry UiSavedStateRegistry(java.util.Map<java.lang.String,?>? restoredValues, kotlin.jvm.functions.Function1<java.lang.Object,java.lang.Boolean> canBeSaved);
+ method public static androidx.compose.runtime.savedinstancestate.UiSavedStateRegistry UiSavedStateRegistry(java.util.Map<java.lang.String,? extends java.util.List<?>>? restoredValues, kotlin.jvm.functions.Function1<java.lang.Object,java.lang.Boolean> canBeSaved);
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.runtime.savedinstancestate.UiSavedStateRegistry> getUiSavedStateRegistryAmbient();
}
diff --git a/ui/ui-saved-instance-state/api/restricted_current.txt b/ui/ui-saved-instance-state/api/restricted_current.txt
index bcbdb06..7e0a56e 100644
--- a/ui/ui-saved-instance-state/api/restricted_current.txt
+++ b/ui/ui-saved-instance-state/api/restricted_current.txt
@@ -34,13 +34,13 @@
public interface UiSavedStateRegistry {
method public boolean canBeSaved(Object value);
method public Object? consumeRestored(String key);
- method public java.util.Map<java.lang.String,java.lang.Object> performSave();
+ method public java.util.Map<java.lang.String,java.util.List<java.lang.Object>> performSave();
method public void registerProvider(String key, kotlin.jvm.functions.Function0<?> valueProvider);
- method public void unregisterProvider(String key);
+ method public void unregisterProvider(String key, kotlin.jvm.functions.Function0<?> valueProvider);
}
public final class UiSavedStateRegistryKt {
- method public static androidx.compose.runtime.savedinstancestate.UiSavedStateRegistry UiSavedStateRegistry(java.util.Map<java.lang.String,?>? restoredValues, kotlin.jvm.functions.Function1<java.lang.Object,java.lang.Boolean> canBeSaved);
+ method public static androidx.compose.runtime.savedinstancestate.UiSavedStateRegistry UiSavedStateRegistry(java.util.Map<java.lang.String,? extends java.util.List<?>>? restoredValues, kotlin.jvm.functions.Function1<java.lang.Object,java.lang.Boolean> canBeSaved);
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.runtime.savedinstancestate.UiSavedStateRegistry> getUiSavedStateRegistryAmbient();
}
diff --git a/ui/ui-saved-instance-state/src/androidAndroidTest/kotlin/androidx/compose/runtime/savedinstancestate/RememberSavedInstanceStateTest.kt b/ui/ui-saved-instance-state/src/androidAndroidTest/kotlin/androidx/compose/runtime/savedinstancestate/RememberSavedInstanceStateTest.kt
index 77a8166..564c693 100644
--- a/ui/ui-saved-instance-state/src/androidAndroidTest/kotlin/androidx/compose/runtime/savedinstancestate/RememberSavedInstanceStateTest.kt
+++ b/ui/ui-saved-instance-state/src/androidAndroidTest/kotlin/androidx/compose/runtime/savedinstancestate/RememberSavedInstanceStateTest.kt
@@ -166,9 +166,9 @@
var registryFactory by mutableStateOf<(UiSavedStateRegistry) -> UiSavedStateRegistry>(
value = {
object : DelegateRegistry(it) {
- override fun unregisterProvider(key: String) {
+ override fun unregisterProvider(key: String, valueProvider: () -> Any?) {
unregisterCalledForKey = key
- super.unregisterProvider(key)
+ super.unregisterProvider(key, valueProvider)
}
}
}
@@ -219,8 +219,8 @@
registerLatch.countDown()
}
- override fun unregisterProvider(key: String) {
- super.unregisterProvider(key)
+ override fun unregisterProvider(key: String, valueProvider: () -> Any?) {
+ super.unregisterProvider(key, valueProvider)
registeredKeys.remove(key)
}
}
@@ -277,9 +277,9 @@
WrapRegistry(
wrap = {
object : DelegateRegistry(it) {
- override fun unregisterProvider(key: String) {
+ override fun unregisterProvider(key: String, valueProvider: () -> Any?) {
latch.countDown()
- super.unregisterProvider(key)
+ super.unregisterProvider(key, valueProvider)
}
}
}
diff --git a/ui/ui-saved-instance-state/src/androidAndroidTest/kotlin/androidx/compose/runtime/savedinstancestate/RestorationInVariousScenariosTest.kt b/ui/ui-saved-instance-state/src/androidAndroidTest/kotlin/androidx/compose/runtime/savedinstancestate/RestorationInVariousScenariosTest.kt
new file mode 100644
index 0000000..d17888f
--- /dev/null
+++ b/ui/ui-saved-instance-state/src/androidAndroidTest/kotlin/androidx/compose/runtime/savedinstancestate/RestorationInVariousScenariosTest.kt
@@ -0,0 +1,283 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime.savedinstancestate
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.key
+import androidx.test.filters.MediumTest
+import androidx.ui.test.StateRestorationTester
+import androidx.ui.test.createComposeRule
+import androidx.ui.test.runOnIdle
+import androidx.ui.test.runOnUiThread
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@MediumTest
+@RunWith(JUnit4::class)
+class RestorationInVariousScenariosTest {
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ private val restorationTester = StateRestorationTester(composeTestRule)
+
+ @Test
+ fun insideForLoop() {
+ val states = arrayOfNulls<MutableState<Int>>(2)
+ restorationTester.setContent {
+ for (i in 0..1) {
+ states[i] = savedInstanceState { 0 }
+ }
+ }
+
+ runOnUiThread {
+ assertThat(states[0]!!.value).isEqualTo(0)
+ assertThat(states[1]!!.value).isEqualTo(0)
+
+ states[0]!!.value = 1
+ states[1]!!.value = 2
+
+ // we null it to ensure recomposition happened
+ states[0] = null
+ states[1] = null
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ runOnUiThread {
+ assertThat(states[0]!!.value).isEqualTo(1)
+ assertThat(states[1]!!.value).isEqualTo(2)
+ }
+ }
+
+ @Test
+ fun insideForLoop_withKey() {
+ val states = arrayOfNulls<MutableState<Int>>(2)
+ restorationTester.setContent {
+ for (i in 0..1) {
+ key(i) {
+ states[i] = savedInstanceState { 0 }
+ }
+ }
+ }
+
+ runOnUiThread {
+ assertThat(states[0]!!.value).isEqualTo(0)
+ assertThat(states[1]!!.value).isEqualTo(0)
+
+ states[0]!!.value = 1
+ states[1]!!.value = 2
+
+ // we null it to ensure recomposition happened
+ states[0] = null
+ states[1] = null
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ runOnUiThread {
+ assertThat(states[0]!!.value).isEqualTo(1)
+ assertThat(states[1]!!.value).isEqualTo(2)
+ }
+ }
+
+ @Test
+ fun insideForLoop_withExtraFunction() {
+ val states = arrayOfNulls<MutableState<Int>>(2)
+ restorationTester.setContent {
+ for (i in 0..1) {
+ FunctionWithState(states, i)
+ }
+ }
+
+ runOnUiThread {
+ assertThat(states[0]!!.value).isEqualTo(0)
+ assertThat(states[1]!!.value).isEqualTo(0)
+
+ states[0]!!.value = 1
+ states[1]!!.value = 2
+
+ // we null it to ensure recomposition happened
+ states[0] = null
+ states[1] = null
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ runOnUiThread {
+ assertThat(states[0]!!.value).isEqualTo(1)
+ assertThat(states[1]!!.value).isEqualTo(2)
+ }
+ }
+
+ @Test
+ fun changingLoopCountWithExtraStateAfter() {
+ var number = 2
+ val statesInLoop = arrayOfNulls<MutableState<Int>?>(2)
+ var stateOutside: MutableState<String>? = null
+ restorationTester.setContent {
+ repeat(number) {
+ statesInLoop[it] = savedInstanceState { 0 }
+ }
+ stateOutside = savedInstanceState { "0" }
+ }
+
+ runOnIdle {
+ statesInLoop[0]!!.value = 1
+ statesInLoop[0] = null
+ stateOutside!!.value = "1"
+ stateOutside = null
+ number = 1
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ runOnIdle {
+ assertThat(statesInLoop[0]?.value).isEqualTo(1)
+ assertThat(stateOutside?.value).isEqualTo("1")
+ }
+ }
+
+ @Test
+ fun twoStates() {
+ val states = arrayOfNulls<MutableState<Int>>(2)
+ restorationTester.setContent {
+ states[0] = savedInstanceState { 0 }
+ states[1] = savedInstanceState { 0 }
+ }
+
+ runOnUiThread {
+ assertThat(states[0]!!.value).isEqualTo(0)
+ assertThat(states[1]!!.value).isEqualTo(0)
+
+ states[0]!!.value = 1
+ states[1]!!.value = 2
+
+ // we null it to ensure recomposition happened
+ states[0] = null
+ states[1] = null
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ runOnUiThread {
+ assertThat(states[0]!!.value).isEqualTo(1)
+ assertThat(states[1]!!.value).isEqualTo(2)
+ }
+ }
+
+ @Test
+ fun twoStates_firstStateIsConditional() {
+ var needFirst = true
+ val states = arrayOfNulls<MutableState<Int>>(2)
+ restorationTester.setContent {
+ if (needFirst) {
+ states[0] = savedInstanceState { 0 }
+ }
+ states[1] = savedInstanceState { 0 }
+ }
+
+ runOnUiThread {
+ assertThat(states[0]!!.value).isEqualTo(0)
+ assertThat(states[1]!!.value).isEqualTo(0)
+
+ states[1]!!.value = 1
+
+ // we null it to ensure recomposition happened
+ states[0] = null
+ states[1] = null
+
+ needFirst = false
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ runOnUiThread {
+ assertThat(states[0]).isNull()
+ assertThat(states[1]!!.value).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun twoStates_withExtraFunction() {
+ val states = arrayOfNulls<MutableState<Int>>(2)
+ restorationTester.setContent {
+ FunctionWithState(states, 0)
+ FunctionWithState(states, 1)
+ }
+
+ runOnUiThread {
+ assertThat(states[0]!!.value).isEqualTo(0)
+ assertThat(states[1]!!.value).isEqualTo(0)
+
+ states[0]!!.value = 1
+ states[1]!!.value = 2
+
+ // we null it to ensure recomposition happened
+ states[0] = null
+ states[1] = null
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ runOnUiThread {
+ assertThat(states[0]!!.value).isEqualTo(1)
+ assertThat(states[1]!!.value).isEqualTo(2)
+ }
+ }
+
+ @Test
+ fun twoStates_withExtraFunction_firstStateIsConditional() {
+ var needFirst = true
+ val states = arrayOfNulls<MutableState<Int>>(2)
+ restorationTester.setContent {
+ if (needFirst) {
+ FunctionWithState(states, 0)
+ }
+ FunctionWithState(states, 1)
+ }
+
+ runOnUiThread {
+ assertThat(states[0]!!.value).isEqualTo(0)
+ assertThat(states[1]!!.value).isEqualTo(0)
+
+ states[1]!!.value = 1
+
+ // we null it to ensure recomposition happened
+ states[0] = null
+ states[1] = null
+
+ needFirst = false
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ runOnUiThread {
+ assertThat(states[0]).isNull()
+ assertThat(states[1]!!.value).isEqualTo(1)
+ }
+ }
+
+ @Composable
+ fun FunctionWithState(states: Array<MutableState<Int>?>, index: Int) {
+ states[index] = savedInstanceState { 0 }
+ }
+}
diff --git a/ui/ui-saved-instance-state/src/commonMain/kotlin/androidx/compose/runtime/savedinstancestate/RememberSavedInstanceState.kt b/ui/ui-saved-instance-state/src/commonMain/kotlin/androidx/compose/runtime/savedinstancestate/RememberSavedInstanceState.kt
index f0dae9b..0cb1e05 100644
--- a/ui/ui-saved-instance-state/src/commonMain/kotlin/androidx/compose/runtime/savedinstancestate/RememberSavedInstanceState.kt
+++ b/ui/ui-saved-instance-state/src/commonMain/kotlin/androidx/compose/runtime/savedinstancestate/RememberSavedInstanceState.kt
@@ -17,10 +17,11 @@
package androidx.compose.runtime.savedinstancestate
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLifecycleObserver
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.currentComposer
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.onPreCommit
import androidx.compose.runtime.remember
/**
@@ -58,66 +59,63 @@
key: String? = null,
init: () -> T
): T {
+ // key is the one provided by the user or the one generated by the compose runtime
val finalKey = if (!key.isNullOrEmpty()) {
key
} else {
currentComposer.currentCompoundKeyHash.toString()
}
+ @Suppress("UNCHECKED_CAST")
+ (saver as Saver<T, Any>)
+
val registry = UiSavedStateRegistryAmbient.current
- val valueProvider = remember(*inputs) { ValueProvider<T>() }
- return valueProvider.updateAndReturnValue(registry, saver, finalKey, init)
+ // value is restored using the registry or created via [init] lambda
+ val value = remember(*inputs) {
+ // TODO not restore when the input values changed (use hashKeys?) b/152014032
+ val restored = registry?.consumeRestored(finalKey)?.let {
+ saver.restore(it)
+ }
+ restored ?: init()
+ }
+
+ // save the latest passed saver object into a state object to be able to use it when we will
+ // be saving the value. keeping value in mutableStateOf() allows us to properly handle
+ // possible compose transactions cancellations
+ val saverHolder = remember { mutableStateOf(saver) }
+ saverHolder.value = saver
+
+ // re-register if the registry or key has been changed
+ onPreCommit(registry, finalKey) {
+ if (registry != null) {
+ val valueProvider = {
+ with(saverHolder.value) { SaverScopeImpl(registry::canBeSaved).save(value) }
+ }
+ registry.requireCanBeSaved(valueProvider())
+ registry.registerProvider(finalKey, valueProvider)
+ onDispose {
+ registry.unregisterProvider(finalKey, valueProvider)
+ }
+ }
+ }
+ return value
}
-private class ValueProvider<T : Any> : SaverScope, CompositionLifecycleObserver {
-
- private var registry: UiSavedStateRegistry? = null
- private var saver: Saver<T, out Any>? = null
- private var key: String? = null
- private var value: T? = null
-
- @Suppress("UNCHECKED_CAST")
- private fun saver(): Saver<T, Any> = (saver as Saver<T, Any>)
-
- fun updateAndReturnValue(
- registry: UiSavedStateRegistry?,
- saver: Saver<T, out Any>,
- key: String,
- init: () -> T
- ): T {
- val oldRegistry = this.registry
- val oldKey = this.key
- this.saver = saver
- this.registry = registry
- this.key = key
- val value = value ?: run {
- // TODO not restore when the input values changed (use hashKeys?) b/152014032
- val restored = registry?.consumeRestored(key)?.let {
- saver().restore(it)
+private fun UiSavedStateRegistry.requireCanBeSaved(value: Any?) {
+ if (value != null && !canBeSaved(value)) {
+ throw IllegalArgumentException(
+ if (value is MutableState<*>) {
+ "Please use savedInstanceState() if you want to save a MutableState"
+ } else {
+ "$value cannot be saved using the current UiSavedStateRegistry. The default " +
+ "implementation only supports types which can be stored inside the Bundle" +
+ ". Please consider implementing a custom Saver for this class and pass it" +
+ " to savedInstanceState() or rememberSavedInstanceState()."
}
- val result = restored ?: init()
- this.value = result
- result
- }
- if (oldRegistry !== registry || oldKey != key) {
- oldRegistry?.unregisterProvider(oldKey!!)
- registry?.registerProvider(key) {
- val result = with(saver()) { save(value) }
- if (result != null) {
- check(registry.canBeSaved(result))
- }
- result
- }
- }
- return value
+ )
}
+}
- override fun canBeSaved(value: Any) = registry!!.canBeSaved(value)
-
- override fun onLeave() {
- registry?.unregisterProvider(key!!)
- }
-
- override fun onEnter() {
- // no-op
- }
+// TODO this will not be needed when we make SaverScope "fun interface"
+private class SaverScopeImpl(val canBeSaved: (Any) -> Boolean) : SaverScope {
+ override fun canBeSaved(value: Any) = canBeSaved.invoke(value)
}
diff --git a/ui/ui-saved-instance-state/src/commonMain/kotlin/androidx/compose/runtime/savedinstancestate/Saver.kt b/ui/ui-saved-instance-state/src/commonMain/kotlin/androidx/compose/runtime/savedinstancestate/Saver.kt
index 1736596..8e5c4fa 100644
--- a/ui/ui-saved-instance-state/src/commonMain/kotlin/androidx/compose/runtime/savedinstancestate/Saver.kt
+++ b/ui/ui-saved-instance-state/src/commonMain/kotlin/androidx/compose/runtime/savedinstancestate/Saver.kt
@@ -85,8 +85,7 @@
}
/**
- * The default implementation of [Saver] which does not perform any conversion, but asserts that
- * the passed value can be saved.
+ * The default implementation of [Saver] which does not perform any conversion.
*
* It is used by [savedInstanceState] and [rememberSavedInstanceState] by default.
*
@@ -97,16 +96,6 @@
(AutoSaver as Saver<T, Any>)
private val AutoSaver = Saver<Any?, Any>(
- save = {
- if (it != null) {
- require(canBeSaved(it)) {
- "$it cannot be saved using the current UiSavedInstanceStateRegistry. The default" +
- " implementation only supports types which can be stored inside the " +
- "Bundle. Please consider implementing a custom Saver for this class " +
- "and pass it to savedInstanceState() or rememberSavedInstanceState()."
- }
- }
- it
- },
+ save = { it },
restore = { it }
)
diff --git a/ui/ui-saved-instance-state/src/commonMain/kotlin/androidx/compose/runtime/savedinstancestate/UiSavedStateRegistry.kt b/ui/ui-saved-instance-state/src/commonMain/kotlin/androidx/compose/runtime/savedinstancestate/UiSavedStateRegistry.kt
index dff81a3..4dc2bcc 100644
--- a/ui/ui-saved-instance-state/src/commonMain/kotlin/androidx/compose/runtime/savedinstancestate/UiSavedStateRegistry.kt
+++ b/ui/ui-saved-instance-state/src/commonMain/kotlin/androidx/compose/runtime/savedinstancestate/UiSavedStateRegistry.kt
@@ -33,8 +33,13 @@
/**
* Registers the value provider.
*
- * The same [key] cannot be registered twice, if you need to update the provider call
- * [unregisterProvider] first and then register again.
+ * There are could be multiple providers registered for the same [key]. In this case the
+ * order in which they were registered matters.
+ *
+ * Say we registered two providers for the key. One provides "1", second provides "2".
+ * [performSave] in this case will have listOf("1", "2) as a value for the key in the map.
+ * And later, when the registry will be recreated with the previously saved values, the first
+ * execution of [consumeRestored] would consume "1" and the second one "2".
*
* @param key Key to use for storing the value
* @param valueProvider Provides the current value, to be executed when [performSave]
@@ -46,8 +51,9 @@
* Unregisters the value provider previously registered via [registerProvider].
*
* @param key Key of the value which shouldn't be saved anymore
+ * @param valueProvider The provider previously passed to [registerProvider]
*/
- fun unregisterProvider(key: String)
+ fun unregisterProvider(key: String, valueProvider: () -> Any?)
/**
* Returns true if the value can be saved using this Registry.
@@ -58,9 +64,10 @@
fun canBeSaved(value: Any): Boolean
/**
- * Executes all the registered value providers and combines these values into a key-value map.
+ * Executes all the registered value providers and combines these values into a map. We have
+ * a list of values for each key as it is allowed to have multiple providers for the same key.
*/
- fun performSave(): Map<String, Any>
+ fun performSave(): Map<String, List<Any?>>
}
/**
@@ -70,7 +77,7 @@
* @param canBeSaved Function which returns true if the given value can be saved by the registry
*/
fun UiSavedStateRegistry(
- restoredValues: Map<String, Any>?,
+ restoredValues: Map<String, List<Any?>>?,
canBeSaved: (Any) -> Boolean
): UiSavedStateRegistry = UiSavedStateRegistryImpl(restoredValues, canBeSaved)
@@ -80,44 +87,70 @@
val UiSavedStateRegistryAmbient = staticAmbientOf<UiSavedStateRegistry?> { null }
private class UiSavedStateRegistryImpl(
- restored: Map<String, Any>?,
+ restored: Map<String, List<Any?>>?,
private val canBeSaved: (Any) -> Boolean
) : UiSavedStateRegistry {
- private val restored: MutableMap<String, Any> = restored?.toMutableMap() ?: mutableMapOf()
- private val valueProviders = mutableMapOf<String, () -> Any?>()
+ private val restored: MutableMap<String, List<Any?>> =
+ restored?.toMutableMap() ?: mutableMapOf()
+ private val valueProviders = mutableMapOf<String, MutableList<() -> Any?>>()
override fun canBeSaved(value: Any): Boolean = canBeSaved.invoke(value)
- override fun consumeRestored(key: String): Any? = restored.remove(key)
+ override fun consumeRestored(key: String): Any? {
+ val list = restored.remove(key)
+ return if (list != null && list.isNotEmpty()) {
+ if (list.size > 1) {
+ restored[key] = list.subList(1, list.size)
+ }
+ list[0]
+ } else {
+ null
+ }
+ }
override fun registerProvider(key: String, valueProvider: () -> Any?) {
require(key.isNotBlank()) { "Registered key is empty or blank" }
- require(!valueProviders.contains(key)) {
- "Key $key was already registered. Please call " +
- "unregister before registering again"
- }
@Suppress("UNCHECKED_CAST")
- valueProviders[key] = valueProvider
+ valueProviders.getOrPut(key) { mutableListOf() }.add(valueProvider)
}
- override fun unregisterProvider(key: String) {
- require(valueProviders.contains(key)) {
- "Key $key wasn't registered, but unregister " +
- "requested"
+ override fun unregisterProvider(key: String, valueProvider: () -> Any?) {
+ val list = valueProviders.remove(key)
+ val found = list?.remove(valueProvider)
+ require(found == true) {
+ "The given key $key , valueProvider pair wasn't previously registered"
}
- valueProviders.remove(key)
+ if (list.isNotEmpty()) {
+ // if there are other providers for this key return list back to the map
+ valueProviders[key] = list
+ }
}
- override fun performSave(): Map<String, Any> {
+ override fun performSave(): Map<String, List<Any?>> {
val map = restored.toMutableMap()
- valueProviders.forEach {
- val value = it.value()
- if (value != null) {
- check(canBeSaved(value))
- map[it.key] = value
+ valueProviders.forEach { (key, list) ->
+ if (list.size == 1) {
+ val value = list[0].invoke()
+ if (value != null) {
+ check(canBeSaved(value))
+ map[key] = arrayListOf<Any?>(value)
+ }
+ } else {
+ // if we have multiple providers we should store null values as well to preserve
+ // the order in which providers were registered. say there were two providers.
+ // the first provider returned null(nothing to save) and the second one returned
+ // "1". when we will be restoring the first provider would restore null (it is the
+ // same as to have nothing to restore) and the second one restore "1".
+ map[key] = list.map {
+ val value = it.invoke()
+ if (value != null) {
+ check(canBeSaved(value))
+ }
+ value
+ }
}
}
return map
}
-}
\ No newline at end of file
+}
diff --git a/ui/ui-saved-instance-state/src/test/java/androidx/compose/runtime/savedinstancestate/AutoSaverTest.kt b/ui/ui-saved-instance-state/src/test/java/androidx/compose/runtime/savedinstancestate/AutoSaverTest.kt
index a464f9d..af4f817 100644
--- a/ui/ui-saved-instance-state/src/test/java/androidx/compose/runtime/savedinstancestate/AutoSaverTest.kt
+++ b/ui/ui-saved-instance-state/src/test/java/androidx/compose/runtime/savedinstancestate/AutoSaverTest.kt
@@ -35,15 +35,6 @@
.isEqualTo(2)
}
}
-
- @Test(expected = IllegalArgumentException::class)
- fun exceptionWhenCantBeSaved() {
- val saver = autoSaver<Int>()
-
- with(saver) {
- disallowingScope.save(2)
- }
- }
}
val allowingScope = object : SaverScope {
diff --git a/ui/ui-saved-instance-state/src/test/java/androidx/compose/runtime/savedinstancestate/UiSavedStateRegistryTest.kt b/ui/ui-saved-instance-state/src/test/java/androidx/compose/runtime/savedinstancestate/UiSavedStateRegistryTest.kt
index f92cd0d..d432171 100644
--- a/ui/ui-saved-instance-state/src/test/java/androidx/compose/runtime/savedinstancestate/UiSavedStateRegistryTest.kt
+++ b/ui/ui-saved-instance-state/src/test/java/androidx/compose/runtime/savedinstancestate/UiSavedStateRegistryTest.kt
@@ -33,7 +33,7 @@
registry.registerProvider("key") { 10 }
registry.performSave().apply {
- assertThat(get("key")).isEqualTo(10)
+ assertThat(get("key")).isEqualTo(listOf(10))
}
}
@@ -41,8 +41,9 @@
fun unregisteredValuesAreNotSaved() {
val registry = createRegistry()
- registry.registerProvider("key") { 10 }
- registry.unregisterProvider("key")
+ val provider = { 10 }
+ registry.registerProvider("key", provider)
+ registry.unregisterProvider("key", provider)
registry.performSave().apply {
assertThat(containsKey("key")).isFalse()
@@ -53,12 +54,13 @@
fun registerAgainAfterUnregister() {
val registry = createRegistry()
- registry.registerProvider("key") { "value1" }
- registry.unregisterProvider("key")
+ val provider1 = { "value1" }
+ registry.registerProvider("key", provider1)
+ registry.unregisterProvider("key", provider1)
registry.registerProvider("key") { "value2" }
registry.performSave().apply {
- assertThat(get("key")).isEqualTo("value2")
+ assertThat(get("key")).isEqualTo(listOf("value2"))
}
}
@@ -67,27 +69,20 @@
val registry = createRegistry()
registry.registerProvider("key1") { 100L }
- registry.registerProvider("key2") { 100L }
+ val provider2 = { 100 }
+ registry.registerProvider("key2", provider2)
registry.registerProvider("key3") { "value" }
registry.registerProvider("key4") { listOf("item") }
- registry.unregisterProvider("key2")
+ registry.unregisterProvider("key2", provider2)
registry.performSave().apply {
- assertThat(get("key1")).isEqualTo(100L)
+ assertThat(get("key1")).isEqualTo(listOf(100L))
assertThat(containsKey("key2")).isFalse()
- assertThat(get("key3")).isEqualTo("value")
- assertThat(get("key4")).isEqualTo(listOf("item"))
+ assertThat(get("key3")).isEqualTo(listOf("value"))
+ assertThat(get("key4")).isEqualTo(listOf(listOf("item")))
}
}
- @Test(expected = IllegalArgumentException::class)
- fun registeringTheSameKeysTwiceIsNotAllowed() {
- val registry = createRegistry()
-
- registry.registerProvider("key") { 100L }
- registry.registerProvider("key") { 100L }
- }
-
@Test
fun nullValuesAreNotSaved() {
val registry = createRegistry()
@@ -115,7 +110,7 @@
@Test
fun restoreSimpleValues() {
- val restored = mapOf("key1" to "value", "key2" to 2f)
+ val restored = mapOf("key1" to listOf("value"), "key2" to listOf(2f))
val registry = createRegistry(restored)
assertThat(registry.consumeRestored("key1")).isEqualTo("value")
@@ -124,7 +119,7 @@
@Test
fun restoreClearsTheStoredValue() {
- val restored = mapOf("key" to "value")
+ val restored = mapOf("key" to listOf("value"))
val registry = createRegistry(restored)
assertThat(registry.consumeRestored("key")).isEqualTo("value")
@@ -133,14 +128,14 @@
@Test
fun unusedRestoredValueSavedAgain() {
- val restored = mapOf("key1" to "value")
+ val restored = mapOf("key1" to listOf("value"))
val registry = createRegistry(restored)
registry.registerProvider("key2") { 1 }
registry.performSave().apply {
- assertThat(get("key1")).isEqualTo("value")
- assertThat(get("key2")).isEqualTo(1)
+ assertThat(get("key1")).isEqualTo(listOf("value"))
+ assertThat(get("key2")).isEqualTo(listOf(1))
}
}
@@ -169,8 +164,36 @@
registry.performSave()
}
+ @Test
+ fun registeringTheSameKeysTwice() {
+ val registry = createRegistry()
+
+ registry.registerProvider("key") { 100L }
+ registry.registerProvider("key") { 200L }
+
+ val restoredRegistry = createRegistry(registry.performSave())
+ assertThat(restoredRegistry.consumeRestored("key")).isEqualTo(100L)
+ assertThat(restoredRegistry.consumeRestored("key")).isEqualTo(200L)
+ assertThat(restoredRegistry.consumeRestored("key")).isNull()
+ }
+
+ @Test
+ fun registeringAndUnregisteringTheSameKeys() {
+ val registry = createRegistry()
+
+ registry.registerProvider("key") { 1L }
+ val provider2 = { 2 }
+ registry.registerProvider("key", provider2)
+ registry.registerProvider("key") { 3 }
+ registry.unregisterProvider("key", provider2)
+
+ val restoredRegistry = createRegistry(registry.performSave())
+ assertThat(restoredRegistry.consumeRestored("key")).isEqualTo(1L)
+ assertThat(restoredRegistry.consumeRestored("key")).isEqualTo(3L)
+ }
+
private fun createRegistry(
- restored: Map<String, Any>? = null,
+ restored: Map<String, List<Any?>>? = null,
canBeSaved: (Any) -> Boolean = { true }
) = UiSavedStateRegistry(restored, canBeSaved)
}
\ No newline at end of file
diff --git a/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/StateRestorationTester.kt b/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/StateRestorationTester.kt
index 6fcbab8..adccbde 100644
--- a/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/StateRestorationTester.kt
+++ b/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/StateRestorationTester.kt
@@ -98,7 +98,7 @@
var shouldEmitChildren by mutableStateOf(true)
private set
private var currentRegistry: UiSavedStateRegistry = original
- private var savedMap: Map<String, Any> = emptyMap()
+ private var savedMap: Map<String, List<Any?>> = emptyMap()
fun saveStateAndDisposeChildren() {
savedMap = currentRegistry.performSave()
@@ -118,7 +118,8 @@
override fun registerProvider(key: String, valueProvider: () -> Any?) =
currentRegistry.registerProvider(key, valueProvider)
- override fun unregisterProvider(key: String) = currentRegistry.unregisterProvider(key)
+ override fun unregisterProvider(key: String, valueProvider: () -> Any?) =
+ currentRegistry.unregisterProvider(key, valueProvider)
override fun canBeSaved(value: Any) = currentRegistry.canBeSaved(value)
diff --git a/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/ComposeViewAdapterTest.kt b/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/ComposeViewAdapterTest.kt
index f67cccc..bb77cf2 100644
--- a/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/ComposeViewAdapterTest.kt
+++ b/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/ComposeViewAdapterTest.kt
@@ -18,10 +18,15 @@
import android.app.Activity
import android.os.Bundle
+import androidx.compose.animation.core.InternalAnimationApi
+import androidx.compose.animation.core.TransitionAnimation
import androidx.ui.tooling.preview.ComposeViewAdapter
import androidx.ui.tooling.preview.ViewInfo
+import androidx.ui.tooling.preview.animation.PreviewAnimationClock
import androidx.ui.tooling.test.R
import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
@@ -74,6 +79,29 @@
}
}
+ @OptIn(InternalAnimationApi::class)
+ @Test
+ fun transitionAnimationsAreSubscribedToTheClock() {
+ val clock = PreviewAnimationClock()
+
+ activityTestRule.runOnUiThread {
+ composeViewAdapter.init(
+ "androidx.ui.tooling.TestAnimationPreviewKt",
+ "PressStateAnimation"
+ )
+ composeViewAdapter.clock = clock
+ assertTrue(clock.observersToAnimations.isEmpty())
+
+ composeViewAdapter.findAndSubscribeTransitions()
+ assertFalse(clock.observersToAnimations.isEmpty())
+
+ val observer = clock.observersToAnimations.keys.single()
+ val transitionAnimation =
+ (observer as TransitionAnimation<*>.TransitionAnimationClockObserver).animation
+ assertEquals("colorAnim", transitionAnimation.label)
+ }
+ }
+
@Test
fun lineNumberMapping() {
val viewInfos = assertRendersCorrectly(
diff --git a/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/TestAnimationPreview.kt b/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/TestAnimationPreview.kt
new file mode 100644
index 0000000..8596e0a
--- /dev/null
+++ b/ui/ui-tooling/src/androidTest/java/androidx/ui/tooling/TestAnimationPreview.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ui.tooling
+
+import androidx.compose.animation.ColorPropKey
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.transitionDefinition
+import androidx.compose.animation.transition
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.gesture.pressIndicatorGestureFilter
+import androidx.compose.ui.graphics.Color
+import androidx.ui.tooling.preview.Preview
+
+private enum class PressState { Pressed, Released }
+
+private val color = ColorPropKey()
+
+private val definition = transitionDefinition<PressState> {
+ state(PressState.Released) {
+ this[color] = Color.Red
+ }
+ state(PressState.Pressed) {
+ this[color] = Color.Blue
+ }
+ transition {
+ color using spring(
+ stiffness = 50f
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PressStateAnimation() {
+ val toState = remember { mutableStateOf(PressState.Released) }
+ val pressIndicator =
+ Modifier.pressIndicatorGestureFilter(
+ onStart = { toState.value = PressState.Pressed },
+ onStop = { toState.value = PressState.Released },
+ onCancel = { toState.value = PressState.Released })
+
+ val state = transition(label = "colorAnim", definition = definition, toState = toState.value)
+ ColorRect(pressIndicator, color = state[color])
+}
+
+@Composable
+private fun ColorRect(modifier: Modifier = Modifier, color: Color) {
+ Canvas(modifier.fillMaxSize()) {
+ drawRect(color)
+ }
+}
\ No newline at end of file
diff --git a/ui/ui-tooling/src/main/java/androidx/ui/tooling/preview/ComposeViewAdapter.kt b/ui/ui-tooling/src/main/java/androidx/ui/tooling/preview/ComposeViewAdapter.kt
index e1ae408..9b52095 100644
--- a/ui/ui-tooling/src/main/java/androidx/ui/tooling/preview/ComposeViewAdapter.kt
+++ b/ui/ui-tooling/src/main/java/androidx/ui/tooling/preview/ComposeViewAdapter.kt
@@ -25,6 +25,8 @@
import android.util.Log
import android.widget.FrameLayout
import androidx.annotation.VisibleForTesting
+import androidx.compose.animation.TransitionModel
+import androidx.compose.animation.core.InternalAnimationApi
import androidx.compose.runtime.AtomicReference
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composition
@@ -120,6 +122,11 @@
private val slotTableRecord = SlotTableRecord.create()
/**
+ * Simple function name of the Composable being previewed.
+ */
+ private var composableName = ""
+
+ /**
* Saved exception from the last composition. Since we can not handle the exception during the
* composition, we save it and throw it during onLayout, this allows Studio to catch it and
* display it to the user.
@@ -204,6 +211,72 @@
walkTable(it)
}
}
+
+ if (::clock.isInitialized && composableName.isNotEmpty()) {
+ // TODO(b/160126628): support other APIs, e.g. animate
+ findAndSubscribeTransitions()
+ }
+ }
+
+ /**
+ * Finds all the transition animations defined in the Compose tree where the root is the
+ * `@Composable` being previewed. We only return animations defined in the user code, i.e.
+ * the ones we've got source information for.
+ */
+ @OptIn(InternalAnimationApi::class)
+ @VisibleForTesting
+ internal fun findAndSubscribeTransitions() {
+ val slotTrees = slotTableRecord.store.map { it.asTree() }
+ slotTrees.map { tree -> tree.firstOrNull { it.name == composableName } }
+ .firstOrNull()?.let { composable ->
+ // Find all the AnimationClockObservers corresponding to transition animations
+ val observers = composable.findAll {
+ // Find `transition` calls in the user code, i.e. when source location is known
+ it.name == "transition" && it.location != null
+ }.mapNotNull {
+ val rememberCall =
+ it.firstOrNull { it.name == "remember" } ?: return@mapNotNull null
+ val transitionModel = rememberCall.data.firstOrNull { data ->
+ data is TransitionModel<*>
+ } as? TransitionModel<*>
+ transitionModel?.anim?.animationClockObserver
+ }
+ // Subscribe all the observers found to the `PreviewAnimationClock`
+ observers.forEach { clock.subscribe(it) }
+ }
+ }
+
+ private fun Group.firstOrNull(predicate: (Group) -> Boolean): Group? {
+ return findGroupsThatMatchPredicate(this, predicate, true).firstOrNull()
+ }
+
+ private fun Group.findAll(predicate: (Group) -> Boolean): List<Group> {
+ return findGroupsThatMatchPredicate(this, predicate)
+ }
+
+ /**
+ * Search [Group]s that match a given [predicate], starting from a given [root]. An optional
+ * boolean parameter can be set if we're interested in a single occurrence. If it's set, we
+ * return early after finding the first matching [Group].
+ */
+ private fun findGroupsThatMatchPredicate(
+ root: Group,
+ predicate: (Group) -> Boolean,
+ findOnlyFirst: Boolean = false
+ ): List<Group> {
+ val result = mutableListOf<Group>()
+ val stack = mutableListOf(root)
+ while (stack.isNotEmpty()) {
+ val current = stack.removeLast()
+ if (predicate(current)) {
+ if (findOnlyFirst) {
+ return listOf(current)
+ }
+ result.add(current)
+ }
+ stack.addAll(current.children)
+ }
+ return result
}
override fun dispatchDraw(canvas: Canvas?) {
@@ -235,7 +308,8 @@
*
* @suppress
*/
- private lateinit var clock: PreviewAnimationClock
+ @VisibleForTesting
+ internal lateinit var clock: PreviewAnimationClock
/**
* Wraps a given [Preview] method an does any necessary setup.
@@ -279,6 +353,7 @@
ViewTreeViewModelStoreOwner.set(this, FakeViewModelStoreOwner)
this.debugPaintBounds = debugPaintBounds
this.debugViewInfos = debugViewInfos
+ this.composableName = methodName
composition = setContent(Recomposer.current()) {
WrapPreview {
diff --git a/work/gradle.properties b/work/gradle.properties
new file mode 120000
index 0000000..d952fb0
--- /dev/null
+++ b/work/gradle.properties
@@ -0,0 +1 @@
+../playground-common/androidx-shared.properties
\ No newline at end of file