Merge changes I25114692,I84f51fc5 into androidx-main

* changes:
  Bump Compose to 1.0.0-beta03
  Revert "Use prebuilts for compose dependencies"
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/PreviewViewFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/PreviewViewFragment.java
index 9b1a8e2..6933449 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/PreviewViewFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/PreviewViewFragment.java
@@ -93,6 +93,10 @@
     @SuppressWarnings("WeakerAccess")
     Preview mPreview;
 
+    // Synthetic access
+    @SuppressWarnings("WeakerAccess")
+    ProcessCameraProvider mCameraProvider;
+
     public PreviewViewFragment() {
         super(R.layout.fragment_preview_view);
     }
@@ -108,12 +112,19 @@
         super.onViewCreated(view, savedInstanceState);
         mPreviewView = view.findViewById(R.id.preview_view);
         mPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
-
+        mPreviewView.addOnLayoutChangeListener(
+                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom)
+                        -> {
+                    if (mCameraProvider != null) {
+                        bindPreview(mCameraProvider);
+                    }
+                });
         mBlurBitmap = new BlurBitmap(requireContext());
         Futures.addCallback(mCameraProviderFuture, new FutureCallback<ProcessCameraProvider>() {
             @Override
             public void onSuccess(@Nullable ProcessCameraProvider cameraProvider) {
                 Preconditions.checkNotNull(cameraProvider);
+                mCameraProvider = cameraProvider;
                 mPreview = new Preview.Builder()
                         .setTargetRotation(view.getDisplay().getRotation())
                         .setTargetName("Preview")
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java
index c181f0d..7d9d55c 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java
@@ -286,13 +286,16 @@
         }
         mIsNavigating = false;
 
-        NavigationManagerCallback callback = mNavigationManagerCallback;
-        Executor executor = mNavigationManagerCallbackExecutor;
-        if (callback == null || executor == null) {
+        if (mNavigationManagerCallbackExecutor == null) {
             return;
         }
 
-        executor.execute(callback::onStopNavigation);
+        mNavigationManagerCallbackExecutor.execute(() -> {
+            NavigationManagerCallback callback = mNavigationManagerCallback;
+            if (callback != null) {
+                callback.onStopNavigation();
+            }
+        });
     }
 
     /**
diff --git a/car/app/app/src/test/java/androidx/car/app/navigation/NavigationManagerTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/NavigationManagerTest.java
index 8e3b9c9..4916000 100644
--- a/car/app/app/src/test/java/androidx/car/app/navigation/NavigationManagerTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/NavigationManagerTest.java
@@ -74,12 +74,12 @@
             new TravelEstimate.Builder(
                     Distance.create(/* displayDistance= */ 10, Distance.UNIT_KILOMETERS),
                     createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific"))
-            .setRemainingTimeSeconds(TimeUnit.HOURS.toSeconds(1)).build();
+                    .setRemainingTimeSeconds(TimeUnit.HOURS.toSeconds(1)).build();
     private final TravelEstimate mDestinationTravelEstimate =
             new TravelEstimate.Builder(
                     Distance.create(/* displayDistance= */ 100, Distance.UNIT_KILOMETERS),
                     createDateTimeWithZone("2020-04-14T16:57:00", "US/Pacific"))
-            .setRemainingTimeSeconds(TimeUnit.HOURS.toSeconds(1)).build();
+                    .setRemainingTimeSeconds(TimeUnit.HOURS.toSeconds(1)).build();
     private static final String CURRENT_ROAD = "State St.";
     private final Trip mTrip =
             new Trip.Builder()
@@ -236,6 +236,40 @@
     }
 
     @Test
+    public void onStopNavigation_asynchronousCallback_callsIt() throws RemoteException {
+        InOrder inOrder = inOrder(mMockNavHost, mNavigationListener);
+
+        AsynchronousExecutor executor = new AsynchronousExecutor();
+        mNavigationManager.setNavigationManagerCallback(executor,
+                mNavigationListener);
+        mNavigationManager.navigationStarted();
+        inOrder.verify(mMockNavHost).navigationStarted();
+
+        mNavigationManager.onStopNavigation();
+        executor.run();
+
+        inOrder.verify(mNavigationListener).onStopNavigation();
+    }
+
+    @Test
+    public void onStopNavigation_asynchronousCallbackClearedBeforeExecution_doesNotCallIt()
+            throws RemoteException {
+        InOrder inOrder = inOrder(mMockNavHost, mNavigationListener);
+
+        AsynchronousExecutor executor = new AsynchronousExecutor();
+        mNavigationManager.setNavigationManagerCallback(executor,
+                mNavigationListener);
+        mNavigationManager.navigationStarted();
+        inOrder.verify(mMockNavHost).navigationStarted();
+
+        mNavigationManager.onStopNavigation();
+        mNavigationManager.clearNavigationManagerCallback();
+        executor.run();
+
+        inOrder.verify(mNavigationListener, never()).onStopNavigation();
+    }
+
+    @Test
     public void onAutoDriveEnabled_callsListener() {
         mNavigationManager.setNavigationManagerCallback(new SynchronousExecutor(),
                 mNavigationListener);
@@ -260,4 +294,19 @@
         }
     }
 
+    static class AsynchronousExecutor implements Executor {
+        private Runnable mToRun;
+
+        @Override
+        public void execute(Runnable r) {
+            mToRun = r;
+        }
+
+        void run() {
+            if (mToRun != null) {
+                mToRun.run();
+            }
+        }
+    }
+
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldScroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldScroll.kt
index fd4438b..1cac994 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldScroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldScroll.kt
@@ -16,10 +16,10 @@
 
 package androidx.compose.foundation.text
 
-import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.rememberScrollableState
 import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.offset
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
@@ -229,13 +229,13 @@
      * Taken with the opposite sign defines the x or y position of the text field in the
      * horizontal or vertical scroller container correspondingly.
      */
-    var offset by mutableStateOf(initial, structuralEqualityPolicy())
+    var offset by mutableStateOf(initial)
 
     /**
      * Maximum length by which the text field can be scrolled. Defined as a difference in
      * size between the scroller container and the text field.
      */
-    var maximum by mutableStateOf(Float.POSITIVE_INFINITY, structuralEqualityPolicy())
+    var maximum by mutableStateOf(0f)
         private set
 
     /**
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SnapshotState.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SnapshotState.kt
index 3e8396f..95d136a 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SnapshotState.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SnapshotState.kt
@@ -25,6 +25,7 @@
 import androidx.compose.runtime.snapshots.StateObject
 import androidx.compose.runtime.snapshots.StateRecord
 import androidx.compose.runtime.snapshots.newWritableRecord
+import androidx.compose.runtime.snapshots.overwritable
 import androidx.compose.runtime.snapshots.readable
 import androidx.compose.runtime.snapshots.sync
 import androidx.compose.runtime.snapshots.withCurrent
@@ -140,7 +141,7 @@
         get() = next.readable(this).value
         set(value) = next.withCurrent {
             if (!policy.equivalent(it.value, value)) {
-                next.writable(this) { this.value = value }
+                next.overwritable(this, it) { this.value = value }
             }
         }
 
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index 6414755..dec01ae 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -1578,6 +1578,27 @@
     return newData
 }
 
+internal fun <T : StateRecord> T.overwritableRecord(
+    state: StateObject,
+    snapshot: Snapshot,
+    candidate: T
+): T {
+    if (snapshot.readOnly) {
+        // If the snapshot is read-only, use the snapshot recordModified to report it.
+        snapshot.recordModified(state)
+    }
+    val id = snapshot.id
+
+    if (candidate.snapshotId == id) return candidate
+
+    val newData = newOverwritableRecord(state, snapshot)
+    newData.snapshotId = id
+
+    snapshot.recordModified(state)
+
+    return newData
+}
+
 internal fun <T : StateRecord> T.newWritableRecord(state: StateObject, snapshot: Snapshot): T {
     // Calling used() on a state object might return the same record for each thread calling
     // used() therefore selecting the record to reuse should be guarded.
@@ -1591,16 +1612,32 @@
     // cache the result of readable() as the mutating thread calls to writable() can change the
     // result of readable().
     @Suppress("UNCHECKED_CAST")
-    val newData = (used(state, snapshot.id, openSnapshots) as T?)?.apply {
+    val newData = newOverwritableRecord(state, snapshot)
+    newData.assign(this)
+    newData.snapshotId = snapshot.id
+    return newData
+}
+
+internal fun <T : StateRecord> T.newOverwritableRecord(state: StateObject, snapshot: Snapshot): T {
+    // Calling used() on a state object might return the same record for each thread calling
+    // used() therefore selecting the record to reuse should be guarded.
+
+    // Note: setting the snapshotId to Int.MAX_VALUE will make it invalid for all snapshots.
+    // This means the lock can be released as used() will no longer select it. Using id could
+    // also be used but it puts the object into a state where the reused value appears to be
+    // the current valid value for the snapshot. This is not an issue if the snapshot is only
+    // being read from a single thread but using Int.MAX_VALUE allows multiple readers,
+    // single writer, of a snapshot. Note that threads reading a mutating snapshot should not
+    // cache the result of readable() as the mutating thread calls to writable() can change the
+    // result of readable().
+    @Suppress("UNCHECKED_CAST")
+    return (used(state, snapshot.id, openSnapshots) as T?)?.apply {
         snapshotId = Int.MAX_VALUE
     } ?: create().apply {
         snapshotId = Int.MAX_VALUE
         this.next = state.firstStateRecord
         state.prependStateRecord(this as T)
     } as T
-    newData.assign(this)
-    newData.snapshotId = snapshot.id
-    return newData
 }
 
 @PublishedApi
@@ -1649,6 +1686,34 @@
 }
 
 /**
+ * Call [block] with a writable state record for the given record. It is assumed that this is
+ * called for the first state record in a state object. A record is writable if it was created in
+ * the current mutable snapshot. This should only be used when the record will be overwritten in
+ * its entirety (such as having only one field and that field is written to).
+ *
+ * WARNING: If the caller doesn't overwrite all the fields in the state record the object will be
+ * inconsistent and the fields not written are almost guaranteed to be incorrect. If it is
+ * possible that [block] will not write to all the fields use [writable] instead.
+ *
+ * @param state The object that has this record in its record list.
+ * @param candidate The current for the snapshot record returned by [withCurrent]
+ * @param block The block that will mutate all the field of the record.
+ */
+internal inline fun <T : StateRecord, R> T.overwritable(
+    state: StateObject,
+    candidate: T,
+    block: T.() -> R
+): R {
+    var snapshot: Snapshot = snapshotInitializer
+    return sync {
+        snapshot = Snapshot.current
+        this.overwritableRecord(state, snapshot, candidate).block()
+    }.also {
+        notifyWrite(snapshot, state)
+    }
+}
+
+/**
  * Produce a set of optimistic merges of the state records, this is performed outside the
  * a synchronization block to reduce the amount of time taken in the synchronization block
  * reducing the thread contention of merging state values.
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
index 3611a1e..f89d206 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
@@ -21,27 +21,18 @@
 import androidx.compose.runtime.Stable
 
 /**
- * Immutable constraints used for measuring layouts, usually as part of parent
- * [layouts][androidx.compose.ui.layout.Layout] or
- * [layout modifiers][androidx.compose.ui.layout.LayoutModifier].
- * Children layouts can be measured using the `measure` method on the corresponding
- * [Measurables][androidx.compose.ui.layout.Measurable]. This method takes the [Constraints],
- * in pixels, which the child should respect. A measured child is responsible to choose
- * and return a size which satisfies the set of [Constraints] received from its parent:
- * - minWidth <= chosenWidth <= maxWidth
- * - minHeight <= chosenHeight <= maxHeight
- * The parent can then access the size chosen by the child on the resulting
- * [Placeable][androidx.compose.ui.layout.Placeable]. Based on the children sizes, the parent
- * is responsible for defining a valid positioning of the children. This means that children need
- * to be measured with appropriate [Constraints], such that whatever valid sizes children choose,
- * they can be laid out correctly according to the parent's layout algorithm. Note that
- * different children can be measured with different [Constraints].
- * A child is allowed to choose a size that does not satisfy its constraints. However, when this
- * happens, the parent will not read from the placeable the real size of the child, but rather
- * one that was coerced in the child's constraints; therefore, a parent can assume that its
- * children will always respect the constraints in their layout algorithm. When this does not
- * happen in reality, the position assigned to the child will be automatically offset to be centered
- * on the space assigned by the parent under the assumption that constraints were respected.
+ * Immutable constraints for measuring layouts, used by [layouts][androidx.compose.ui.layout.Layout]
+ * or [layout modifiers][androidx.compose.ui.layout.LayoutModifier] to measure their layout
+ * children. The parent chooses the [Constraints] defining a range, in pixels, within which
+ * the measured layout should choose a size:
+ *
+ * - `minWidth` <= `chosenWidth` <= `maxWidth`
+ * - `minHeight` <= `chosenHeight` <= `maxHeight`
+ *
+ * For more details about how layout measurement works, see
+ * [androidx.compose.ui.layout.MeasurePolicy] or
+ * [androidx.compose.ui.layout.LayoutModifier.measure].
+ *
  * A set of [Constraints] can have infinite maxWidth and/or maxHeight. This is a trick often
  * used by parents to ask their children for their preferred size: unbounded constraints force
  * children whose default behavior is to fill the available space (always size to
@@ -129,7 +120,7 @@
         }
 
     /**
-     * `false` when [maxHeight] is [Infinity] and `true` if [maxWidth] is a non-[Infinity] value.
+     * `false` when [maxHeight] is [Infinity] and `true` if [maxHeight] is a non-[Infinity] value.
      * @see hasBoundedWidth
      */
     val hasBoundedHeight: Boolean
@@ -196,7 +187,7 @@
         /**
          * A value that [maxWidth] or [maxHeight] will be set to when the constraint should
          * be considered infinite. [hasBoundedHeight] or [hasBoundedWidth] will be
-         * `true` when [maxHeight] or [maxWidth] is [Infinity], respectively.
+         * `false` when [maxWidth] or [maxHeight] is [Infinity], respectively.
          */
         const val Infinity = Int.MAX_VALUE
 
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
index 8a3533b..3aa73c6 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
@@ -30,6 +30,7 @@
 import android.widget.LinearLayout
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Applier
@@ -610,6 +611,34 @@
         }
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    fun testMove_withoutRedraw() {
+        var offset by mutableStateOf(0)
+        rule.setContent {
+            Box(Modifier.testTag("box").fillMaxSize()) {
+                val offsetDp = with(rule.density) { offset.toDp() }
+                Box(Modifier.offset(offsetDp, offsetDp)) {
+                    AndroidView(::ColoredSquareView, Modifier.graphicsLayer())
+                }
+            }
+        }
+        val offsetColorProvider: (IntOffset) -> Color? = {
+            if (it.x >= offset && it.x < offset + 100 && it.y >= offset && it.y < offset + 100) {
+                Color.Blue
+            } else {
+                null
+            }
+        }
+        rule.onNodeWithTag("box").captureToImage()
+            .assertPixels(expectedColorProvider = offsetColorProvider)
+        rule.runOnUiThread {
+            offset = 100
+        }
+        rule.onNodeWithTag("box").captureToImage()
+            .assertPixels(expectedColorProvider = offsetColorProvider)
+    }
+
     class ColoredSquareView(context: Context) : View(context) {
         var size: Int = 100
             set(value) {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewsHandler.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewsHandler.android.kt
index 7f9c907..30b40a3 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewsHandler.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewsHandler.android.kt
@@ -63,13 +63,7 @@
     override fun invalidateChildInParent(location: IntArray?, dirty: Rect?) = null
 
     fun drawView(view: AndroidViewHolder, canvas: Canvas) {
-        // The canvas is already translated by the Compose logic. But the position of the
-        // AndroidViewHolder is also set on it inside the AndroidViewsHandler, for correct
-        // `getLocationInWindow` results for the Composed View. Therefore, we need to
-        // compensate here to avoid double translating.
-        canvas.translate(-view.x, -view.y)
-        drawChild(canvas, view, drawingTime)
-        canvas.translate(view.x, view.y)
+        view.draw(canvas)
     }
 
     // Touch events forwarding will be handled by component nodes.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutModifier.kt
index a57fce3..3888e20 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutModifier.kt
@@ -45,6 +45,10 @@
      * [Placeable], which defines how the wrapped content should be positioned inside
      * the [LayoutModifier]. A convenient way to create the [MeasureResult]
      * is to use the [MeasureScope.layout] factory function.
+     *
+     * A [LayoutModifier] uses the same measurement and layout concepts and principles as a
+     * [Layout], the only difference is that they apply to exactly one child. For a more detailed
+     * explanation of measurement and layout, see [MeasurePolicy].
      */
     fun MeasureScope.measure(
         measurable: Measurable,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasurePolicy.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasurePolicy.kt
index 936d433..8753c20 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasurePolicy.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasurePolicy.kt
@@ -55,11 +55,31 @@
     /**
      * The function that defines the measurement and layout. Each [Measurable] in the [measurables]
      * list corresponds to a layout child of the layout, and children can be measured using the
-     * [Measurable.measure] method. Measuring a child returns a [Placeable], which can then
-     * be positioned in the [MeasureResult.placeChildren] of the returned [MeasureResult].
-     * Usually [MeasureResult] objects are created using the [MeasureScope.layout] factory, which
-     * takes the calculated size of this layout, its alignment lines, and a block defining
-     * the positioning of the children layouts.
+     * [Measurable.measure] method. This method takes the [Constraints] which the child should
+     * respect; different children can be measured with different constraints.
+     * Measuring a child returns a [Placeable], which reveals the size chosen by the child as a
+     * result of its own measurement. According to the children sizes, the parent is defining the
+     * positioning of the children, by [placing][Placeable.PlacementScope.place] the [Placeable]s in
+     * the [MeasureResult.placeChildren] of the returned [MeasureResult]. Therefore the parent needs
+     * to measure its children with appropriate [Constraints], such that whatever valid sizes
+     * children choose, they can be laid out correctly according to the parent's layout algorithm.
+     * This is because there is no measurement negotiation between the parent and children:
+     * once a child chooses its size, the parent needs to handle it correctly.
+     *
+     * Note that a child is allowed to choose a size that does not satisfy its constraints. However,
+     * when this happens, the placeable's [width][Placeable.width] and [height][Placeable.height]
+     * will not represent the real size of the child, but rather the size coerced in the
+     * child's constraints. Therefore, it is common for parents to assume in their layout
+     * algorithm that its children will always respect the constraints. When this
+     * does not happen in reality, the position assigned to the child will be
+     * automatically offset to be centered on the space assigned by the parent under
+     * the assumption that constraints were respected. Rarely, when a parent really needs to know
+     * the true size of the child, they can read this from the placeable's
+     * [Placeable.measuredWidth] and [Placeable.measuredHeight].
+     *
+     * [MeasureResult] objects are usually created using the [MeasureScope.layout]
+     * factory, which takes the calculated size of this layout, its alignment lines, and a block
+     * defining the positioning of the children layouts.
      */
     fun MeasureScope.measure(
         measurables: List<Measurable>,
diff --git a/core/core-google-shortcuts/api/restricted_current.txt b/core/core-google-shortcuts/api/restricted_current.txt
index e6f50d0..3827b08 100644
--- a/core/core-google-shortcuts/api/restricted_current.txt
+++ b/core/core-google-shortcuts/api/restricted_current.txt
@@ -1 +1,14 @@
 // Signature format: 4.0
+package @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) androidx.core.google.shortcuts {
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public class ShortcutInfoChangeListenerImpl {
+    method public static androidx.core.google.shortcuts.ShortcutInfoChangeListenerImpl getInstance(android.content.Context);
+    method public void onAllShortcutsRemoved();
+    method public void onShortcutAdded(java.util.List<androidx.core.content.pm.ShortcutInfoCompat!>);
+    method public void onShortcutRemoved(java.util.List<java.lang.String!>);
+    method public void onShortcutUpdated(java.util.List<androidx.core.content.pm.ShortcutInfoCompat!>);
+    method public void onShortcutUsageReported(java.util.List<java.lang.String!>);
+  }
+
+}
+
diff --git a/core/core-google-shortcuts/build.gradle b/core/core-google-shortcuts/build.gradle
index 8451eed..a5be6a42 100644
--- a/core/core-google-shortcuts/build.gradle
+++ b/core/core-google-shortcuts/build.gradle
@@ -25,9 +25,25 @@
     id("kotlin-android")
 }
 
+android {
+    defaultConfig {
+        minSdkVersion 21
+    }
+}
+
 dependencies {
     api(KOTLIN_STDLIB)
-    // Add dependencies here
+    api("androidx.core:core:1.5.0-beta03")
+
+    implementation("com.google.firebase:firebase-appindexing:19.2.0")
+
+    androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
+    androidTestImplementation(ANDROIDX_TEST_CORE)
+    androidTestImplementation(ANDROIDX_TEST_RUNNER)
+    androidTestImplementation(ANDROIDX_TEST_RULES)
+    androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy)
+    androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy)
+    androidTestImplementation(TRUTH)
 }
 
 androidx {
diff --git a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImplTest.java b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImplTest.java
new file mode 100644
index 0000000..cc73d99
--- /dev/null
+++ b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImplTest.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2021 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.core.google.shortcuts;
+
+import static androidx.core.google.shortcuts.ShortcutUtils.SHORTCUT_DESCRIPTION_KEY;
+import static androidx.core.google.shortcuts.ShortcutUtils.SHORTCUT_LABEL_KEY;
+import static androidx.core.google.shortcuts.ShortcutUtils.SHORTCUT_URL_KEY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.only;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.core.content.pm.ShortcutInfoCompat;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.firebase.appindexing.Action;
+import com.google.firebase.appindexing.FirebaseAppIndex;
+import com.google.firebase.appindexing.FirebaseUserActions;
+import com.google.firebase.appindexing.Indexable;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RunWith(AndroidJUnit4.class)
+public class ShortcutInfoChangeListenerImplTest {
+    private static final String TEST_PACKAGE = "com.test.package";
+
+    private FirebaseAppIndex mFirebaseAppIndex;
+    private FirebaseUserActions mFirebaseUserActions;
+    private Context mContext;
+    private ShortcutInfoChangeListenerImpl mShortcutInfoChangeListener;
+
+    @Before
+    public void setUp() {
+        mFirebaseAppIndex = mock(FirebaseAppIndex.class);
+        mFirebaseUserActions = mock(FirebaseUserActions.class);
+        mContext = mock(Context.class);
+        mShortcutInfoChangeListener = new ShortcutInfoChangeListenerImpl(
+                mContext, mFirebaseAppIndex, mFirebaseUserActions);
+
+        when(mContext.getPackageName()).thenReturn(TEST_PACKAGE);
+    }
+
+    @Test
+    @SmallTest
+    public void onShortcutUpdated_publicIntent_savesToAppIndex() throws Exception {
+        ArgumentCaptor<Indexable> indexableCaptor = ArgumentCaptor.forClass(Indexable.class);
+
+        Intent intent = Intent.parseUri("http://www.google.com", 0);
+        ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(mContext, "publicIntent")
+                .setShortLabel("short label")
+                .setLongLabel("long label")
+                .setIntent(intent)
+                .setIcon(IconCompat.createWithContentUri("content://abc"))
+                .build();
+
+        mShortcutInfoChangeListener.onShortcutUpdated(Collections.singletonList(shortcut));
+
+        verify(mFirebaseAppIndex, only()).update(indexableCaptor.capture());
+        List<Indexable> allValues = indexableCaptor.getAllValues();
+        Indexable expected = new Indexable.Builder()
+                .setId("publicIntent")
+                .setUrl(ShortcutUtils.getIndexableUrl(mContext, "publicIntent"))
+                .put(SHORTCUT_LABEL_KEY, "short label")
+                .put(SHORTCUT_DESCRIPTION_KEY, "long label")
+                .put(SHORTCUT_URL_KEY, ShortcutUtils.getIndexableShortcutUrl(mContext, intent))
+                .setImage("content://abc")
+                .build();
+        assertThat(allValues).containsExactly(expected);
+    }
+
+    @Test
+    @SmallTest
+    public void onShortcutUpdated_privateIntent_savesToAppIndex() throws Exception {
+        ArgumentCaptor<Indexable> indexableCaptor = ArgumentCaptor.forClass(Indexable.class);
+
+        String privateIntentUri = "#Intent;component=androidx.core.google.shortcuts.test/androidx"
+                + ".core.google.shortcuts.TrampolineActivity;end";
+        Intent intent = Intent.parseUri(privateIntentUri, 0);
+
+        ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(mContext, "privateIntent")
+                .setShortLabel("short label")
+                .setIntent(intent)
+                .build();
+
+        mShortcutInfoChangeListener.onShortcutUpdated(Collections.singletonList(shortcut));
+
+        verify(mFirebaseAppIndex, only()).update(indexableCaptor.capture());
+        List<Indexable> allValues = indexableCaptor.getAllValues();
+        Indexable expected = new Indexable.Builder()
+                .setUrl(ShortcutUtils.getIndexableUrl(mContext, "privateIntent"))
+                .setId("privateIntent")
+                .put("shortcutLabel", "short label")
+                .put("shortcutUrl", ShortcutUtils.getIndexableShortcutUrl(mContext, intent))
+                .build();
+        assertThat(allValues).containsExactly(expected);
+    }
+
+    @Test
+    @SmallTest
+    public void onShortcutAdded_savesToAppIndex() throws Exception {
+        ArgumentCaptor<Indexable> indexableCaptor = ArgumentCaptor.forClass(Indexable.class);
+
+        Intent intent = Intent.parseUri("http://www.google.com", 0);
+        ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(mContext, "intent")
+                .setShortLabel("short label")
+                .setLongLabel("long label")
+                .setIntent(intent)
+                .setIcon(IconCompat.createWithContentUri("content://abc"))
+                .build();
+
+        mShortcutInfoChangeListener.onShortcutAdded(Collections.singletonList(shortcut));
+
+        verify(mFirebaseAppIndex, only()).update(indexableCaptor.capture());
+        List<Indexable> allValues = indexableCaptor.getAllValues();
+        Indexable expected = new Indexable.Builder()
+                .setId("intent")
+                .setUrl(ShortcutUtils.getIndexableUrl(mContext, "intent"))
+                .put(SHORTCUT_LABEL_KEY, "short label")
+                .put(SHORTCUT_DESCRIPTION_KEY, "long label")
+                .put(SHORTCUT_URL_KEY, ShortcutUtils.getIndexableShortcutUrl(mContext, intent))
+                .setImage("content://abc")
+                .build();
+        assertThat(allValues).containsExactly(expected);
+    }
+
+    @Test
+    @SmallTest
+    public void onShortcutRemoved_removeFromAppIndex() {
+        ArgumentCaptor<String> urlsCaptor = ArgumentCaptor.forClass(String.class);
+
+        mShortcutInfoChangeListener.onShortcutRemoved(Arrays.asList("id1", "id2"));
+
+        verify(mFirebaseAppIndex, only()).remove(urlsCaptor.capture());
+        List<String> urls = urlsCaptor.getAllValues();
+        assertThat(urls).containsExactly(
+                ShortcutUtils.getIndexableUrl(mContext, "id1"),
+                ShortcutUtils.getIndexableUrl(mContext, "id2"));
+    }
+
+    @Test
+    @SmallTest
+    public void onAllShortcutRemoved_removeFromAppIndex() {
+        mShortcutInfoChangeListener.onAllShortcutsRemoved();
+        verify(mFirebaseAppIndex, only()).removeAll();
+    }
+
+    @Test
+    @SmallTest
+    public void onShortcutUsageReported_savesToUserActions() {
+        ArgumentCaptor<Action> actionCaptor = ArgumentCaptor.forClass(Action.class);
+
+        mShortcutInfoChangeListener.onShortcutUsageReported(Arrays.asList("id1", "id2"));
+
+        verify(mFirebaseUserActions, times(2)).end(actionCaptor.capture());
+        List<Action> actions = actionCaptor.getAllValues();
+        List<String> actionsString =
+                actions.stream().map(Object::toString).collect(Collectors.toList());
+        Action expectedAction1 = new Action.Builder(Action.Builder.VIEW_ACTION)
+                .setObject("", ShortcutUtils.getIndexableUrl(mContext, "id1"))
+                .setMetadata(new Action.Metadata.Builder().setUpload(false))
+                .build();
+        Action expectedAction2 = new Action.Builder(Action.Builder.VIEW_ACTION)
+                .setObject("", ShortcutUtils.getIndexableUrl(mContext, "id2"))
+                .setMetadata(new Action.Metadata.Builder().setUpload(false))
+                .build();
+        // Action has no equals comparator, so instead we compare their string forms.
+        assertThat(actionsString).containsExactly(expectedAction1.toString(),
+                expectedAction2.toString());
+    }
+}
diff --git a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutUtilsTest.java b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutUtilsTest.java
new file mode 100644
index 0000000..84862ac
--- /dev/null
+++ b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutUtilsTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2021 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.core.google.shortcuts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class ShortcutUtilsTest {
+    private static final String TEST_PACKAGE = "com.test.package";
+
+    @Test
+    @SmallTest
+    public void testGetIndexableUrl_returnsTrampolineActivityIntentUriWithIdExtra() {
+        Context context = mock(Context.class);
+        when(context.getPackageName()).thenReturn(TEST_PACKAGE);
+
+        String id = "intentId";
+
+        String url = ShortcutUtils.getIndexableUrl(context, id);
+
+        String expectedUri = String.format("intent:#Intent;component=%s/androidx.core.google"
+                        + ".shortcuts.TrampolineActivity;S.id=%s;end", TEST_PACKAGE, id);
+        assertThat(url).isEqualTo(expectedUri);
+    }
+
+    @Test
+    @SmallTest
+    public void testGetIndexableShortcutUrl_returnsShortcutUrl() throws Exception {
+        Context context = mock(Context.class);
+        Intent intent = Intent.parseUri("http://www.google.com", 0);
+
+        String shortcutUrl = ShortcutUtils.getIndexableShortcutUrl(context, intent);
+
+        String expectedShortcutUrl = "http://www.google.com";
+        assertThat(shortcutUrl).isEqualTo(expectedShortcutUrl);
+    }
+}
diff --git a/core/core-google-shortcuts/src/main/AndroidManifest.xml b/core/core-google-shortcuts/src/main/AndroidManifest.xml
index c97471d..877e94a 100644
--- a/core/core-google-shortcuts/src/main/AndroidManifest.xml
+++ b/core/core-google-shortcuts/src/main/AndroidManifest.xml
@@ -16,5 +16,11 @@
   -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="androidx.core.google.shortcuts">
-
+    <application>
+        <activity android:name=".TrampolineActivity"
+            android:noHistory="true"
+            android:theme="@android:style/Theme.NoDisplay"
+            android:exported="false">
+        </activity>
+    </application>
 </manifest>
\ No newline at end of file
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java
new file mode 100644
index 0000000..c9f9e80
--- /dev/null
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2021 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.core.google.shortcuts;
+
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.core.google.shortcuts.ShortcutUtils.SHORTCUT_DESCRIPTION_KEY;
+import static androidx.core.google.shortcuts.ShortcutUtils.SHORTCUT_LABEL_KEY;
+import static androidx.core.google.shortcuts.ShortcutUtils.SHORTCUT_URL_KEY;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.content.pm.ShortcutInfoCompat;
+import androidx.core.graphics.drawable.IconCompat;
+
+import com.google.firebase.appindexing.Action;
+import com.google.firebase.appindexing.FirebaseAppIndex;
+import com.google.firebase.appindexing.FirebaseUserActions;
+import com.google.firebase.appindexing.Indexable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides a listener on changes to shortcuts in ShortcutInfoCompat.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+// TODO (b/182185987): update class to extend ShortcutInfoChangeListener once that is added to core
+public class ShortcutInfoChangeListenerImpl {
+    private final Context mContext;
+    private final FirebaseAppIndex mFirebaseAppIndex;
+    private final FirebaseUserActions mFirebaseUserActions;
+
+    /**
+     * Create an instance of {@link ShortcutInfoChangeListenerImpl}.
+     *
+     * @param context The application context.
+     * @return {@link ShortcutInfoChangeListenerImpl}.
+     */
+    @NonNull
+    public static ShortcutInfoChangeListenerImpl getInstance(@NonNull Context context) {
+        return new ShortcutInfoChangeListenerImpl(context, FirebaseAppIndex.getInstance(context),
+                FirebaseUserActions.getInstance(context));
+    }
+
+    @VisibleForTesting
+    ShortcutInfoChangeListenerImpl(Context context, FirebaseAppIndex firebaseAppIndex,
+            FirebaseUserActions firebaseUserActions) {
+        mContext = context;
+        mFirebaseAppIndex = firebaseAppIndex;
+        mFirebaseUserActions = firebaseUserActions;
+    }
+
+    /**
+     * Called when shortcut is added by {@link androidx.core.content.pm.ShortcutManagerCompat}.
+     *
+     * @param shortcuts list of shortcuts added
+     */
+    public void onShortcutAdded(@NonNull List<ShortcutInfoCompat> shortcuts) {
+        mFirebaseAppIndex.update(buildIndexables(shortcuts));
+    }
+
+    /**
+     * Called when shortcut is updated by {@link androidx.core.content.pm.ShortcutManagerCompat}.
+     *
+     * @param shortcuts list of shortcuts updated
+     */
+    public void onShortcutUpdated(@NonNull List<ShortcutInfoCompat> shortcuts) {
+        mFirebaseAppIndex.update(buildIndexables(shortcuts));
+    }
+
+    /**
+     * Called when shortcut is removed by {@link androidx.core.content.pm.ShortcutManagerCompat}.
+     *
+     * @param shortcutIds list of shortcut ids removed
+     */
+    public void onShortcutRemoved(@NonNull List<String> shortcutIds) {
+        List<String> urls = new ArrayList<>();
+        for (String shortcutId : shortcutIds) {
+            urls.add(ShortcutUtils.getIndexableUrl(mContext, shortcutId));
+        }
+        mFirebaseAppIndex.remove(urls.toArray(new String[0]));
+    }
+
+    /**
+     * Called when shortcut is used by {@link androidx.core.content.pm.ShortcutManagerCompat}.
+     *
+     * @param shortcutIds list of shortcut ids used
+     */
+    public void onShortcutUsageReported(@NonNull List<String> shortcutIds) {
+        for (String shortcutId : shortcutIds) {
+            // Actions reported here is only on-device due to setUpload(false) in buildAction
+            // method.
+            mFirebaseUserActions.end(buildAction(ShortcutUtils.getIndexableUrl(mContext,
+                    shortcutId)));
+        }
+    }
+
+    /**
+     * Called when all shortcuts are removed
+     * by {@link androidx.core.content.pm.ShortcutManagerCompat}.
+     */
+    public void onAllShortcutsRemoved() {
+        mFirebaseAppIndex.removeAll();
+    }
+
+    @NonNull
+    private Action buildAction(@NonNull String url) {
+        // The reported action isn't uploaded to the server.
+        Action.Metadata.Builder metadataBuilder = new Action.Metadata.Builder().setUpload(false);
+        return new Action.Builder(Action.Builder.VIEW_ACTION)
+                // Empty label as placeholder.
+                .setObject("", url)
+                .setMetadata(metadataBuilder)
+                .build();
+    }
+
+    @NonNull
+    private Indexable[] buildIndexables(@NonNull List<ShortcutInfoCompat> shortcuts) {
+        List<Indexable> indexables = new ArrayList<>();
+        for (ShortcutInfoCompat shortcut : shortcuts) {
+            indexables.add(buildIndexable(shortcut));
+        }
+        return indexables.toArray(new Indexable[0]);
+    }
+
+    @NonNull
+    private Indexable buildIndexable(@NonNull ShortcutInfoCompat shortcut) {
+        String url = ShortcutUtils.getIndexableUrl(mContext, shortcut.getId());
+        String shortcutUrl = ShortcutUtils.getIndexableShortcutUrl(mContext, shortcut.getIntent());
+
+        Indexable.Builder builder = new Indexable.Builder()
+                .setId(shortcut.getId())
+                .setUrl(url)
+                .put(SHORTCUT_URL_KEY, shortcutUrl)
+                .put(SHORTCUT_LABEL_KEY, shortcut.getShortLabel().toString());
+
+        if (shortcut.getLongLabel() != null) {
+            builder.put(SHORTCUT_DESCRIPTION_KEY, shortcut.getLongLabel().toString());
+        }
+
+        if (shortcut.getIcon() != null && shortcut.getIcon().getType() == IconCompat.TYPE_URI) {
+            builder.setImage(shortcut.getIcon().getUri().toString());
+        }
+
+        // TODO (b/182186140): add logic for matching names and CapabilityBinding
+
+        // By default, the indexable will be saved only on-device.
+        return builder.build();
+    }
+}
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutUtils.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutUtils.java
new file mode 100644
index 0000000..d7b68ef
--- /dev/null
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutUtils.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2021 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.core.google.shortcuts;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Utility methods and constants used by the google shortcuts library.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+class ShortcutUtils {
+    public static final String SHORTCUT_LABEL_KEY = "shortcutLabel";
+    public static final String SHORTCUT_DESCRIPTION_KEY = "shortcutDescription";
+    public static final String SHORTCUT_URL_KEY = "shortcutUrl";
+    public static final String ID_KEY = "id";
+
+    /**
+     * Generate value for Indexable url field. The url field will not be used for anything other
+     * than referencing the Indexable object. But since it requires that it's openable by the
+     * app, we generate it as an intent that opens the Trampoline Activity.
+     *
+     * @param context the app's context.
+     * @param shortcutId the shortcut id used to generate the url.
+     * @return the indexable url.
+     */
+    public static String getIndexableUrl(@NonNull Context context, @NonNull String shortcutId) {
+        Intent intent = new Intent(context, TrampolineActivity.class);
+        intent.putExtra(ID_KEY, shortcutId);
+
+        return intent.toUri(Intent.URI_INTENT_SCHEME);
+    }
+
+    /**
+     * Generate value for Indexable shortcutUrl field. This field will be used by Google
+     * Assistant to open shortcuts.
+     *
+     * @param context the app's context.
+     * @param shortcutIntent the intent that the shortcut opens.
+     * @return the shortcut url.
+     */
+    public static String getIndexableShortcutUrl(@NonNull Context context,
+            @NonNull Intent shortcutIntent) {
+        // TODO (b/182599835): support private shortcut intents by wrapping it inside an intent
+        //  that launches the trampoline activity.
+        return shortcutIntent.toUri(0);
+    }
+
+    private ShortcutUtils() {}
+}
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/TrampolineActivity.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/TrampolineActivity.java
new file mode 100644
index 0000000..6100911
--- /dev/null
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/TrampolineActivity.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 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.core.google.shortcuts;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+
+/**
+ * Activity used to receives shortcut intents sent from Google, extracts its shortcut url, and
+ * launches it in the scope of the app.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+class TrampolineActivity extends Activity {
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+    }
+}
diff --git a/core/core-google-shortcuts/src/main/androidx/core/package-info.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/package-info.java
similarity index 75%
rename from core/core-google-shortcuts/src/main/androidx/core/package-info.java
rename to core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/package-info.java
index 2b06a14..fe061a9 100644
--- a/core/core-google-shortcuts/src/main/androidx/core/package-info.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/package-info.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright 2021 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.
@@ -15,6 +15,11 @@
  */
 
 /**
- * Insert package level documentation here
+ * @hide
  */
+@RestrictTo(LIBRARY_GROUP)
 package androidx.core.google.shortcuts;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import androidx.annotation.RestrictTo;
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index 79dae26..e4767be 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -1074,6 +1074,7 @@
 
   public static class ShortcutInfoCompat.Builder {
     ctor public ShortcutInfoCompat.Builder(android.content.Context, String);
+    method public androidx.core.content.pm.ShortcutInfoCompat.Builder addCapabilityBinding(String, java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!>?);
     method public androidx.core.content.pm.ShortcutInfoCompat build();
     method public androidx.core.content.pm.ShortcutInfoCompat.Builder setActivity(android.content.ComponentName);
     method public androidx.core.content.pm.ShortcutInfoCompat.Builder setAlwaysBadged();
@@ -1092,6 +1093,7 @@
     method public androidx.core.content.pm.ShortcutInfoCompat.Builder setPersons(androidx.core.app.Person![]);
     method public androidx.core.content.pm.ShortcutInfoCompat.Builder setRank(int);
     method public androidx.core.content.pm.ShortcutInfoCompat.Builder setShortLabel(CharSequence);
+    method public androidx.core.content.pm.ShortcutInfoCompat.Builder setSliceUri(android.net.Uri);
   }
 
   public class ShortcutManagerCompat {
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index 2db02d0..862c495 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -1074,6 +1074,7 @@
 
   public static class ShortcutInfoCompat.Builder {
     ctor public ShortcutInfoCompat.Builder(android.content.Context, String);
+    method public androidx.core.content.pm.ShortcutInfoCompat.Builder addCapabilityBinding(String, java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!>?);
     method public androidx.core.content.pm.ShortcutInfoCompat build();
     method public androidx.core.content.pm.ShortcutInfoCompat.Builder setActivity(android.content.ComponentName);
     method public androidx.core.content.pm.ShortcutInfoCompat.Builder setAlwaysBadged();
@@ -1092,6 +1093,7 @@
     method public androidx.core.content.pm.ShortcutInfoCompat.Builder setPersons(androidx.core.app.Person![]);
     method public androidx.core.content.pm.ShortcutInfoCompat.Builder setRank(int);
     method public androidx.core.content.pm.ShortcutInfoCompat.Builder setShortLabel(CharSequence);
+    method public androidx.core.content.pm.ShortcutInfoCompat.Builder setSliceUri(android.net.Uri);
   }
 
   public class ShortcutManagerCompat {
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 3fb7af8..a1037c06 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -1152,6 +1152,15 @@
     method public static int getProtectionFlags(android.content.pm.PermissionInfo);
   }
 
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class ShortcutInfoChangeListener {
+    ctor public ShortcutInfoChangeListener();
+    method @AnyThread public void onAllShortcutsRemoved();
+    method @AnyThread public void onShortcutAdded(java.util.List<androidx.core.content.pm.ShortcutInfoCompat!>);
+    method @AnyThread public void onShortcutRemoved(java.util.List<java.lang.String!>);
+    method @AnyThread public void onShortcutUpdated(java.util.List<androidx.core.content.pm.ShortcutInfoCompat!>);
+    method @AnyThread public void onShortcutUsageReported(java.util.List<java.lang.String!>);
+  }
+
   public class ShortcutInfoCompat {
     method public android.content.ComponentName? getActivity();
     method public java.util.Set<java.lang.String!>? getCategories();
@@ -1183,6 +1192,7 @@
     ctor public ShortcutInfoCompat.Builder(android.content.Context, String);
     ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public ShortcutInfoCompat.Builder(androidx.core.content.pm.ShortcutInfoCompat);
     ctor @RequiresApi(25) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public ShortcutInfoCompat.Builder(android.content.Context, android.content.pm.ShortcutInfo);
+    method public androidx.core.content.pm.ShortcutInfoCompat.Builder addCapabilityBinding(String, java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!>?);
     method public androidx.core.content.pm.ShortcutInfoCompat build();
     method public androidx.core.content.pm.ShortcutInfoCompat.Builder setActivity(android.content.ComponentName);
     method public androidx.core.content.pm.ShortcutInfoCompat.Builder setAlwaysBadged();
@@ -1201,6 +1211,7 @@
     method public androidx.core.content.pm.ShortcutInfoCompat.Builder setPersons(androidx.core.app.Person![]);
     method public androidx.core.content.pm.ShortcutInfoCompat.Builder setRank(int);
     method public androidx.core.content.pm.ShortcutInfoCompat.Builder setShortLabel(CharSequence);
+    method public androidx.core.content.pm.ShortcutInfoCompat.Builder setSliceUri(android.net.Uri);
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class ShortcutInfoCompatSaver<T> {
diff --git a/core/core/src/androidTest/java/androidx/core/content/pm/ShortcutInfoCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/pm/ShortcutInfoCompatTest.java
index 50637e2..b25919c 100644
--- a/core/core/src/androidTest/java/androidx/core/content/pm/ShortcutInfoCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/pm/ShortcutInfoCompatTest.java
@@ -34,8 +34,10 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
+import android.net.Uri;
 import android.os.PersistableBundle;
 
+import androidx.collection.ArrayMap;
 import androidx.core.app.Person;
 import androidx.core.app.TestActivity;
 import androidx.core.content.ContextCompat;
@@ -52,7 +54,11 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 @MediumTest
@@ -158,6 +164,110 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 21)
+    public void testBuilder_setCapabilities() {
+        final String capability1 = "START_EXERCISE";
+        final String capability1Param1 = "exerciseName";
+        final String capability1Param2 = "duration";
+        final String capability1Param1Value1 = "jogging;running";
+        final String capability1Param1Value2 = "sleeping";
+        final String capability2 = "STOP_EXERCISE";
+        final String capability2Param1 = "exerciseName";
+        final String capability2Param2Value1 = "sleeping";
+        final String sliceUri = "slice-content://com.myfitnessapp/exercise{?start,end}";
+
+        /*
+         * Setup capability 1
+         * {
+         *     "START_EXERCISE": {
+         *         "exerciseName": ["jogging;running","sleeping"],
+         *         "duration": null
+         *     }
+         * }
+         */
+        final Map<String, List<String>> capability1Params = new ArrayMap<>();
+        final List<String> capability1Params1Values = new ArrayList<>();
+        capability1Params1Values.add(capability1Param1Value1);
+        capability1Params1Values.add(capability1Param1Value2);
+        capability1Params.put(capability1Param1, capability1Params1Values);
+        capability1Params.put(capability1Param2, null);
+
+        /*
+         * Setup capability 2
+         * {
+         *     "STOP_EXERCISE": {
+         *         "exerciseName": ["sleeping"],
+         *     }
+         * }
+         */
+        final Map<String, List<String>> capability2Params = new ArrayMap<>();
+        final List<String> capability2Param2Values = new ArrayList<>();
+        capability2Param2Values.add(capability2Param2Value1);
+        capability2Params.put(capability2Param1, capability2Param2Values);
+
+        final ShortcutInfoCompat compat = mBuilder
+                .addCapabilityBinding(capability1, capability1Params)
+                .addCapabilityBinding(capability2, capability2Params)
+                .setSliceUri(Uri.parse(sliceUri))
+                .build();
+
+        /*
+         * Verify the extras contains mapping of capability to their parameter names.
+         * {
+         *     "START_EXERCISE": ["exerciseName","duration"],
+         *     "STOP_EXERCISE": ["exerciseName"],
+         * }
+         */
+        final Set<String> categories = compat.mCategories;
+        assertNotNull(categories);
+        assertTrue(categories.contains(capability1));
+        assertTrue(categories.contains(capability2));
+        final PersistableBundle extra = compat.getExtras();
+        assertNotNull(extra);
+        assertTrue(extra.containsKey(capability1));
+        assertTrue(extra.containsKey(capability2));
+        final String[] paramNamesForCapability1 = extra.getStringArray(capability1);
+        final String[] paramNamesForCapability2 = extra.getStringArray(capability2);
+        assertNotNull(paramNamesForCapability1);
+        assertNotNull(paramNamesForCapability2);
+        assertEquals(2, paramNamesForCapability1.length);
+        assertEquals(1, paramNamesForCapability2.length);
+        final List<String> parameterListForCapability1 = Arrays.asList(paramNamesForCapability1);
+        assertTrue(parameterListForCapability1.contains(capability1Param1));
+        assertTrue(parameterListForCapability1.contains(capability1Param2));
+        assertEquals(capability2Param1, paramNamesForCapability2[0]);
+
+        /*
+         * Verify the extras contains mapping of capability params to their values.
+         * {
+         *     "START_EXERCISE/exerciseName": ["jogging;running","sleeping"],
+         *     "START_EXERCISE/duration": [],
+         *     "STOP_EXERCISE/exerciseName": ["sleeping"],
+         * }
+         */
+        final String capability1Param1Key = capability1 + "/" + capability1Param1;
+        final String capability1Param2Key = capability1 + "/" + capability1Param2;
+        final String capability2Param1Key = capability2 + "/" + capability2Param1;
+        assertTrue(extra.containsKey(capability1Param1Key));
+        assertTrue(extra.containsKey(capability1Param2Key));
+        assertTrue(extra.containsKey(capability2Param1Key));
+        final String[] actualCapability1Params1 = extra.getStringArray(capability1Param1Key);
+        final String[] actualCapability1Params2 = extra.getStringArray(capability1Param2Key);
+        final String[] actualCapability2Params1 = extra.getStringArray(capability2Param1Key);
+        assertNotNull(actualCapability1Params1);
+        assertEquals(2, actualCapability1Params1.length);
+        assertEquals(capability1Param1Value1, actualCapability1Params1[0]);
+        assertEquals(capability1Param1Value2, actualCapability1Params1[1]);
+        assertNotNull(actualCapability1Params2);
+        assertEquals(0, actualCapability1Params2.length);
+        assertNotNull(actualCapability2Params1);
+        assertEquals(1, actualCapability2Params1.length);
+        assertEquals(capability2Param2Value1, actualCapability2Params1[0]);
+        assertTrue(extra.containsKey("extraSliceUri"));
+        assertEquals(sliceUri, extra.getString("extraSliceUri"));
+    }
+
+    @Test
     @SdkSuppress(minSdkVersion = 25)
     public void testBuilder_copyConstructor() {
         String longLabel = "Test long label";
diff --git a/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoChangeListener.java b/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoChangeListener.java
new file mode 100644
index 0000000..9eb69cc
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoChangeListener.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2021 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.core.content.pm;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+import java.util.List;
+
+/**
+ * Defines a listener for {@link ShortcutInfoCompat} changes in {@link ShortcutManagerCompat}. This
+ * class is no-op as is and may be overridden to provide the required functionality.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP_PREFIX)
+public abstract class ShortcutInfoChangeListener {
+    @AnyThread
+    public void onShortcutAdded(@NonNull List<ShortcutInfoCompat> shortcuts) {}
+
+    @AnyThread
+    public void onShortcutUpdated(@NonNull List<ShortcutInfoCompat> shortcuts) {}
+
+    @AnyThread
+    public void onShortcutRemoved(@NonNull List<String> shortcutIds) {}
+
+    @AnyThread
+    public void onAllShortcutsRemoved() {}
+
+    @AnyThread
+    public void onShortcutUsageReported(@NonNull List<String> shortcutIds) {}
+
+    /**
+     * Implementation that does nothing and returns directly from asynchronous methods.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY)
+    public static class NoopImpl extends ShortcutInfoChangeListener {}
+}
diff --git a/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompat.java b/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompat.java
index 7751de0..dfbc80e 100644
--- a/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompat.java
@@ -17,6 +17,7 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
 
+import android.annotation.SuppressLint;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -24,6 +25,7 @@
 import android.content.pm.ShortcutInfo;
 import android.content.pm.ShortcutManager;
 import android.graphics.drawable.Drawable;
+import android.net.Uri;
 import android.os.Build;
 import android.os.PersistableBundle;
 import android.os.UserHandle;
@@ -37,11 +39,14 @@
 import androidx.core.app.Person;
 import androidx.core.content.LocusIdCompat;
 import androidx.core.graphics.drawable.IconCompat;
+import androidx.core.net.UriCompat;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -54,6 +59,8 @@
     private static final String EXTRA_LOCUS_ID = "extraLocusId";
     private static final String EXTRA_LONG_LIVED = "extraLongLived";
 
+    private static final String EXTRA_SLICE_URI = "extraSliceUri";
+
     Context mContext;
     String mId;
     String mPackageName;
@@ -490,6 +497,9 @@
 
         private final ShortcutInfoCompat mInfo;
         private boolean mIsConversation;
+        private Set<String> mCapabilityBindings;
+        private Map<String, Map<String, List<String>>> mCapabilityBindingParams;
+        private Uri mSliceUri;
 
         public Builder(@NonNull Context context, @NonNull String id) {
             mInfo = new ShortcutInfoCompat();
@@ -744,7 +754,7 @@
          * <li> Used by the system to associate a published Sharing Shortcut with supported
          * mimeTypes. Required for published Sharing Shortcuts with a matching category
          * declared in share targets, defined in the app's manifest linked shortcuts xml file.
-         * </ul>         
+         * </ul>
          *
          * @see ShortcutInfo#getCategories()
          */
@@ -802,8 +812,53 @@
         }
 
         /**
+         * Associates a shortcut with a capability. Used when the shortcut is an instance
+         * of a capability.
+         *
+         * <P>This method can be called multiple times to associate multiple capabilities with
+         * this shortcut.
+         *
+         * @param parameters Optional capability parameters associated with given
+         * capability. This will be a mapping of parameter names to zero or
+         * more parameter values. e.g. {"START_EXERCISE": ["jogging", "dancing"],
+         * "STOP_EXERCISE": []}
+         */
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder addCapabilityBinding(@NonNull String capability,
+                @Nullable Map<String, List<String>> parameters) {
+            if (mCapabilityBindings == null) {
+                mCapabilityBindings = new HashSet<>();
+            }
+            mCapabilityBindings.add(capability);
+
+            if (parameters != null) {
+                if (mCapabilityBindingParams == null) {
+                    mCapabilityBindingParams = new HashMap<>();
+                }
+                if (mCapabilityBindingParams.get(capability) == null) {
+                    mCapabilityBindingParams.put(capability, new HashMap<String, List<String>>());
+                }
+                mCapabilityBindingParams.get(capability).putAll(parameters);
+            }
+            return this;
+        }
+
+        /**
+         * Sets the slice uri for a shortcut. The uri will be used if this shortcuts represents a
+         * slice, instead of an intent.
+         */
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder setSliceUri(@NonNull Uri sliceUri) {
+            mSliceUri = sliceUri;
+            return this;
+        }
+
+        /**
          * Creates a {@link ShortcutInfoCompat} instance.
          */
+        @SuppressLint("UnsafeNewApiCall")
         @NonNull
         public ShortcutInfoCompat build() {
             // Verify the arguments
@@ -819,6 +874,41 @@
                 }
                 mInfo.mIsLongLived = true;
             }
+
+            if (mCapabilityBindings != null) {
+                if (mInfo.mCategories == null) {
+                    mInfo.mCategories = new HashSet<>();
+                }
+                mInfo.mCategories.addAll(mCapabilityBindings);
+            }
+            if (Build.VERSION.SDK_INT >= 21) {
+                if (mCapabilityBindingParams != null) {
+                    if (mInfo.mExtras == null) {
+                        mInfo.mExtras = new PersistableBundle();
+                    }
+                    for (String capability : mCapabilityBindingParams.keySet()) {
+                        final Map<String, List<String>> params =
+                                mCapabilityBindingParams.get(capability);
+                        final Set<String> paramNames = params.keySet();
+                        // Persist the mapping of <Capability1> -> [<Param1>, <Param2> ... ]
+                        mInfo.mExtras.putStringArray(
+                                capability, paramNames.toArray(new String[0]));
+                        // Persist the capability param in respect to capability
+                        // i.e. <Capability1/Param1> -> [<Value1>, <Value2> ... ]
+                        for (String paramName : params.keySet()) {
+                            final List<String> value = params.get(paramName);
+                            mInfo.mExtras.putStringArray(capability + "/" + paramName,
+                                    value == null ? new String[0] : value.toArray(new String[0]));
+                        }
+                    }
+                }
+                if (mSliceUri != null) {
+                    if (mInfo.mExtras == null) {
+                        mInfo.mExtras = new PersistableBundle();
+                    }
+                    mInfo.mExtras.putString(EXTRA_SLICE_URI, UriCompat.toSafeString(mSliceUri));
+                }
+            }
             return mInfo;
         }
     }
diff --git a/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java b/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java
index 1e2a225..1478ad1 100644
--- a/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java
@@ -124,6 +124,13 @@
      */
     private static volatile ShortcutInfoCompatSaver<?> sShortcutInfoCompatSaver = null;
 
+    /**
+     * Will be instantiated by reflection to load an implementation from another module if possible.
+     * If fails to load an implementation via reflection, will use the default implementation which
+     * is no-op.
+     */
+    private static volatile ShortcutInfoChangeListener sShortcutInfoChangeListener = null;
+
     private ShortcutManagerCompat() {
         /* Hide constructor */
     }
@@ -310,6 +317,7 @@
         }
 
         getShortcutInfoSaverInstance(context).addShortcuts(shortcutInfoList);
+        getShortcutInfoListenerInstance(context).onShortcutAdded(shortcutInfoList);
         return true;
     }
 
@@ -397,6 +405,9 @@
         if (Build.VERSION.SDK_INT >= 25) {
             context.getSystemService(ShortcutManager.class).reportShortcutUsed(shortcutId);
         }
+
+        getShortcutInfoListenerInstance(context)
+                .onShortcutUsageReported(Collections.singletonList(shortcutId));
     }
 
     /**
@@ -435,6 +446,9 @@
         }
         getShortcutInfoSaverInstance(context).removeAllShortcuts();
         getShortcutInfoSaverInstance(context).addShortcuts(shortcutInfoList);
+
+        getShortcutInfoListenerInstance(context).onAllShortcutsRemoved();
+        getShortcutInfoListenerInstance(context).onShortcutAdded(shortcutInfoList);
         return true;
     }
 
@@ -493,6 +507,7 @@
         }
 
         getShortcutInfoSaverInstance(context).addShortcuts(shortcutInfoList);
+        getShortcutInfoListenerInstance(context).onShortcutUpdated(shortcutInfoList);
         return true;
     }
 
@@ -556,6 +571,7 @@
         }
 
         getShortcutInfoSaverInstance(context).removeShortcuts(shortcutIds);
+        getShortcutInfoListenerInstance(context).onShortcutRemoved(shortcutIds);
     }
 
     /**
@@ -583,6 +599,7 @@
         }
 
         getShortcutInfoSaverInstance(context).addShortcuts(shortcutInfoList);
+        getShortcutInfoListenerInstance(context).onShortcutAdded(shortcutInfoList);
     }
 
     /**
@@ -599,6 +616,7 @@
         }
 
         getShortcutInfoSaverInstance(context).removeShortcuts(shortcutIds);
+        getShortcutInfoListenerInstance(context).onShortcutRemoved(shortcutIds);
     }
 
     /**
@@ -614,6 +632,7 @@
         }
 
         getShortcutInfoSaverInstance(context).removeAllShortcuts();
+        getShortcutInfoListenerInstance(context).onAllShortcutsRemoved();
     }
 
     /**
@@ -636,6 +655,7 @@
 
         context.getSystemService(ShortcutManager.class).removeLongLivedShortcuts(shortcutIds);
         getShortcutInfoSaverInstance(context).removeShortcuts(shortcutIds);
+        getShortcutInfoListenerInstance(context).onShortcutRemoved(shortcutIds);
     }
 
     /**
@@ -709,6 +729,9 @@
             return true;
         } catch (Exception e) {
             // Ignore
+        } finally {
+            getShortcutInfoListenerInstance(context)
+                    .onShortcutAdded(Collections.singletonList(shortcut));
         }
         return false;
     }
@@ -766,6 +789,29 @@
         return sShortcutInfoCompatSaver;
     }
 
+    private static ShortcutInfoChangeListener getShortcutInfoListenerInstance(Context context) {
+        if (sShortcutInfoChangeListener == null) {
+            if (Build.VERSION.SDK_INT >= 21) {
+                try {
+                    ClassLoader loader = ShortcutManagerCompat.class.getClassLoader();
+                    Class<?> listener =
+                            Class.forName(
+                                    "androidx.core.google.shortcuts.ShortcutInfoChangeListenerImpl",
+                                    false, loader);
+                    Method getInstanceMethod = listener.getMethod("getInstance", Context.class);
+                    sShortcutInfoChangeListener =
+                            (ShortcutInfoChangeListener) getInstanceMethod.invoke(null, context);
+                } catch (Exception e) { /* Do nothing */ }
+
+                if (sShortcutInfoChangeListener == null) {
+                    // Implementation not available. Instantiate to the default no-op impl.
+                    sShortcutInfoChangeListener = new ShortcutInfoChangeListener.NoopImpl();
+                }
+            }
+        }
+        return sShortcutInfoChangeListener;
+    }
+
     @RequiresApi(25)
     private static class Api25Impl {
         static String getShortcutInfoWithLowestRank(@NonNull final List<ShortcutInfo> shortcuts) {