Merge "Use dackka for localbroadcastmanager docs" into androidx-main
diff --git a/car/app/OWNERS b/car/app/OWNERS
index 7cdc4ee..cc30871 100644
--- a/car/app/OWNERS
+++ b/car/app/OWNERS
@@ -10,3 +10,4 @@
 
 # Feature owners
 per-file app/*[email protected]
+per-file app-automotive/*[email protected]
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index a696657a..5a2452e 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -972,6 +972,7 @@
   }
 
   @androidx.car.app.annotations.CarProtocol public final class Row implements androidx.car.app.model.Item {
+    method public java.util.List getActions();
     method public androidx.car.app.model.CarIcon? getImage();
     method public androidx.car.app.model.Metadata? getMetadata();
     method public androidx.car.app.model.OnClickDelegate? getOnClickDelegate();
@@ -990,6 +991,7 @@
 
   public static final class Row.Builder {
     ctor public Row.Builder();
+    method public androidx.car.app.model.Row.Builder addAction(androidx.car.app.model.Action);
     method public androidx.car.app.model.Row.Builder addText(CharSequence);
     method public androidx.car.app.model.Row.Builder addText(androidx.car.app.model.CarText);
     method public androidx.car.app.model.Row build();
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index d69fc33..f3ccb17 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -1165,6 +1165,7 @@
   }
 
   @androidx.car.app.annotations.CarProtocol public final class Row implements androidx.car.app.model.Item {
+    method public java.util.List getActions();
     method public androidx.car.app.model.CarIcon? getImage();
     method public androidx.car.app.model.Metadata? getMetadata();
     method public androidx.car.app.model.OnClickDelegate? getOnClickDelegate();
@@ -1183,6 +1184,7 @@
 
   public static final class Row.Builder {
     ctor public Row.Builder();
+    method public androidx.car.app.model.Row.Builder addAction(androidx.car.app.model.Action);
     method public androidx.car.app.model.Row.Builder addText(CharSequence);
     method public androidx.car.app.model.Row.Builder addText(androidx.car.app.model.CarText);
     method public androidx.car.app.model.Row build();
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index a696657a..5a2452e 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -972,6 +972,7 @@
   }
 
   @androidx.car.app.annotations.CarProtocol public final class Row implements androidx.car.app.model.Item {
+    method public java.util.List getActions();
     method public androidx.car.app.model.CarIcon? getImage();
     method public androidx.car.app.model.Metadata? getMetadata();
     method public androidx.car.app.model.OnClickDelegate? getOnClickDelegate();
@@ -990,6 +991,7 @@
 
   public static final class Row.Builder {
     ctor public Row.Builder();
+    method public androidx.car.app.model.Row.Builder addAction(androidx.car.app.model.Action);
     method public androidx.car.app.model.Row.Builder addText(CharSequence);
     method public androidx.car.app.model.Row.Builder addText(androidx.car.app.model.CarText);
     method public androidx.car.app.model.Row build();
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Row.java b/car/app/app/src/main/java/androidx/car/app/model/Row.java
index 64c0391..8987cfa 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Row.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Row.java
@@ -31,6 +31,7 @@
 import androidx.annotation.RestrictTo;
 import androidx.car.app.annotations.CarProtocol;
 import androidx.car.app.annotations.RequiresCarApi;
+import androidx.car.app.model.constraints.ActionsConstraints;
 import androidx.car.app.model.constraints.CarIconConstraints;
 import androidx.car.app.model.constraints.CarTextConstraints;
 import androidx.car.app.utils.CollectionUtils;
@@ -103,6 +104,8 @@
     @Nullable
     private final CarIcon mImage;
     @Keep
+    private final List mActions;
+    @Keep
     @Nullable
     private final Toggle mToggle;
     @Keep
@@ -148,6 +151,16 @@
         return mImage;
     }
 
+    /**
+     * Returns the list of additional actions at the end of the row.
+     *
+     * @see Builder#addAction(Action)
+     */
+    @NonNull
+    public List getActions() {
+        return mActions;
+    }
+
     /** Returns the type of the image in the row. */
     @RowImageType
     public int getRowImageType() {
@@ -278,6 +291,7 @@
         mTitle = builder.mTitle;
         mTexts = CollectionUtils.unmodifiableCopy(builder.mTexts);
         mImage = builder.mImage;
+        mActions = CollectionUtils.unmodifiableCopy(builder.mActions);
         mToggle = builder.mToggle;
         mOnClickDelegate = builder.mOnClickDelegate;
         mMetadata = builder.mMetadata;
@@ -291,6 +305,7 @@
         mTitle = null;
         mTexts = Collections.emptyList();
         mImage = null;
+        mActions = Collections.emptyList();
         mToggle = null;
         mOnClickDelegate = null;
         mMetadata = EMPTY_METADATA;
@@ -307,6 +322,7 @@
         final List mTexts = new ArrayList<>();
         @Nullable
         CarIcon mImage;
+        final List mActions = new ArrayList<>();
         @Nullable
         Toggle mToggle;
         @Nullable
@@ -489,6 +505,23 @@
         }
 
         /**
+         * Adds an additional action to the end of the row.
+         *
+         * @throws NullPointerException     if {@code action} is {@code null}
+         * @throws IllegalArgumentException if {@code action} contains unsupported Action types,
+         *                                  exceeds the maximum number of allowed actions or does
+         *                                  not contain a valid {@link CarIcon}.
+         */
+        @NonNull
+        public Builder addAction(@NonNull Action action) {
+            List mActionsCopy = new ArrayList<>(mActions);
+            mActionsCopy.add(requireNonNull(action));
+            ActionsConstraints.ACTIONS_CONSTRAINTS_ROW.validateOrThrow(mActionsCopy);
+            mActions.add(action);
+            return this;
+        }
+
+        /**
          * Sets a {@link Toggle} to show in the row.
          *
          * @throws NullPointerException if {@code toggle} is {@code null}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/constraints/ActionsConstraints.java b/car/app/app/src/main/java/androidx/car/app/model/constraints/ActionsConstraints.java
index d5df89c..5d05848 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/constraints/ActionsConstraints.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/constraints/ActionsConstraints.java
@@ -120,6 +120,17 @@
                     .setMaxActions(4)
                     .build();
 
+    /**
+     * Constraints for additional row actions. Only allows custom and back actions.
+     */
+    @NonNull
+    public static final ActionsConstraints ACTIONS_CONSTRAINTS_ROW =
+            new ActionsConstraints.Builder()
+                    .setMaxActions(2)
+                    .addDisallowedActionType(Action.TYPE_APP_ICON)
+                    .setRequireActionIcons(true)
+                    .build();
+
     private final int mMaxActions;
     private final int mMaxPrimaryActions;
     private final int mMaxCustomTitles;
diff --git a/car/app/app/src/test/java/androidx/car/app/model/RowTest.java b/car/app/app/src/test/java/androidx/car/app/model/RowTest.java
index 06aee1d..d64eac7 100644
--- a/car/app/app/src/test/java/androidx/car/app/model/RowTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/RowTest.java
@@ -165,6 +165,49 @@
     }
 
     @Test
+    public void addAction() {
+        Row row = new Row.Builder()
+                .setTitle("Title")
+                .addAction(Action.PAN)
+                .addAction(Action.BACK)
+                .build();
+        assertThat(row.getActions()).containsExactly(Action.PAN, Action.BACK);
+    }
+
+    @Test
+    public void addAction_invalidActionType_throws() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new Row.Builder().setTitle("Title").addAction(Action.APP_ICON).build());
+    }
+
+    @Test
+    public void addAction_manyActions_throws() {
+        CarIcon carIcon = TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+                "ic_test_1");
+        Action customAction = TestUtils.createAction("Title", carIcon);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new Row.Builder().setTitle("Title")
+                        .addAction(Action.BACK)
+                        .addAction(Action.PAN)
+                        .addAction(customAction)
+                        .build());
+    }
+
+    @Test
+    public void addAction_invalidActionNullIcon_throws() {
+        Action customAction = TestUtils.createAction("Title", null);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new Row.Builder().setTitle("Title")
+                        .addAction(customAction)
+                        .build());
+    }
+
+    @Test
     public void setMetadata() {
         Metadata metadata =
                 new Metadata.Builder().setPlace(
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SnapshotStateObserverBenchmark.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SnapshotStateObserverBenchmark.kt
index 7b760be..4fe328d 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SnapshotStateObserverBenchmark.kt
+++ b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SnapshotStateObserverBenchmark.kt
@@ -22,6 +22,7 @@
 import androidx.benchmark.junit4.measureRepeated
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.runtime.snapshots.SnapshotStateObserver
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -46,7 +47,7 @@
     private val doNothing: (Any) -> Unit = { _ -> }
 
     private lateinit var stateObserver: SnapshotStateObserver
-    private val models: List> = List(StateCount) { mutableStateOf(0) }
+    private val models: List> = List(StateCount) { mutableStateOf(0) }
     private val nodes: List = List(ScopeCount) { it }
     private lateinit var random: Random
 
@@ -116,6 +117,40 @@
     }
 
     @Test
+    fun derivedStateObservation() {
+        runOnUiThread {
+            val node = Any()
+            val states = models.take(3)
+            val derivedState = derivedStateOf {
+                states[0].value + states[1].value + states[2].value
+            }
+
+            stateObserver.observeReads(node, doNothing) {
+                // read derived state a few times
+                repeat(10) {
+                    derivedState.value
+                }
+            }
+
+            benchmarkRule.measureRepeated {
+                stateObserver.observeReads(node, doNothing) {
+                    // read derived state a few times
+                    repeat(10) {
+                        derivedState.value
+                    }
+                }
+
+                runWithTimingDisabled {
+                    states.forEach {
+                        it.value += 1
+                    }
+                    Snapshot.sendApplyNotifications()
+                }
+            }
+        }
+    }
+
+    @Test
     fun deeplyNestedModelObservations() {
         assumeTrue(Build.VERSION.SDK_INT != 29)
         runOnUiThread {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
index 1b652e8..65a96b1 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
@@ -710,7 +710,9 @@
                 // Record derived state dependency mapping
                 if (value is DerivedState<*>) {
                     derivedStates.removeScope(value)
-                    value.dependencies.forEach { dependency ->
+                    for (dependency in value.dependencies) {
+                        // skip over empty objects from dependency array
+                        if (dependency == null) break
                         derivedStates.add(dependency, value)
                     }
                 }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt
index ee172c5..76d772b 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt
@@ -18,13 +18,13 @@
 @file:JvmMultifileClass
 package androidx.compose.runtime
 
-import androidx.compose.runtime.external.kotlinx.collections.immutable.PersistentList
-import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentListOf
+import androidx.compose.runtime.collection.IdentityArrayMap
+import androidx.compose.runtime.collection.MutableVector
+import androidx.compose.runtime.collection.mutableVectorOf
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.runtime.snapshots.StateObject
 import androidx.compose.runtime.snapshots.StateRecord
 import androidx.compose.runtime.snapshots.current
-import androidx.compose.runtime.snapshots.fastForEach
 import androidx.compose.runtime.snapshots.newWritableRecord
 import androidx.compose.runtime.snapshots.sync
 import androidx.compose.runtime.snapshots.withCurrent
@@ -50,7 +50,7 @@
      * The [dependencies] list can be used to determine when a [StateObject] appears in the apply
      * observer set, if the state could affect value of this derived state.
      */
-    val dependencies: Set
+    val dependencies: Array
 
     /**
      * Mutation policy that controls how changes are handled after state dependencies update.
@@ -60,10 +60,6 @@
     val policy: SnapshotMutationPolicy?
 }
 
-private typealias DerivedStateObservers = Pair<(DerivedState<*>) -> Unit, (DerivedState<*>) -> Unit>
-
-private val derivedStateObservers = SnapshotThreadLocal>()
-
 private val calculationBlockNestedLevel = SnapshotThreadLocal()
 
 private class DerivedSnapshotState(
@@ -77,7 +73,7 @@
             val Unset = Any()
         }
 
-        var dependencies: HashMap? = null
+        var dependencies: IdentityArrayMap? = null
         var result: Any? = Unset
         var resultHash: Int = 0
 
@@ -99,9 +95,9 @@
             val dependencies = sync { dependencies }
             if (dependencies != null) {
                 notifyObservers(derivedState) {
-                    for ((stateObject, readLevel) in dependencies.entries) {
+                    dependencies.forEach { stateObject, readLevel ->
                         if (readLevel != 1) {
-                            continue
+                            return@forEach
                         }
 
                         if (stateObject is DerivedSnapshotState<*>) {
@@ -140,9 +136,9 @@
             // for correct invalidation later
             if (forceDependencyReads) {
                 notifyObservers(this) {
-                    val dependencies = readable.dependencies ?: emptyMap()
+                    val dependencies = readable.dependencies
                     val invalidationNestedLevel = calculationBlockNestedLevel.get() ?: 0
-                    for ((dependency, nestedLevel) in dependencies) {
+                    dependencies?.forEach { dependency, nestedLevel ->
                         calculationBlockNestedLevel.set(nestedLevel + invalidationNestedLevel)
                         snapshot.readObserver?.invoke(dependency)
                     }
@@ -153,7 +149,7 @@
         }
         val nestedCalculationLevel = calculationBlockNestedLevel.get() ?: 0
 
-        val newDependencies = HashMap()
+        val newDependencies = IdentityArrayMap()
         val result = notifyObservers(this) {
             calculationBlockNestedLevel.set(nestedCalculationLevel + 1)
 
@@ -218,18 +214,23 @@
             // value is used instead which doesn't notify. This allow the read observer to read the
             // value and only update the cache once.
             Snapshot.current.readObserver?.invoke(this)
-            return currentValue
+            return first.withCurrent {
+                @Suppress("UNCHECKED_CAST")
+                currentRecord(it, Snapshot.current, true, calculation).result as T
+            }
         }
 
     override val currentValue: T
         get() = first.withCurrent {
             @Suppress("UNCHECKED_CAST")
-            currentRecord(it, Snapshot.current, true, calculation).result as T
+            currentRecord(it, Snapshot.current, false, calculation).result as T
         }
 
-    override val dependencies: Set
+    override val dependencies: Array
         get() = first.withCurrent {
-            currentRecord(it, Snapshot.current, false, calculation).dependencies?.keys ?: emptySet()
+            val record = currentRecord(it, Snapshot.current, false, calculation)
+            @Suppress("UNCHECKED_CAST")
+            record.dependencies?.keys ?: emptyArray()
         }
 
     override fun toString(): String = first.withCurrent {
@@ -260,16 +261,6 @@
     }
 }
 
-private inline fun  notifyObservers(derivedState: DerivedState<*>, block: () -> R): R {
-    val observers = derivedStateObservers.get() ?: persistentListOf()
-    observers.fastForEach { (start, _) -> start(derivedState) }
-    return try {
-        block()
-    } finally {
-        observers.fastForEach { (_, done) -> done(derivedState) }
-    }
-}
-
 /**
  * Creates a [State] object whose [State.value] is the result of [calculation]. The result of
  * calculation will be cached in such a way that calling [State.value] repeatedly will not cause
@@ -307,6 +298,20 @@
     calculation: () -> T,
 ): State = DerivedSnapshotState(calculation, policy)
 
+private typealias DerivedStateObservers = Pair<(DerivedState<*>) -> Unit, (DerivedState<*>) -> Unit>
+
+private val derivedStateObservers = SnapshotThreadLocal>()
+
+private inline fun  notifyObservers(derivedState: DerivedState<*>, block: () -> R): R {
+    val observers = derivedStateObservers.get() ?: MutableVector(0)
+    observers.forEach { (start, _) -> start(derivedState) }
+    return try {
+        block()
+    } finally {
+        observers.forEach { (_, done) -> done(derivedState) }
+    }
+}
+
 /**
  * Observe the recalculations performed by any derived state that is recalculated during the
  * execution of [block]. [start] is called before a calculation starts and [done] is called
@@ -321,15 +326,15 @@
     done: (derivedState: State<*>) -> Unit,
     block: () -> R
 ) {
-    val previous = derivedStateObservers.get()
+    val observers = derivedStateObservers.get() ?: mutableVectorOf().also {
+        derivedStateObservers.set(it)
+    }
+
+    val observer = start to done
     try {
-        derivedStateObservers.set(
-            (derivedStateObservers.get() ?: persistentListOf()).add(
-                start to done
-            )
-        )
+        observers.add(observer)
         block()
     } finally {
-        derivedStateObservers.set(previous)
+        observers.removeAt(observers.lastIndex)
     }
 }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
index 55240ef..86dfb6b 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
@@ -17,6 +17,7 @@
 package androidx.compose.runtime.snapshots
 
 import androidx.compose.runtime.DerivedState
+import androidx.compose.runtime.State
 import androidx.compose.runtime.TestOnly
 import androidx.compose.runtime.collection.IdentityArrayMap
 import androidx.compose.runtime.collection.IdentityArraySet
@@ -125,8 +126,8 @@
             scopeMap.currentScope = scope
 
             observeDerivedStateRecalculations(
-                start = { scopeMap.deriveStateScopeCount++ },
-                done = { scopeMap.deriveStateScopeCount-- },
+                start = scopeMap.derivedStateEnterObserver,
+                done = scopeMap.derivedStateExitObserver
             ) {
                 Snapshot.observe(readObserver, null, block)
             }
@@ -237,7 +238,22 @@
          */
         var currentScope: Any? = null
 
-        var deriveStateScopeCount = 0
+        /**
+         * Start observer for derived state recalculation
+         */
+        val derivedStateEnterObserver: (State<*>) -> Unit = { deriveStateScopeCount++ }
+
+        /**
+         * Exit observer for derived state recalculation
+         */
+        val derivedStateExitObserver: (State<*>) -> Unit = { deriveStateScopeCount-- }
+
+        /**
+         * Counter for skipping reads inside derived states. If count is > 0, read happens inside
+         * a derived state.
+         * Reads for derived states are captured separately through [DerivedState.dependencies].
+         */
+        private var deriveStateScopeCount = 0
 
         /**
          * Values that have been read during the scope's [SnapshotStateObserver.observeReads].
@@ -255,9 +271,15 @@
          */
         private val invalidated = hashSetOf()
 
+        /**
+         * Invalidation index from state objects to derived states reading them.
+         */
         private val dependencyToDerivedStates = IdentityScopeMap>()
 
-        private val derivedStateToValue = HashMap, Any?>()
+        /**
+         * Last derived state value recorded during read.
+         */
+        private val recordedDerivedStateValues = HashMap, Any?>()
 
         /**
          * Record that [value] was read in [currentScope].
@@ -276,12 +298,13 @@
             recordedValues.add(value)
 
             if (value is DerivedState<*>) {
-                dependencyToDerivedStates.removeScope(value)
                 val dependencies = value.dependencies
                 for (dependency in dependencies) {
+                    // skip over dependency array
+                    if (dependency == null) break
                     dependencyToDerivedStates.add(dependency, value)
                 }
-                derivedStateToValue[value] = value.currentValue
+                recordedDerivedStateValues[value] = value.currentValue
             }
         }
 
@@ -293,7 +316,9 @@
             recordedValues.fastForEach {
                 removeObservation(scope, it)
             }
-            scopeToValues.remove(scope)
+            // clearing the scope usually means that we are about to start observation again
+            // so it doesn't make sense to reallocate the set
+            recordedValues.clear()
         }
 
         /**
@@ -313,8 +338,9 @@
 
         private fun removeObservation(scope: Any, value: Any) {
             valueToScopes.remove(value, scope)
-            if (value is DerivedState<*>) {
+            if (value is DerivedState<*> && value !in valueToScopes) {
                 dependencyToDerivedStates.removeScope(value)
+                recordedDerivedStateValues.remove(value)
             }
         }
 
@@ -325,6 +351,7 @@
             valueToScopes.clear()
             scopeToValues.clear()
             dependencyToDerivedStates.clear()
+            recordedDerivedStateValues.clear()
         }
 
         /**
@@ -338,7 +365,7 @@
                     // Find derived state that is invalidated by this change
                     dependencyToDerivedStates.forEachScopeOf(value) { derivedState ->
                         derivedState as DerivedState
-                        val previousValue = derivedStateToValue[derivedState]
+                        val previousValue = recordedDerivedStateValues[derivedState]
                         val policy = derivedState.policy ?: structuralEqualityPolicy()
 
                         // Invalidate only if currentValue is different than observed on read
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTestsCommon.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTestsCommon.kt
index 6b9bd48..efc63b90 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTestsCommon.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTestsCommon.kt
@@ -542,6 +542,32 @@
         assertEquals(1, changes)
     }
 
+    @Test
+    fun readingDerivedStateConditionallyInvalidatesBothScopes() {
+        var changes = 0
+
+        runSimpleTest { stateObserver, state ->
+            val derivedState = derivedStateOf { state.value }
+
+            val onChange: (String) -> Unit = { changes++ }
+            stateObserver.observeReads("scope", onChange) {
+                // read derived state
+                derivedState.value
+            }
+
+            // read the same state in other scope
+            stateObserver.observeReads("other scope", onChange) {
+                derivedState.value
+            }
+
+            // stop observing state in other scope
+            stateObserver.observeReads("other scope", onChange) {
+                /* no-op */
+            }
+        }
+        assertEquals(1, changes)
+    }
+
     private fun runSimpleTest(
         block: (modelObserver: SnapshotStateObserver, data: MutableState) -> Unit
     ) {
diff --git a/core/core/src/androidTest/java/androidx/core/app/ActivityCompatTest.java b/core/core/src/androidTest/java/androidx/core/app/ActivityCompatTest.java
index dd22f6e..0d145e0 100644
--- a/core/core/src/androidTest/java/androidx/core/app/ActivityCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/ActivityCompatTest.java
@@ -38,7 +38,9 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.SdkSuppress;
+import androidx.test.rule.GrantPermissionRule;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -48,6 +50,13 @@
 @RunWith(AndroidJUnit4.class)
 @LargeTest
 public class ActivityCompatTest extends BaseInstrumentationTestCase {
+    @Rule
+    public GrantPermissionRule mRuntimePermissionRule =
+            GrantPermissionRule.grant(Manifest.permission.ACCESS_FINE_LOCATION);
+
+    public static final String[] LOCATION_PERMISSIONS = {
+            Manifest.permission.ACCESS_FINE_LOCATION
+    };
 
     public ActivityCompatTest() {
         super(TestActivity.class);
@@ -61,27 +70,23 @@
     public void testPermissionDelegate() {
         try (ActivityScenario scenario =
                      ActivityScenario.launch(TestActivity.class)) {
-            scenario.onActivity(new ActivityScenario.ActivityAction() {
-                @Override
-                public void perform(TestActivity activity) {
-                    ActivityCompat.PermissionCompatDelegate delegate =
-                            mock(PermissionCompatDelegate.class);
+            scenario.onActivity(activity -> {
+                PermissionCompatDelegate delegate =
+                        mock(PermissionCompatDelegate.class);
 
-                    // First test setting the delegate
-                    ActivityCompat.setPermissionCompatDelegate(delegate);
+                // First test setting the delegate
+                ActivityCompat.setPermissionCompatDelegate(delegate);
 
-                    ActivityCompat.requestPermissions(activity, new String[]{
-                            Manifest.permission.ACCESS_FINE_LOCATION}, 42);
-                    verify(delegate).requestPermissions(same(activity), aryEq(
-                            new String[]{Manifest.permission.ACCESS_FINE_LOCATION}), eq(42));
+                ActivityCompat.requestPermissions(activity, LOCATION_PERMISSIONS, 42);
+                //noinspection ConstantConditions
+                verify(delegate).requestPermissions(
+                        same(activity), aryEq(LOCATION_PERMISSIONS), eq(42));
 
-                    // Test clearing the delegate
-                    ActivityCompat.setPermissionCompatDelegate(null);
+                // Test clearing the delegate
+                ActivityCompat.setPermissionCompatDelegate(null);
 
-                    ActivityCompat.requestPermissions(activity, new String[]{
-                            Manifest.permission.ACCESS_FINE_LOCATION}, 42);
-                    verifyNoMoreInteractions(delegate);
-                }
+                ActivityCompat.requestPermissions(activity, LOCATION_PERMISSIONS, 42);
+                verifyNoMoreInteractions(delegate);
             });
         }
     }
@@ -90,18 +95,15 @@
     public void testPermissionNull() {
         try (ActivityScenario scenario =
                      ActivityScenario.launch(TestActivity.class)) {
-            scenario.onActivity(new ActivityScenario.ActivityAction() {
-                @Override
-                public void perform(TestActivity activity) {
-                    String[] permissions = new String[]{null};
+            scenario.onActivity(activity -> {
+                String[] permissions = new String[]{null};
 
-                    try {
-                        ActivityCompat.requestPermissions(activity, permissions, 42);
-                    } catch (IllegalArgumentException e) {
-                        assertThat(e).hasMessageThat().contains("Permission request for "
-                                + "permissions " + Arrays.toString(permissions) + " must not "
-                                + "contain null or empty values");
-                    }
+                try {
+                    ActivityCompat.requestPermissions(activity, permissions, 42);
+                } catch (IllegalArgumentException e) {
+                    assertThat(e).hasMessageThat().contains("Permission request for "
+                            + "permissions " + Arrays.toString(permissions) + " must not "
+                            + "contain null or empty values");
                 }
             });
         }
@@ -111,20 +113,17 @@
     public void testPermissionEmpty() {
         try (ActivityScenario scenario =
                      ActivityScenario.launch(TestActivity.class)) {
-            scenario.onActivity(new ActivityScenario.ActivityAction() {
-                @Override
-                public void perform(TestActivity activity) {
-                    String[] permissions = new String[]{
-                            Manifest.permission.ACCESS_FINE_LOCATION, ""
-                    };
+            scenario.onActivity(activity -> {
+                String[] permissions = new String[]{
+                        Manifest.permission.ACCESS_FINE_LOCATION, ""
+                };
 
-                    try {
-                        ActivityCompat.requestPermissions(activity, permissions, 42);
-                    } catch (IllegalArgumentException e) {
-                        assertThat(e).hasMessageThat().contains("Permission request for "
-                                + "permissions " + Arrays.toString(permissions) + " must not "
-                                + "contain null or empty values");
-                    }
+                try {
+                    ActivityCompat.requestPermissions(activity, permissions, 42);
+                } catch (IllegalArgumentException e) {
+                    assertThat(e).hasMessageThat().contains("Permission request for "
+                            + "permissions " + Arrays.toString(permissions) + " must not "
+                            + "contain null or empty values");
                 }
             });
         }
@@ -152,7 +151,8 @@
     @Test
     public void testOnSharedElementsReady() {
         AtomicInteger counter = new AtomicInteger();
-        SharedElementCallback callback = new SharedElementCallback() {};
+        SharedElementCallback callback = new SharedElementCallback() {
+        };
         android.app.SharedElementCallback.OnSharedElementsReadyListener listener =
                 counter::incrementAndGet;