[go: nahoru, domu]

Add pan and zoom feature in `PlaceListNavigationTemplate` and `RoutePreviewNavigationTemplate`

Bug: 200328371
Test: unit test
Relnote: Added support for panning and zooming in `PlaceListNavigationTemplate` and `RoutePreviewNavigationTemplate`
Change-Id: I9d8a3ff7751ffd5423201b2f29886adff38e8bde
diff --git a/car/app/app/api/public_plus_experimental_1.1.0-beta02.txt b/car/app/app/api/public_plus_experimental_1.1.0-beta02.txt
index e43b6f474..d688300 100644
--- a/car/app/app/api/public_plus_experimental_1.1.0-beta02.txt
+++ b/car/app/app/api/public_plus_experimental_1.1.0-beta02.txt
@@ -1200,6 +1200,8 @@
     method public androidx.car.app.model.ActionStrip? getActionStrip();
     method public androidx.car.app.model.Action? getHeaderAction();
     method public androidx.car.app.model.ItemList? getItemList();
+    method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(4) public androidx.car.app.model.ActionStrip? getMapActionStrip();
+    method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(4) public androidx.car.app.navigation.model.PanModeDelegate? getPanModeDelegate();
     method public androidx.car.app.model.CarText? getTitle();
     method public boolean isLoading();
   }
@@ -1211,6 +1213,8 @@
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action);
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList);
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setLoading(boolean);
+    method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(4) public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setMapActionStrip(androidx.car.app.model.ActionStrip);
+    method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(4) public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setPanModeListener(androidx.car.app.navigation.model.PanModeListener);
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setTitle(CharSequence);
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setTitle(androidx.car.app.model.CarText);
   }
@@ -1219,7 +1223,9 @@
     method public androidx.car.app.model.ActionStrip? getActionStrip();
     method public androidx.car.app.model.Action? getHeaderAction();
     method public androidx.car.app.model.ItemList? getItemList();
+    method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(4) public androidx.car.app.model.ActionStrip? getMapActionStrip();
     method public androidx.car.app.model.Action? getNavigateAction();
+    method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(4) public androidx.car.app.navigation.model.PanModeDelegate? getPanModeDelegate();
     method public androidx.car.app.model.CarText? getTitle();
     method public boolean isLoading();
   }
@@ -1231,7 +1237,9 @@
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setLoading(boolean);
+    method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(4) public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setMapActionStrip(androidx.car.app.model.ActionStrip);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setNavigateAction(androidx.car.app.model.Action);
+    method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(4) public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setPanModeListener(androidx.car.app.navigation.model.PanModeListener);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setTitle(CharSequence);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setTitle(androidx.car.app.model.CarText);
   }
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index e43b6f474..d688300 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -1200,6 +1200,8 @@
     method public androidx.car.app.model.ActionStrip? getActionStrip();
     method public androidx.car.app.model.Action? getHeaderAction();
     method public androidx.car.app.model.ItemList? getItemList();
+    method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(4) public androidx.car.app.model.ActionStrip? getMapActionStrip();
+    method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(4) public androidx.car.app.navigation.model.PanModeDelegate? getPanModeDelegate();
     method public androidx.car.app.model.CarText? getTitle();
     method public boolean isLoading();
   }
@@ -1211,6 +1213,8 @@
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action);
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList);
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setLoading(boolean);
+    method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(4) public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setMapActionStrip(androidx.car.app.model.ActionStrip);
+    method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(4) public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setPanModeListener(androidx.car.app.navigation.model.PanModeListener);
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setTitle(CharSequence);
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setTitle(androidx.car.app.model.CarText);
   }
@@ -1219,7 +1223,9 @@
     method public androidx.car.app.model.ActionStrip? getActionStrip();
     method public androidx.car.app.model.Action? getHeaderAction();
     method public androidx.car.app.model.ItemList? getItemList();
+    method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(4) public androidx.car.app.model.ActionStrip? getMapActionStrip();
     method public androidx.car.app.model.Action? getNavigateAction();
+    method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(4) public androidx.car.app.navigation.model.PanModeDelegate? getPanModeDelegate();
     method public androidx.car.app.model.CarText? getTitle();
     method public boolean isLoading();
   }
@@ -1231,7 +1237,9 @@
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setLoading(boolean);
+    method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(4) public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setMapActionStrip(androidx.car.app.model.ActionStrip);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setNavigateAction(androidx.car.app.model.Action);
+    method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(4) public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setPanModeListener(androidx.car.app.navigation.model.PanModeListener);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setTitle(CharSequence);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setTitle(androidx.car.app.model.CarText);
   }
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 b2ac358..0675d63 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
@@ -93,12 +93,12 @@
                     .build();
 
     /**
-     * Constraints for map action buttons in navigation templates.
+     * Constraints for map action buttons.
      *
      * <p>Only buttons with icons are allowed.
      */
     @NonNull
-    public static final ActionsConstraints ACTIONS_CONSTRAINTS_NAVIGATION_MAP =
+    public static final ActionsConstraints ACTIONS_CONSTRAINTS_MAP =
             new ActionsConstraints.Builder(ACTIONS_CONSTRAINTS_CONSERVATIVE)
                     .setMaxActions(4)
                     .build();
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java
index a95b950..d157202 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java
@@ -16,8 +16,8 @@
 
 package androidx.car.app.navigation.model;
 
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_MAP;
 import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_NAVIGATION;
-import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_NAVIGATION_MAP;
 import static androidx.car.app.model.constraints.CarColorConstraints.UNCONSTRAINED;
 
 import static java.util.Objects.requireNonNull;
@@ -283,7 +283,6 @@
         @Nullable
         PanModeDelegate mPanModeDelegate;
 
-
         /**
          * Sets the navigation information to display on the template.
          *
@@ -375,7 +374,7 @@
         @RequiresCarApi(2)
         @NonNull
         public Builder setMapActionStrip(@NonNull ActionStrip actionStrip) {
-            ACTIONS_CONSTRAINTS_NAVIGATION_MAP.validateOrThrow(
+            ACTIONS_CONSTRAINTS_MAP.validateOrThrow(
                     requireNonNull(actionStrip).getActions());
             mMapActionStrip = actionStrip;
             return this;
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java
index 5e15393..17c4e8d 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java
@@ -17,17 +17,22 @@
 package androidx.car.app.navigation.model;
 
 import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_HEADER;
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_MAP;
 import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE;
 import static androidx.car.app.model.constraints.RowListConstraints.ROW_LIST_CONSTRAINTS_SIMPLE;
 
 import static java.util.Objects.requireNonNull;
 
+import android.annotation.SuppressLint;
+
 import androidx.annotation.Keep;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.car.app.Screen;
 import androidx.car.app.SurfaceCallback;
 import androidx.car.app.annotations.CarProtocol;
+import androidx.car.app.annotations.ExperimentalCarApi;
+import androidx.car.app.annotations.RequiresCarApi;
 import androidx.car.app.model.Action;
 import androidx.car.app.model.ActionStrip;
 import androidx.car.app.model.CarText;
@@ -84,6 +89,12 @@
     @Keep
     @Nullable
     private final ActionStrip mActionStrip;
+    @Keep
+    @Nullable
+    private final ActionStrip mMapActionStrip;
+    @Keep
+    @Nullable
+    private final PanModeDelegate mPanModeDelegate;
 
     /**
      * Returns the title of the template or {@code null} if not set.
@@ -117,6 +128,29 @@
     }
 
     /**
+     * Returns the map {@link ActionStrip} for this template or {@code null} if not set.
+     *
+     * @see Builder#setMapActionStrip(ActionStrip)
+     */
+    @ExperimentalCarApi
+    @RequiresCarApi(4)
+    @Nullable
+    public ActionStrip getMapActionStrip() {
+        return mMapActionStrip;
+    }
+
+    /**
+     * Returns the {@link PanModeDelegate} that should be called when the user interacts with
+     * pan mode on this template, or {@code null} if a {@link PanModeListener} was not set.
+     */
+    @ExperimentalCarApi
+    @RequiresCarApi(4)
+    @Nullable
+    public PanModeDelegate getPanModeDelegate() {
+        return mPanModeDelegate;
+    }
+
+    /**
      * Returns whether the template is loading.
      *
      * @see Builder#setLoading(boolean)
@@ -144,7 +178,8 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(mTitle, mIsLoading, mItemList, mHeaderAction, mActionStrip);
+        return Objects.hash(mTitle, mIsLoading, mItemList, mHeaderAction, mActionStrip,
+                mMapActionStrip, mPanModeDelegate == null);
     }
 
     @Override
@@ -161,7 +196,9 @@
                 && Objects.equals(mTitle, otherTemplate.mTitle)
                 && Objects.equals(mItemList, otherTemplate.mItemList)
                 && Objects.equals(mHeaderAction, otherTemplate.mHeaderAction)
-                && Objects.equals(mActionStrip, otherTemplate.mActionStrip);
+                && Objects.equals(mActionStrip, otherTemplate.mActionStrip)
+                && Objects.equals(mMapActionStrip, otherTemplate.mMapActionStrip)
+                && Objects.equals(mPanModeDelegate == null, otherTemplate.mPanModeDelegate == null);
     }
 
     PlaceListNavigationTemplate(Builder builder) {
@@ -170,6 +207,8 @@
         mItemList = builder.mItemList;
         mHeaderAction = builder.mHeaderAction;
         mActionStrip = builder.mActionStrip;
+        mMapActionStrip = builder.mMapActionStrip;
+        mPanModeDelegate = builder.mPanModeDelegate;
     }
 
     /** Constructs an empty instance, used by serialization code. */
@@ -179,6 +218,8 @@
         mItemList = null;
         mHeaderAction = null;
         mActionStrip = null;
+        mMapActionStrip = null;
+        mPanModeDelegate = null;
     }
 
     /** A builder of {@link PlaceListNavigationTemplate}. */
@@ -192,6 +233,10 @@
         Action mHeaderAction;
         @Nullable
         ActionStrip mActionStrip;
+        @Nullable
+        ActionStrip mMapActionStrip;
+        @Nullable
+        PanModeDelegate mPanModeDelegate;
 
         /**
          * Sets the title of the template.
@@ -328,6 +373,57 @@
         }
 
         /**
+         * Sets an {@link ActionStrip} with a list of map-control related actions for this
+         * template, such as pan or zoom.
+         *
+         * <p>The host will draw the buttons in an area that is associated with map controls.
+         *
+         * <p>If the app does not include the {@link Action#PAN} button in this
+         * {@link ActionStrip}, the app will not receive the user input for panning gestures from
+         * {@link SurfaceCallback} methods, and the host will exit any previously activated pan
+         * mode.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template allows up to 4 {@link Action}s in its map {@link ActionStrip}. Only
+         * {@link Action}s with icons set via {@link Action.Builder#setIcon} are allowed.
+         *
+         * @throws IllegalArgumentException if {@code actionStrip} does not meet the template's
+         *                                  requirements
+         * @throws NullPointerException     if {@code actionStrip} is {@code null}
+         */
+        @ExperimentalCarApi
+        @RequiresCarApi(4)
+        @NonNull
+        public Builder setMapActionStrip(@NonNull ActionStrip actionStrip) {
+            ACTIONS_CONSTRAINTS_MAP.validateOrThrow(
+                    requireNonNull(actionStrip).getActions());
+            mMapActionStrip = actionStrip;
+            return this;
+        }
+
+        /**
+         * Sets a {@link PanModeListener} that notifies when the user enters and exits
+         * the pan mode.
+         *
+         * <p>If the app does not include the {@link Action#PAN} button in the map
+         * {@link ActionStrip}, the app will not receive the user input for panning gestures from
+         * {@link SurfaceCallback} methods, and the host will exit any previously activated pan
+         * mode.
+         *
+         * @throws NullPointerException if {@code panModeListener} is {@code null}
+         */
+        @SuppressLint({"MissingGetterMatchingBuilder", "ExecutorRegistration"})
+        @ExperimentalCarApi
+        @RequiresCarApi(4)
+        @NonNull
+        public Builder setPanModeListener(@NonNull PanModeListener panModeListener) {
+            requireNonNull(panModeListener);
+            mPanModeDelegate = PanModeDelegateImpl.create(panModeListener);
+            return this;
+        }
+
+        /**
          * Constructs the template defined by this builder.
          *
          * <h4>Requirements</h4>
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java
index 47d711d..5077063 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java
@@ -17,17 +17,22 @@
 package androidx.car.app.navigation.model;
 
 import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_HEADER;
+import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_MAP;
 import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE;
 import static androidx.car.app.model.constraints.RowListConstraints.ROW_LIST_CONSTRAINTS_ROUTE_PREVIEW;
 
 import static java.util.Objects.requireNonNull;
 
+import android.annotation.SuppressLint;
+
 import androidx.annotation.Keep;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.car.app.Screen;
 import androidx.car.app.SurfaceCallback;
 import androidx.car.app.annotations.CarProtocol;
+import androidx.car.app.annotations.ExperimentalCarApi;
+import androidx.car.app.annotations.RequiresCarApi;
 import androidx.car.app.model.Action;
 import androidx.car.app.model.ActionStrip;
 import androidx.car.app.model.CarText;
@@ -97,6 +102,12 @@
     @Keep
     @Nullable
     private final ActionStrip mActionStrip;
+    @Keep
+    @Nullable
+    private final ActionStrip mMapActionStrip;
+    @Keep
+    @Nullable
+    private final PanModeDelegate mPanModeDelegate;
 
     /**
      * Returns the title of the template or {@code null} if not set.
@@ -130,6 +141,29 @@
     }
 
     /**
+     * Returns the map {@link ActionStrip} for this template or {@code null} if not set.
+     *
+     * @see Builder#setMapActionStrip(ActionStrip)
+     */
+    @ExperimentalCarApi
+    @RequiresCarApi(4)
+    @Nullable
+    public ActionStrip getMapActionStrip() {
+        return mMapActionStrip;
+    }
+
+    /**
+     * Returns the {@link PanModeDelegate} that should be called when the user interacts with
+     * pan mode on this template, or {@code null} if a {@link PanModeListener} was not set.
+     */
+    @ExperimentalCarApi
+    @RequiresCarApi(4)
+    @Nullable
+    public PanModeDelegate getPanModeDelegate() {
+        return mPanModeDelegate;
+    }
+
+    /**
      * Returns whether the template is loading.
      *
      * @see Builder#setLoading(boolean)
@@ -169,7 +203,7 @@
     @Override
     public int hashCode() {
         return Objects.hash(mTitle, mIsLoading, mNavigateAction, mItemList, mHeaderAction,
-                mActionStrip);
+                mActionStrip, mMapActionStrip, mPanModeDelegate == null);
     }
 
     @Override
@@ -187,7 +221,9 @@
                 && Objects.equals(mNavigateAction, otherTemplate.mNavigateAction)
                 && Objects.equals(mItemList, otherTemplate.mItemList)
                 && Objects.equals(mHeaderAction, otherTemplate.mHeaderAction)
-                && Objects.equals(mActionStrip, otherTemplate.mActionStrip);
+                && Objects.equals(mActionStrip, otherTemplate.mActionStrip)
+                && Objects.equals(mMapActionStrip, otherTemplate.mMapActionStrip)
+                && Objects.equals(mPanModeDelegate == null, otherTemplate.mPanModeDelegate == null);
     }
 
     RoutePreviewNavigationTemplate(Builder builder) {
@@ -197,6 +233,8 @@
         mItemList = builder.mItemList;
         mHeaderAction = builder.mHeaderAction;
         mActionStrip = builder.mActionStrip;
+        mMapActionStrip = builder.mMapActionStrip;
+        mPanModeDelegate = builder.mPanModeDelegate;
     }
 
     /** Constructs an empty instance, used by serialization code. */
@@ -207,6 +245,8 @@
         mItemList = null;
         mHeaderAction = null;
         mActionStrip = null;
+        mMapActionStrip = null;
+        mPanModeDelegate = null;
     }
 
     /** A builder of {@link RoutePreviewNavigationTemplate}. */
@@ -222,6 +262,10 @@
         Action mHeaderAction;
         @Nullable
         ActionStrip mActionStrip;
+        @Nullable
+        ActionStrip mMapActionStrip;
+        @Nullable
+        PanModeDelegate mPanModeDelegate;
 
         /**
          * Sets the title of the template.
@@ -379,6 +423,57 @@
         }
 
         /**
+         * Sets an {@link ActionStrip} with a list of map-control related actions for this
+         * template, such as pan or zoom.
+         *
+         * <p>The host will draw the buttons in an area that is associated with map controls.
+         *
+         * <p>If the app does not include the {@link Action#PAN} button in this
+         * {@link ActionStrip}, the app will not receive the user input for panning gestures from
+         * {@link SurfaceCallback} methods, and the host will exit any previously activated pan
+         * mode.
+         *
+         * <h4>Requirements</h4>
+         *
+         * This template allows up to 4 {@link Action}s in its map {@link ActionStrip}. Only
+         * {@link Action}s with icons set via {@link Action.Builder#setIcon} are allowed.
+         *
+         * @throws IllegalArgumentException if {@code actionStrip} does not meet the template's
+         *                                  requirements
+         * @throws NullPointerException     if {@code actionStrip} is {@code null}
+         */
+        @ExperimentalCarApi
+        @RequiresCarApi(4)
+        @NonNull
+        public Builder setMapActionStrip(@NonNull ActionStrip actionStrip) {
+            ACTIONS_CONSTRAINTS_MAP.validateOrThrow(
+                    requireNonNull(actionStrip).getActions());
+            mMapActionStrip = actionStrip;
+            return this;
+        }
+
+        /**
+         * Sets a {@link PanModeListener} that notifies when the user enters and exits
+         * the pan mode.
+         *
+         * <p>If the app does not include the {@link Action#PAN} button in the map
+         * {@link ActionStrip}, the app will not receive the user input for panning gestures from
+         * {@link SurfaceCallback} methods, and the host will exit any previously activated pan
+         * mode.
+         *
+         * @throws NullPointerException if {@code panModeListener} is {@code null}
+         */
+        @SuppressLint({"MissingGetterMatchingBuilder", "ExecutorRegistration"})
+        @ExperimentalCarApi
+        @RequiresCarApi(4)
+        @NonNull
+        public Builder setPanModeListener(@NonNull PanModeListener panModeListener) {
+            requireNonNull(panModeListener);
+            mPanModeDelegate = PanModeDelegateImpl.create(panModeListener);
+            return this;
+        }
+
+        /**
          * Constructs the template defined by this builder.
          *
          * <h4>Requirements</h4>
diff --git a/car/app/app/src/test/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java
index 82764d7..fd43af8 100644
--- a/car/app/app/src/test/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java
@@ -52,6 +52,19 @@
     private final DistanceSpan mDistanceSpan =
             DistanceSpan.create(
                     Distance.create(/* displayDistance= */ 1, Distance.UNIT_KILOMETERS_P1));
+    private final ActionStrip mActionStrip =
+            new ActionStrip.Builder().addAction(TestUtils.createAction("test", null)).build();
+    private final ActionStrip mMapActionStrip =
+            new ActionStrip.Builder().addAction(
+                    TestUtils.createAction(null, TestUtils.getTestCarIcon(
+                            ApplicationProvider.getApplicationContext(),
+                            "ic_test_1"))).build();
+
+    @Test
+    public void textButtonInMapActionStrip_throws() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new PlaceListNavigationTemplate.Builder().setMapActionStrip(mActionStrip));
+    }
 
     @Test
     public void createInstance_emptyList_notLoading_Throws() {
@@ -205,10 +218,12 @@
                         .setItemList(itemList)
                         .setTitle(title)
                         .setActionStrip(actionStrip)
+                        .setMapActionStrip(mMapActionStrip)
                         .build();
         assertThat(template.getItemList()).isEqualTo(itemList);
         assertThat(template.getActionStrip()).isEqualTo(actionStrip);
         assertThat(template.getTitle().toString()).isEqualTo(title);
+        assertThat(template.getMapActionStrip()).isEqualTo(mMapActionStrip);
     }
 
     @Test
@@ -324,6 +339,8 @@
                                 TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
                         .setHeaderAction(Action.BACK)
                         .setActionStrip(new ActionStrip.Builder().addAction(Action.BACK).build())
+                        .setMapActionStrip(mMapActionStrip)
+                        .setPanModeListener((panModechanged) -> {})
                         .setTitle("title")
                         .build();
 
@@ -335,6 +352,8 @@
                                 .setHeaderAction(Action.BACK)
                                 .setActionStrip(
                                         new ActionStrip.Builder().addAction(Action.BACK).build())
+                                .setMapActionStrip(mMapActionStrip)
+                                .setPanModeListener((panModechanged) -> {})
                                 .setTitle("title")
                                 .build());
     }
@@ -398,6 +417,58 @@
     }
 
     @Test
+    public void notEquals_differentMapActionStrip() {
+        PlaceListNavigationTemplate template = new PlaceListNavigationTemplate.Builder()
+                .setTitle("Title")
+                .setItemList(
+                        TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+                .setActionStrip(
+                        mActionStrip)
+                .setMapActionStrip(mMapActionStrip)
+                .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        new PlaceListNavigationTemplate.Builder()
+                                .setTitle("Title")
+                                .setItemList(
+                                        TestUtils.createItemListWithDistanceSpan(6, false,
+                                                mDistanceSpan))
+                                .setActionStrip(mActionStrip)
+                                .setMapActionStrip(new ActionStrip.Builder().addAction(
+                                        TestUtils.createAction(null, TestUtils.getTestCarIcon(
+                                                ApplicationProvider.getApplicationContext(),
+                                                "ic_test_2"))).build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_panModeListenerChange() {
+        PlaceListNavigationTemplate template = new PlaceListNavigationTemplate.Builder()
+                .setTitle("Title")
+                .setItemList(
+                        TestUtils.createItemListWithDistanceSpan(6, false, mDistanceSpan))
+                .setActionStrip(
+                        mActionStrip)
+                .setMapActionStrip(mMapActionStrip)
+                .setPanModeListener((panModechanged) -> {
+                })
+                .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        new PlaceListNavigationTemplate.Builder()
+                                .setTitle("Title")
+                                .setItemList(
+                                        TestUtils.createItemListWithDistanceSpan(6, false,
+                                                mDistanceSpan))
+                                .setActionStrip(
+                                        mActionStrip)
+                                .setMapActionStrip(mMapActionStrip)
+                                .build());
+    }
+
+    @Test
     public void notEquals_differentTitle() {
         PlaceListNavigationTemplate template =
                 new PlaceListNavigationTemplate.Builder()
diff --git a/car/app/app/src/test/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java
index 302eec5..5705004 100644
--- a/car/app/app/src/test/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java
@@ -51,6 +51,13 @@
             DistanceSpan.create(
                     Distance.create(/* displayDistance= */ 1, Distance.UNIT_KILOMETERS_P1));
     private final Context mContext = ApplicationProvider.getApplicationContext();
+    private final ActionStrip mActionStrip =
+            new ActionStrip.Builder().addAction(TestUtils.createAction("test", null)).build();
+    private final ActionStrip mMapActionStrip =
+            new ActionStrip.Builder().addAction(
+                    TestUtils.createAction(null, TestUtils.getTestCarIcon(
+                            ApplicationProvider.getApplicationContext(),
+                            "ic_test_1"))).build();
 
     @Test
     public void createInstance_emptyList_notLoading_Throws() {
@@ -135,6 +142,12 @@
     }
 
     @Test
+    public void textButtonInMapActionStrip_throws() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new RoutePreviewNavigationTemplate.Builder().setMapActionStrip(mActionStrip));
+    }
+
+    @Test
     public void createInstance() {
         ItemList itemList = TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE);
         String title = "title";
@@ -145,6 +158,7 @@
                         .setNavigateAction(
                                 new Action.Builder().setTitle("Navigate").setOnClickListener(() -> {
                                 }).build())
+                        .setMapActionStrip(mMapActionStrip)
                         .build();
         assertThat(template.getItemList()).isEqualTo(itemList);
         assertThat(template.getTitle().toString()).isEqualTo(title);
@@ -364,6 +378,9 @@
                         .setNavigateAction(
                                 new Action.Builder().setTitle("drive").setOnClickListener(() -> {
                                 }).build())
+                        .setMapActionStrip(mMapActionStrip)
+                        .setPanModeListener((panModechanged) -> {
+                        })
                         .build();
 
         assertThat(template)
@@ -379,6 +396,9 @@
                                         new Action.Builder().setTitle("drive").setOnClickListener(
                                                 () -> {
                                                 }).build())
+                                .setMapActionStrip(mMapActionStrip)
+                                .setPanModeListener((panModechanged) -> {
+                                })
                                 .build());
     }
 
@@ -459,6 +479,70 @@
     }
 
     @Test
+    public void notEquals_differentMapActionStrip() {
+        RoutePreviewNavigationTemplate template =
+                new RoutePreviewNavigationTemplate.Builder()
+                        .setTitle("Title")
+                        .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                        .setActionStrip(new ActionStrip.Builder().addAction(Action.BACK).build())
+                        .setNavigateAction(
+                                new Action.Builder().setTitle("drive").setOnClickListener(() -> {
+                                }).build())
+                        .setMapActionStrip(mMapActionStrip)
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        new RoutePreviewNavigationTemplate.Builder()
+                                .setTitle("Title")
+                                .setItemList(
+                                        TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                                .setActionStrip(
+                                        new ActionStrip.Builder().addAction(
+                                                Action.APP_ICON).build())
+                                .setNavigateAction(
+                                        new Action.Builder().setTitle("drive").setOnClickListener(
+                                                () -> {
+                                                }).build())
+                                .setMapActionStrip(new ActionStrip.Builder().addAction(
+                                        TestUtils.createAction(null, TestUtils.getTestCarIcon(
+                                                ApplicationProvider.getApplicationContext(),
+                                                "ic_test_2"))).build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_panModeListenerChange() {
+        RoutePreviewNavigationTemplate template =
+                new RoutePreviewNavigationTemplate.Builder()
+                        .setTitle("Title")
+                        .setItemList(TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                        .setActionStrip(new ActionStrip.Builder().addAction(Action.BACK).build())
+                        .setNavigateAction(
+                                new Action.Builder().setTitle("drive").setOnClickListener(() -> {
+                                }).build())
+                        .setMapActionStrip(mMapActionStrip)
+                        .setPanModeListener((panModechanged) -> {
+                        })
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        new RoutePreviewNavigationTemplate.Builder()
+                                .setTitle("Title")
+                                .setItemList(
+                                        TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
+                                .setActionStrip(
+                                        new ActionStrip.Builder().addAction(Action.BACK).build())
+                                .setNavigateAction(
+                                        new Action.Builder().setTitle("drive").setOnClickListener(
+                                                () -> {
+                                                }).build())
+                                .setMapActionStrip(mMapActionStrip)
+                                .build());
+    }
+
+    @Test
     public void notEquals_differentTitle() {
         SpannableString title = new SpannableString("Title");
         title.setSpan(DISTANCE, 0, 1, 0);