DO NOT MERGE Focus the topmost heads-up notification
Bug: 172267951
Test: manual via kitchen sink
Change-Id: Ib73c2912b06bc9e62d5a64abbecf625f4c8c78a9
diff --git a/res/layout/basic_headsup_notification_template.xml b/res/layout/basic_headsup_notification_template.xml
index f2c2fa5..0f82a23 100644
--- a/res/layout/basic_headsup_notification_template.xml
+++ b/res/layout/basic_headsup_notification_template.xml
@@ -14,10 +14,11 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
+<com.android.car.ui.FocusArea
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:id="@+id/card_view"
@@ -76,4 +77,4 @@
</RelativeLayout>
</androidx.cardview.widget.CardView>
-</FrameLayout>
+</com.android.car.ui.FocusArea>
diff --git a/res/layout/call_headsup_notification_template.xml b/res/layout/call_headsup_notification_template.xml
index c9ec748..d15f13d 100644
--- a/res/layout/call_headsup_notification_template.xml
+++ b/res/layout/call_headsup_notification_template.xml
@@ -15,7 +15,7 @@
limitations under the License.
-->
-<FrameLayout
+<com.android.car.ui.FocusArea
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
@@ -78,4 +78,4 @@
</FrameLayout>
</RelativeLayout>
</androidx.cardview.widget.CardView>
-</FrameLayout>
+</com.android.car.ui.FocusArea>
diff --git a/res/layout/car_emergency_headsup_notification_template.xml b/res/layout/car_emergency_headsup_notification_template.xml
index 725f110..a2b71e1 100644
--- a/res/layout/car_emergency_headsup_notification_template.xml
+++ b/res/layout/car_emergency_headsup_notification_template.xml
@@ -15,7 +15,7 @@
limitations under the License.
-->
-<FrameLayout
+<com.android.car.ui.FocusArea
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
@@ -86,4 +86,4 @@
</RelativeLayout>
</FrameLayout>
</androidx.cardview.widget.CardView>
-</FrameLayout>
+</com.android.car.ui.FocusArea>
diff --git a/res/layout/car_information_headsup_notification_template.xml b/res/layout/car_information_headsup_notification_template.xml
index d588864..0411c51 100644
--- a/res/layout/car_information_headsup_notification_template.xml
+++ b/res/layout/car_information_headsup_notification_template.xml
@@ -15,7 +15,7 @@
limitations under the License.
-->
-<FrameLayout
+<com.android.car.ui.FocusArea
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
@@ -88,4 +88,4 @@
</RelativeLayout>
</FrameLayout>
</androidx.cardview.widget.CardView>
-</FrameLayout>
+</com.android.car.ui.FocusArea>
diff --git a/res/layout/car_warning_headsup_notification_template.xml b/res/layout/car_warning_headsup_notification_template.xml
index d588864..0411c51 100644
--- a/res/layout/car_warning_headsup_notification_template.xml
+++ b/res/layout/car_warning_headsup_notification_template.xml
@@ -15,7 +15,7 @@
limitations under the License.
-->
-<FrameLayout
+<com.android.car.ui.FocusArea
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
@@ -88,4 +88,4 @@
</RelativeLayout>
</FrameLayout>
</androidx.cardview.widget.CardView>
-</FrameLayout>
+</com.android.car.ui.FocusArea>
diff --git a/res/layout/headsup_container.xml b/res/layout/headsup_container.xml
index db649b7..9eec539 100644
--- a/res/layout/headsup_container.xml
+++ b/res/layout/headsup_container.xml
@@ -45,14 +45,13 @@
app:layout_constraintBottom_toBottomOf="@+id/gradient_edge"
app:layout_constraintTop_toTopOf="parent"/>
- <FrameLayout
+ <com.android.car.notification.headsup.HeadsUpContainerView
android:id="@+id/headsup_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/headsup_notification_top_margin"
app:layout_constraintEnd_toStartOf="parent"
app:layout_constraintStart_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- />
+ app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/res/layout/inbox_headsup_notification_template.xml b/res/layout/inbox_headsup_notification_template.xml
index 6f58bef..ae217d9 100644
--- a/res/layout/inbox_headsup_notification_template.xml
+++ b/res/layout/inbox_headsup_notification_template.xml
@@ -15,7 +15,7 @@
limitations under the License.
-->
-<FrameLayout
+<com.android.car.ui.FocusArea
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
@@ -78,4 +78,4 @@
</FrameLayout>
</RelativeLayout>
</androidx.cardview.widget.CardView>
-</FrameLayout>
+</com.android.car.ui.FocusArea>
diff --git a/res/layout/message_headsup_notification_template.xml b/res/layout/message_headsup_notification_template.xml
index abe2f6f..ea49d0e 100644
--- a/res/layout/message_headsup_notification_template.xml
+++ b/res/layout/message_headsup_notification_template.xml
@@ -15,7 +15,7 @@
limitations under the License.
-->
-<FrameLayout
+<com.android.car.ui.FocusArea
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
@@ -88,4 +88,4 @@
</FrameLayout>
</RelativeLayout>
</androidx.cardview.widget.CardView>
-</FrameLayout>
+</com.android.car.ui.FocusArea>
diff --git a/res/layout/navigation_headsup_notification_template.xml b/res/layout/navigation_headsup_notification_template.xml
index 4268905..b4b83e8 100644
--- a/res/layout/navigation_headsup_notification_template.xml
+++ b/res/layout/navigation_headsup_notification_template.xml
@@ -14,10 +14,11 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
+<com.android.car.ui.FocusArea
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:id="@+id/card_view"
@@ -75,4 +76,4 @@
</FrameLayout>
</RelativeLayout>
</androidx.cardview.widget.CardView>
-</FrameLayout>
+</com.android.car.ui.FocusArea>
diff --git a/src/com/android/car/notification/headsup/HeadsUpContainerView.java b/src/com/android/car/notification/headsup/HeadsUpContainerView.java
new file mode 100644
index 0000000..44fbec3
--- /dev/null
+++ b/src/com/android/car/notification/headsup/HeadsUpContainerView.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.notification.headsup;
+
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS;
+
+import static com.android.car.ui.utils.RotaryConstants.ROTARY_FOCUS_DELEGATING_CONTAINER;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.car.notification.R;
+import com.android.car.ui.FocusArea;
+
+/**
+ * Container that is used to present heads-up notifications. It is responsible for delegating the
+ * focus to the topmost notification and ensuring that new HUNs gains focus automatically when
+ * one of the existing HUNs already has focus.
+ */
+public class HeadsUpContainerView extends FrameLayout {
+
+ private Handler mHandler;
+ private int mAnimationDuration;
+
+ public HeadsUpContainerView(@NonNull Context context) {
+ super(context);
+ init();
+ }
+
+ public HeadsUpContainerView(@NonNull Context context,
+ @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public HeadsUpContainerView(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ public HeadsUpContainerView(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init();
+ }
+
+ private void init() {
+ mHandler = new Handler(Looper.getMainLooper());
+ mAnimationDuration = getResources().getInteger(R.integer.headsup_total_enter_duration_ms);
+
+ // This tag is required to make this container receive the focus request in order to
+ // delegate focus to its children, even though the container itself isn't focusable.
+ setContentDescription(ROTARY_FOCUS_DELEGATING_CONTAINER);
+ setClickable(false);
+ }
+
+ @Override
+ protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
+ if (isInTouchMode()) {
+ return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
+ }
+ return focusTopmostChild();
+ }
+
+ @Override
+ public void addView(View child) {
+ super.addView(child);
+
+ if (!isInTouchMode() && getFocusedChild() != null) {
+ // Request focus for the topmost child if one of the children is already focused.
+ // Wait for the duration of the heads-up animation for a smoother UI experience.
+ mHandler.postDelayed(() -> focusTopmostChild(), mAnimationDuration);
+ }
+ }
+
+ private boolean focusTopmostChild() {
+ int childCount = getChildCount();
+ if (childCount <= 0) {
+ return false;
+ }
+
+ View topMostChild = getChildAt(childCount - 1);
+ if (!(topMostChild instanceof FocusArea)) {
+ return false;
+ }
+
+ return topMostChild.performAccessibilityAction(ACTION_FOCUS, /* arguments= */ null);
+ }
+}