Merge 6578e382b4739b4bacfad3436b32907fa57e0d02 on remote branch
Change-Id: I700cd54da930e089597129a523638438edf29d2f
diff --git a/car-apps-common/res/drawable/control_bar_button_background.xml b/car-apps-common/res/drawable/control_bar_button_background.xml
index 2c6e1f2..09bd38a 100644
--- a/car-apps-common/res/drawable/control_bar_button_background.xml
+++ b/car-apps-common/res/drawable/control_bar_button_background.xml
@@ -17,6 +17,15 @@
~
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_focused="true" android:state_pressed="true">
+ <shape android:shape="oval">
+ <solid android:color="@color/car_ui_rotary_focus_pressed_fill_color"/>
+ <stroke android:width="@dimen/car_ui_rotary_focus_pressed_stroke_width"
+ android:color="@color/car_ui_rotary_focus_pressed_stroke_color" />
+ <size android:width="@dimen/control_bar_button_background_radius"
+ android:height="@dimen/control_bar_button_background_radius"/>
+ </shape>
+ </item>
<item android:state_focused="true">
<shape android:shape="oval">
<solid android:color="@color/car_ui_rotary_focus_fill_color"/>
diff --git a/car-apps-common/res/drawable/hero_button_background.xml b/car-apps-common/res/drawable/hero_button_background.xml
index e036f4a..e5aeec5 100644
--- a/car-apps-common/res/drawable/hero_button_background.xml
+++ b/car-apps-common/res/drawable/hero_button_background.xml
@@ -14,6 +14,14 @@
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_focused="true" android:state_pressed="true">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/car_ui_rotary_focus_pressed_fill_color"/>
+ <stroke android:width="@dimen/car_ui_rotary_focus_pressed_stroke_width"
+ android:color="@color/car_ui_rotary_focus_pressed_stroke_color" />
+ <corners android:radius="@dimen/hero_button_corner_radius"/>
+ </shape>
+ </item>
<item android:state_focused="true">
<shape android:shape="rectangle">
<solid android:color="@color/car_ui_rotary_focus_fill_color"/>
diff --git a/car-apps-common/src/com/android/car/apps/common/ControlBar.java b/car-apps-common/src/com/android/car/apps/common/ControlBar.java
index 88d4dae..4181867 100644
--- a/car-apps-common/src/com/android/car/apps/common/ControlBar.java
+++ b/car-apps-common/src/com/android/car/apps/common/ControlBar.java
@@ -16,6 +16,8 @@
package com.android.car.apps.common;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
@@ -29,6 +31,7 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.ViewParent;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.LinearLayout;
@@ -90,6 +93,10 @@
private boolean mExpandEnabled;
// Callback for the expand/collapse button
private ExpandCollapseCallback mExpandCollapseCallback;
+ // The root of the transition animation.
+ private ViewGroup mTransitionRoot;
+ // Whether this control bar has focus.
+ private boolean mHasFocus;
// Default number of columns, if unspecified
private static final int DEFAULT_COLUMNS = 3;
@@ -160,6 +167,15 @@
mDefaultExpandCollapseView.setContentDescription(context.getString(
R.string.control_bar_expand_collapse_button));
mDefaultExpandCollapseView.setOnClickListener(v -> onExpandCollapse());
+
+ // Collapse the control bar when it is expanded and loses focus.
+ getViewTreeObserver().addOnGlobalFocusChangeListener((oldFocus, newFocus) -> {
+ boolean hasFocus = hasFocus();
+ if (mHasFocus && !hasFocus && mIsExpanded) {
+ onExpandCollapse();
+ }
+ mHasFocus = hasFocus;
+ });
}
private int getSlotIndex(@SlotPosition int slotPosition) {
@@ -310,12 +326,34 @@
.addTransition(new Fade())
.setDuration(animationDuration)
.setInterpolator(new FastOutSlowInInterpolator());
- TransitionManager.beginDelayedTransition(this, set);
+ maybeInitTransitionRoot();
+ TransitionManager.beginDelayedTransition(mTransitionRoot, set);
for (int i = 0; i < mNumExtraRowsInUse; i++) {
mRowsContainer.getChildAt(i).setVisibility(mIsExpanded ? View.VISIBLE : View.GONE);
}
}
+ private void maybeInitTransitionRoot() {
+ if (mTransitionRoot != null) {
+ return;
+ }
+ // During the control bar expanding/collapsing animation, the height of the control bar
+ // changes gradually. If the height of its ancestor is WRAP_CONTENT, the height of its
+ // ancestor will not change during the animation, causing janky animation. To fix it the
+ // animation should be played on the highest ancestor that wraps the control bar vertically.
+ mTransitionRoot = this;
+ ViewParent viewParent = getParent();
+ while (viewParent != null && viewParent instanceof ViewGroup) {
+ ViewGroup parent = (ViewGroup) viewParent;
+ if (parent.getLayoutParams().height == WRAP_CONTENT) {
+ mTransitionRoot = parent;
+ viewParent = parent.getParent();
+ } else {
+ break;
+ }
+ }
+ }
+
/**
* Returns the view assigned to the given row and column, after layout.
*
diff --git a/car-assist-client-lib/res/values-iw/strings.xml b/car-assist-client-lib/res/values-iw/strings.xml
index 510432d..dc96640 100644
--- a/car-assist-client-lib/res/values-iw/strings.xml
+++ b/car-assist-client-lib/res/values-iw/strings.xml
@@ -16,6 +16,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="assist_action_failed_toast" msgid="3250146468076483714">"לא ניתן לבקש מה-Assistant לבצע פעולה!"</string>
+ <string name="assist_action_failed_toast" msgid="3250146468076483714">"לא ניתן לבקש מ-Assistant לבצע פעולה!"</string>
<string name="says" msgid="8575666015622916107">"רוצה להודיע כי"</string>
</resources>
diff --git a/car-broadcastradio-support/res/values-bs/strings.xml b/car-broadcastradio-support/res/values-bs/strings.xml
index 42ff388..e29e90e 100644
--- a/car-broadcastradio-support/res/values-bs/strings.xml
+++ b/car-broadcastradio-support/res/values-bs/strings.xml
@@ -20,5 +20,5 @@
<string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
<string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
<string name="program_list_text" msgid="4414150317304422313">"Stanice"</string>
- <string name="favorites_list_text" msgid="7829827713977109155">"Omiljeni"</string>
+ <string name="favorites_list_text" msgid="7829827713977109155">"Omiljeno"</string>
</resources>
diff --git a/car-broadcastradio-support/res/values-is/strings.xml b/car-broadcastradio-support/res/values-is/strings.xml
index 1a2074a..b7e9135 100644
--- a/car-broadcastradio-support/res/values-is/strings.xml
+++ b/car-broadcastradio-support/res/values-is/strings.xml
@@ -20,5 +20,5 @@
<string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
<string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
<string name="program_list_text" msgid="4414150317304422313">"Stöðvar"</string>
- <string name="favorites_list_text" msgid="7829827713977109155">"Eftirlæti"</string>
+ <string name="favorites_list_text" msgid="7829827713977109155">"Uppáhald"</string>
</resources>
diff --git a/car-broadcastradio-support/res/values-ky/strings.xml b/car-broadcastradio-support/res/values-ky/strings.xml
index 4640491..ee67f46 100644
--- a/car-broadcastradio-support/res/values-ky/strings.xml
+++ b/car-broadcastradio-support/res/values-ky/strings.xml
@@ -20,5 +20,5 @@
<string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
<string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
<string name="program_list_text" msgid="4414150317304422313">"Станциялар"</string>
- <string name="favorites_list_text" msgid="7829827713977109155">"Сүйүктүүлөр"</string>
+ <string name="favorites_list_text" msgid="7829827713977109155">"Тандалмалар"</string>
</resources>
diff --git a/car-broadcastradio-support/res/values-uz/strings.xml b/car-broadcastradio-support/res/values-uz/strings.xml
index 5ff7a60..bfb89d7 100644
--- a/car-broadcastradio-support/res/values-uz/strings.xml
+++ b/car-broadcastradio-support/res/values-uz/strings.xml
@@ -20,5 +20,5 @@
<string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
<string name="radio_dab_text" msgid="8456449462266648979">"Raqamli radio"</string>
<string name="program_list_text" msgid="4414150317304422313">"Radiostansiyalar"</string>
- <string name="favorites_list_text" msgid="7829827713977109155">"Saralanganlar"</string>
+ <string name="favorites_list_text" msgid="7829827713977109155">"Saralangan"</string>
</resources>
diff --git a/car-media-common/res/drawable/fab_empty_foreground.xml b/car-media-common/res/drawable/fab_empty_foreground.xml
deleted file mode 100644
index d9fb901..0000000
--- a/car-media-common/res/drawable/fab_empty_foreground.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2018, 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.
--->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:radius="0dp"
- android:color="@color/car_dark_blue_grey_700" />
diff --git a/car-media-common/res/layout/minimized_play_pause_stop_button_layout.xml b/car-media-common/res/layout/minimized_play_pause_stop_button_layout.xml
index 38389b0..1d70c72 100644
--- a/car-media-common/res/layout/minimized_play_pause_stop_button_layout.xml
+++ b/car-media-common/res/layout/minimized_play_pause_stop_button_layout.xml
@@ -20,11 +20,9 @@
android:focusable="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
- <!-- The invisible foreground ripple stops Android O from drawing an ugly square over the play button -->
<com.android.car.media.common.PlayPauseStopImageView
android:id="@+id/play_pause_stop"
style="@style/Widget.ActionButton"
- android:foreground="@drawable/fab_empty_foreground"
android:src="@drawable/ic_play_pause_stop_animated"/>
<ProgressBar
android:id="@+id/circular_progress_bar"
diff --git a/car-media-common/res/layout/play_pause_stop_button_layout.xml b/car-media-common/res/layout/play_pause_stop_button_layout.xml
index f7700fe..f61a821 100644
--- a/car-media-common/res/layout/play_pause_stop_button_layout.xml
+++ b/car-media-common/res/layout/play_pause_stop_button_layout.xml
@@ -20,11 +20,9 @@
android:focusable="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
- <!-- The invisible foreground ripple stops Android O from drawing an ugly square over the play button -->
<com.android.car.media.common.PlayPauseStopImageView
android:id="@+id/play_pause_stop"
style="@style/Widget.ActionButton"
- android:foreground="@drawable/fab_empty_foreground"
android:src="@drawable/ic_play_pause_stop_animated"/>
<ProgressBar
android:id="@+id/circular_progress_bar"
diff --git a/car-media-common/res/values-mk/strings.xml b/car-media-common/res/values-mk/strings.xml
index 7f3a733..5e71b5a 100644
--- a/car-media-common/res/values-mk/strings.xml
+++ b/car-media-common/res/values-mk/strings.xml
@@ -29,6 +29,6 @@
<string name="error_code_not_available_in_region" msgid="5840935836875073145">"Не може да се добие таа содржина тука"</string>
<string name="error_code_content_already_playing" msgid="1306236349553004461">"Веќе е пуштена таа содржина"</string>
<string name="error_code_skip_limit_reached" msgid="4203743406433151146">"Не може да прескокне повеќе песни"</string>
- <string name="error_code_action_aborted" msgid="8611777981356536501">"Не можеше да заврши. Обидете се повторно."</string>
+ <string name="error_code_action_aborted" msgid="8611777981356536501">"Не може да заврши. Обидете се повторно."</string>
<string name="error_code_end_of_queue" msgid="6935022448319288887">"Веќе ништо не чека на ред"</string>
</resources>
diff --git a/car-media-common/src/com/android/car/media/common/ControlBarHelper.java b/car-media-common/src/com/android/car/media/common/ControlBarHelper.java
index 0f011fd..4e3b7b9 100644
--- a/car-media-common/src/com/android/car/media/common/ControlBarHelper.java
+++ b/car-media-common/src/com/android/car/media/common/ControlBarHelper.java
@@ -64,8 +64,8 @@
model.getProgress().observe(owner,
progress -> {
- progressBar.setProgress((int) progress.getProgress());
progressBar.setMax((int) progress.getMaxProgress());
+ progressBar.setProgress((int) progress.getProgress());
});
}
}
diff --git a/car-messenger-common/res/values-mn/strings.xml b/car-messenger-common/res/values-mn/strings.xml
index d47453b..0cd0bcc 100644
--- a/car-messenger-common/res/values-mn/strings.xml
+++ b/car-messenger-common/res/values-mn/strings.xml
@@ -18,8 +18,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
- <item quantity="other">%d шинэ зурвас</item>
- <item quantity="one">Шинэ зурвас</item>
+ <item quantity="other">%d шинэ мессеж</item>
+ <item quantity="one">Шинэ мессеж</item>
</plurals>
<string name="action_play" msgid="1884580550634079470">"Тоглуулах"</string>
<string name="action_mark_as_read" msgid="5185216939940407938">"Уншсан гэж тэмдэглэх"</string>
@@ -30,7 +30,7 @@
<string name="auto_reply_failed_message" msgid="6445984971657465627">"Хариу илгээх боломжгүй байна. Дахин оролдоно уу."</string>
<string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Хариу илгээх боломжгүй байна. Төхөөрөмж холбогдоогүй байна."</string>
<string name="tts_sender_says" msgid="5352698006545359668">"%s хэлж байна"</string>
- <string name="tts_failed_toast" msgid="1483313550894086353">"Зурвасыг унших боломжгүй байна."</string>
+ <string name="tts_failed_toast" msgid="1483313550894086353">"Мессежийг унших боломжгүй байна."</string>
<string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
<string name="message_sent_notice" msgid="7172592196465284673">"%s-д хариу илгээсэн"</string>
<string name="name_not_available" msgid="3800013092212550915">"Нэр ашиглалтад алга"</string>
diff --git a/car-messenger-common/src/com/android/car/messenger/common/Utils.java b/car-messenger-common/src/com/android/car/messenger/common/Utils.java
index 68ed3aa..c52cc56 100644
--- a/car-messenger-common/src/com/android/car/messenger/common/Utils.java
+++ b/car-messenger-common/src/com/android/car/messenger/common/Utils.java
@@ -373,9 +373,9 @@
public static String constructGroupConversationHeader(String senderName, String groupName,
String delimiter, BidiFormatter bidiFormatter) {
String formattedSenderName = bidiFormatter.unicodeWrap(senderName,
- TextDirectionHeuristics.FIRSTSTRONG_LTR);
+ TextDirectionHeuristics.FIRSTSTRONG_LTR, /* isolate= */ true);
String formattedGroupName = bidiFormatter.unicodeWrap(groupName,
- TextDirectionHeuristics.LOCALE);
+ TextDirectionHeuristics.FIRSTSTRONG_LTR, /* isolate= */ true);
String title = String.join(delimiter, formattedSenderName, formattedGroupName);
return bidiFormatter.unicodeWrap(title, TextDirectionHeuristics.LOCALE);
}
diff --git a/car-messenger-common/tests/unit/src/com.android.car.messenger.common/UtilsTest.java b/car-messenger-common/tests/unit/src/com.android.car.messenger.common/UtilsTest.java
index acdffbd..c64703a 100644
--- a/car-messenger-common/tests/unit/src/com.android.car.messenger.common/UtilsTest.java
+++ b/car-messenger-common/tests/unit/src/com.android.car.messenger.common/UtilsTest.java
@@ -200,4 +200,17 @@
assertThat(actual).isEqualTo(expected);
}
+
+ @Test
+ public void testTitleConstructorRtl_withTrailingPunctuation() {
+ String actual = Utils.constructGroupConversationHeader("Christopher",
+ "Abcd!!!", TITLE_DELIMITER, /* isRtl */
+ RTL_FORMATTER).trim();
+
+ String expected =
+ "\u200F\u202A\u200F\u202AChristopher\u202C\u200F : \u200F\u202AAbcd!!!"
+ + "\u202C\u200F\u202C\u200F";
+
+ assertThat(actual).isEqualTo(expected);
+ }
}
diff --git a/car-telephony-common/res/values-bg/strings.xml b/car-telephony-common/res/values-bg/strings.xml
index f308b5e..99d16bd 100644
--- a/car-telephony-common/res/values-bg/strings.xml
+++ b/car-telephony-common/res/values-bg/strings.xml
@@ -20,7 +20,7 @@
<string name="voicemail" msgid="2125552157407909509">"Гласова поща"</string>
<string name="phone_label_with_info" msgid="4652109530699808645">"<xliff:g id="LABEL">%1$s</xliff:g> · <xliff:g id="DURATION">%2$s</xliff:g>"</string>
<string name="call_state_connecting" msgid="5930724746375294866">"Свързва се…"</string>
- <string name="call_state_dialing" msgid="1534599871716648114">"Набира се…"</string>
+ <string name="call_state_dialing" msgid="1534599871716648114">"Набиране…"</string>
<string name="call_state_hold" msgid="8063542005458186874">"Задържане на обаждането"</string>
<string name="call_state_call_ended" msgid="1432127342949555464">"Обаждането завърши"</string>
<string name="call_state_call_active" msgid="2769644783657864202">"Установена е връзка"</string>
diff --git a/car-telephony-common/res/values-et/strings.xml b/car-telephony-common/res/values-et/strings.xml
index 6707e93..9926364 100644
--- a/car-telephony-common/res/values-et/strings.xml
+++ b/car-telephony-common/res/values-et/strings.xml
@@ -25,5 +25,5 @@
<string name="call_state_call_ended" msgid="1432127342949555464">"Kõne lõpetati"</string>
<string name="call_state_call_active" msgid="2769644783657864202">"Ühendatud"</string>
<string name="call_state_call_ringing" msgid="4618803402954375017">"Heliseb …"</string>
- <string name="call_state_call_ending" msgid="5037498349965472247">"Ühenduse katkest. …"</string>
+ <string name="call_state_call_ending" msgid="5037498349965472247">"Ühenduse katkestamine…"</string>
</resources>
diff --git a/car-telephony-common/res/values-in/strings.xml b/car-telephony-common/res/values-in/strings.xml
index fe742e7..7450656 100644
--- a/car-telephony-common/res/values-in/strings.xml
+++ b/car-telephony-common/res/values-in/strings.xml
@@ -25,5 +25,5 @@
<string name="call_state_call_ended" msgid="1432127342949555464">"Panggilan diakhiri"</string>
<string name="call_state_call_active" msgid="2769644783657864202">"Terhubung"</string>
<string name="call_state_call_ringing" msgid="4618803402954375017">"Berdering…"</string>
- <string name="call_state_call_ending" msgid="5037498349965472247">"Memutus hubungan..."</string>
+ <string name="call_state_call_ending" msgid="5037498349965472247">"Memutus sambungan..."</string>
</resources>
diff --git a/car-telephony-common/res/values-iw/strings.xml b/car-telephony-common/res/values-iw/strings.xml
index aad5c48..67620e7 100644
--- a/car-telephony-common/res/values-iw/strings.xml
+++ b/car-telephony-common/res/values-iw/strings.xml
@@ -20,7 +20,7 @@
<string name="voicemail" msgid="2125552157407909509">"דואר קולי"</string>
<string name="phone_label_with_info" msgid="4652109530699808645">"<xliff:g id="LABEL">%1$s</xliff:g> · <xliff:g id="DURATION">%2$s</xliff:g>"</string>
<string name="call_state_connecting" msgid="5930724746375294866">"מתחבר…"</string>
- <string name="call_state_dialing" msgid="1534599871716648114">"מחייג…"</string>
+ <string name="call_state_dialing" msgid="1534599871716648114">"החיוג מתבצע…"</string>
<string name="call_state_hold" msgid="8063542005458186874">"בהמתנה"</string>
<string name="call_state_call_ended" msgid="1432127342949555464">"השיחה הסתיימה"</string>
<string name="call_state_call_active" msgid="2769644783657864202">"מתבצעת שיחה"</string>
diff --git a/car-telephony-common/res/values-ms/strings.xml b/car-telephony-common/res/values-ms/strings.xml
index 98bcb58..038ac0c 100644
--- a/car-telephony-common/res/values-ms/strings.xml
+++ b/car-telephony-common/res/values-ms/strings.xml
@@ -25,5 +25,5 @@
<string name="call_state_call_ended" msgid="1432127342949555464">"Panggilan tamat"</string>
<string name="call_state_call_active" msgid="2769644783657864202">"Disambungkan"</string>
<string name="call_state_call_ringing" msgid="4618803402954375017">"Berdering…"</string>
- <string name="call_state_call_ending" msgid="5037498349965472247">"Memutuskan sambungn…"</string>
+ <string name="call_state_call_ending" msgid="5037498349965472247">"Memutuskan sambungan…"</string>
</resources>
diff --git a/car-telephony-common/res/values-ne/strings.xml b/car-telephony-common/res/values-ne/strings.xml
index 8c3f1ff..2283a98 100644
--- a/car-telephony-common/res/values-ne/strings.xml
+++ b/car-telephony-common/res/values-ne/strings.xml
@@ -23,7 +23,7 @@
<string name="call_state_dialing" msgid="1534599871716648114">"डायल गर्दै…"</string>
<string name="call_state_hold" msgid="8063542005458186874">"होल्डमा छ"</string>
<string name="call_state_call_ended" msgid="1432127342949555464">"कल समाप्त भयो"</string>
- <string name="call_state_call_active" msgid="2769644783657864202">"जडान गरिएको छ"</string>
+ <string name="call_state_call_active" msgid="2769644783657864202">"कनेक्ट गरिएको छ"</string>
<string name="call_state_call_ringing" msgid="4618803402954375017">"घन्टी बज्दै छ…"</string>
<string name="call_state_call_ending" msgid="5037498349965472247">"विच्छेद गर्दै…"</string>
</resources>
diff --git a/car-telephony-common/res/values-nl/strings.xml b/car-telephony-common/res/values-nl/strings.xml
index e97b569..c1f4c00 100644
--- a/car-telephony-common/res/values-nl/strings.xml
+++ b/car-telephony-common/res/values-nl/strings.xml
@@ -25,5 +25,5 @@
<string name="call_state_call_ended" msgid="1432127342949555464">"Gesprek beëindigd"</string>
<string name="call_state_call_active" msgid="2769644783657864202">"Verbonden"</string>
<string name="call_state_call_ringing" msgid="4618803402954375017">"Gaat over…"</string>
- <string name="call_state_call_ending" msgid="5037498349965472247">"Verb. verbreken…"</string>
+ <string name="call_state_call_ending" msgid="5037498349965472247">"Verbreken…"</string>
</resources>
diff --git a/car-telephony-common/res/values-pt/strings.xml b/car-telephony-common/res/values-pt/strings.xml
index 9ac136b..f56f6ad 100644
--- a/car-telephony-common/res/values-pt/strings.xml
+++ b/car-telephony-common/res/values-pt/strings.xml
@@ -20,7 +20,7 @@
<string name="voicemail" msgid="2125552157407909509">"Correio de voz"</string>
<string name="phone_label_with_info" msgid="4652109530699808645">"<xliff:g id="LABEL">%1$s</xliff:g> · <xliff:g id="DURATION">%2$s</xliff:g>"</string>
<string name="call_state_connecting" msgid="5930724746375294866">"Conectando…"</string>
- <string name="call_state_dialing" msgid="1534599871716648114">"Discando…"</string>
+ <string name="call_state_dialing" msgid="1534599871716648114">"Chamando...…"</string>
<string name="call_state_hold" msgid="8063542005458186874">"Em espera"</string>
<string name="call_state_call_ended" msgid="1432127342949555464">"Chamada encerrada"</string>
<string name="call_state_call_active" msgid="2769644783657864202">"Conectado"</string>
diff --git a/car-telephony-common/res/values-sw/strings.xml b/car-telephony-common/res/values-sw/strings.xml
index 6711f55..28a496b 100644
--- a/car-telephony-common/res/values-sw/strings.xml
+++ b/car-telephony-common/res/values-sw/strings.xml
@@ -20,10 +20,10 @@
<string name="voicemail" msgid="2125552157407909509">"Ujumbe wa sauti"</string>
<string name="phone_label_with_info" msgid="4652109530699808645">"<xliff:g id="LABEL">%1$s</xliff:g> · <xliff:g id="DURATION">%2$s</xliff:g>"</string>
<string name="call_state_connecting" msgid="5930724746375294866">"Inaunganisha…"</string>
- <string name="call_state_dialing" msgid="1534599871716648114">"Inapigia…"</string>
+ <string name="call_state_dialing" msgid="1534599871716648114">"Inapiga…"</string>
<string name="call_state_hold" msgid="8063542005458186874">"Imesitishwa"</string>
<string name="call_state_call_ended" msgid="1432127342949555464">"Simu imekamilika"</string>
<string name="call_state_call_active" msgid="2769644783657864202">"Imeunganisha"</string>
<string name="call_state_call_ringing" msgid="4618803402954375017">"Inalia…"</string>
- <string name="call_state_call_ending" msgid="5037498349965472247">"Inaondoa…"</string>
+ <string name="call_state_call_ending" msgid="5037498349965472247">"Inakata…"</string>
</resources>
diff --git a/car-telephony-common/res/values-th/strings.xml b/car-telephony-common/res/values-th/strings.xml
index 0a8a28c..6b5a368 100644
--- a/car-telephony-common/res/values-th/strings.xml
+++ b/car-telephony-common/res/values-th/strings.xml
@@ -25,5 +25,5 @@
<string name="call_state_call_ended" msgid="1432127342949555464">"วางสายแล้ว"</string>
<string name="call_state_call_active" msgid="2769644783657864202">"เชื่อมต่อแล้ว"</string>
<string name="call_state_call_ringing" msgid="4618803402954375017">"กำลังส่งเสียง…"</string>
- <string name="call_state_call_ending" msgid="5037498349965472247">"ยกเลิกการเชื่อมต่อ…"</string>
+ <string name="call_state_call_ending" msgid="5037498349965472247">"ยกเลิกการโทรออก…"</string>
</resources>
diff --git a/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java b/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java
index dac0aa3..8ed7e73 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java
@@ -25,8 +25,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
+import androidx.lifecycle.Transformations;
import com.android.car.apps.common.log.L;
@@ -59,10 +59,9 @@
private final Map<String, Map<String, Contact>> mLookupKeyContactMap = new HashMap<>();
/**
- * A map which divides contacts LiveData by account.
+ * A map which divides contacts by account.
*/
- private final Map<String, MutableLiveData<List<Contact>>> mAccountContactsLiveDataMap =
- new ArrayMap<>();
+ private final Map<String, List<Contact>> mAccountContactsMap = new ArrayMap<>();
private boolean mIsLoaded = false;
/**
@@ -160,10 +159,8 @@
* Bluetooth address.
*/
public LiveData<List<Contact>> getContactsLiveDataByAccount(String accountName) {
- if (!mAccountContactsLiveDataMap.containsKey(accountName)) {
- mAccountContactsLiveDataMap.put(accountName, new MutableLiveData<>());
- }
- return mAccountContactsLiveDataMap.get(accountName);
+ return Transformations.map(mContactListAsyncQueryLiveData,
+ contacts -> contacts == null ? null : mAccountContactsMap.get(accountName));
}
/**
@@ -252,12 +249,13 @@
subMap.put(lookupKey, Contact.fromCursor(mContext, cursor, subMap.get(lookupKey)));
}
+ mAccountContactsMap.clear();
for (String accountName : contactMap.keySet()) {
Map<String, Contact> subMap = contactMap.get(accountName);
contactList.addAll(subMap.values());
- MutableLiveData<List<Contact>> accountContactsLiveData =
- (MutableLiveData<List<Contact>>) getContactsLiveDataByAccount(accountName);
- accountContactsLiveData.postValue(new ArrayList<>(subMap.values()));
+ List<Contact> accountContacts = new ArrayList<>();
+ accountContacts.addAll(subMap.values());
+ mAccountContactsMap.put(accountName, accountContacts);
}
mLookupKeyContactMap.clear();
diff --git a/car-telephony-common/src/com/android/car/telephony/common/ObservableAsyncQuery.java b/car-telephony-common/src/com/android/car/telephony/common/ObservableAsyncQuery.java
index b3c1fb1..394b6d4 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/ObservableAsyncQuery.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/ObservableAsyncQuery.java
@@ -50,9 +50,9 @@
private AsyncQueryHandler mAsyncQueryHandler;
private QueryParam.Provider mQueryParamProvider;
- private Cursor mCurrentCursor;
private OnQueryFinishedListener mOnQueryFinishedListener;
private ContentObserver mContentObserver;
+ private ContentResolver mContentResolver;
private boolean mIsActive = false;
private int mToken;
@@ -65,6 +65,7 @@
@NonNull ContentResolver cr,
@NonNull OnQueryFinishedListener listener) {
mAsyncQueryHandler = new AsyncQueryHandlerImpl(this, cr);
+ mContentResolver = cr;
mContentObserver = new ContentObserver(mAsyncQueryHandler) {
@Override
public void onChange(boolean selfChange) {
@@ -83,6 +84,7 @@
public void startQuery() {
L.d(TAG, "startQuery");
mAsyncQueryHandler.cancelOperation(mToken); // Cancel the query task.
+ mContentResolver.unregisterContentObserver(mContentObserver);
mToken++;
QueryParam queryParam = mQueryParamProvider.getQueryParam();
@@ -95,6 +97,7 @@
queryParam.mSelection,
queryParam.mSelectionArgs,
queryParam.mOrderBy);
+ mContentResolver.registerContentObserver(queryParam.mUri, false, mContentObserver);
} else {
mOnQueryFinishedListener.onQueryFinished(null);
}
@@ -109,7 +112,7 @@
public void stopQuery() {
L.d(TAG, "stopQuery");
mIsActive = false;
- cleanupCursorIfNecessary();
+ mContentResolver.unregisterContentObserver(mContentObserver);
mAsyncQueryHandler.cancelOperation(mToken); // Cancel the query task.
}
@@ -118,23 +121,11 @@
return;
}
L.d(TAG, "onQueryComplete");
- cleanupCursorIfNecessary();
- if (cursor != null) {
- cursor.registerContentObserver(mContentObserver);
- mCurrentCursor = cursor;
- }
if (mOnQueryFinishedListener != null) {
mOnQueryFinishedListener.onQueryFinished(cursor);
}
}
- protected void cleanupCursorIfNecessary() {
- if (mCurrentCursor != null) {
- mCurrentCursor.unregisterContentObserver(mContentObserver);
- }
- mCurrentCursor = null;
- }
-
private static class AsyncQueryHandlerImpl extends AsyncQueryHandler {
private ObservableAsyncQuery mQuery;
diff --git a/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java b/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
index a790318..40d93a7 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
@@ -129,11 +129,10 @@
}
String countryIso = getCurrentCountryIsoFromLocale(context);
- L.d(TAG, "PhoneNumberUtils.formatNumberToE16, number: "
- + piiLog(number) + ", country: " + countryIso);
+ L.d(TAG, "PhoneNumberUtils.formatNumber, number: " + piiLog(number)
+ + ", country: " + countryIso);
- String e164 = PhoneNumberUtils.formatNumberToE164(number, countryIso);
- String formattedNumber = PhoneNumberUtils.formatNumber(number, e164, countryIso);
+ String formattedNumber = PhoneNumberUtils.formatNumber(number, countryIso);
formattedNumber = TextUtils.isEmpty(formattedNumber) ? number : formattedNumber;
L.d(TAG, "getFormattedNumber, result: " + piiLog(formattedNumber));
@@ -509,6 +508,24 @@
* valid, it will mark all new missed call log as read.
*/
public static void markCallLogAsRead(Context context, String phoneNumberString) {
+ markCallLogAsRead(context, CallLog.Calls.NUMBER, phoneNumberString);
+ }
+
+ /**
+ * Mark missed call log matching given call log id as read. If phone number string is not
+ * valid, it will mark all new missed call log as read.
+ */
+ public static void markCallLogAsRead(Context context, long callLogId) {
+ markCallLogAsRead(context, CallLog.Calls._ID,
+ callLogId < 0 ? null : String.valueOf(callLogId));
+ }
+
+ /**
+ * Mark missed call log matching given column name and selection argument as read. If the column
+ * name or the selection argument is not valid, mark all new missed call log as read.
+ */
+ private static void markCallLogAsRead(Context context, String columnName,
+ String selectionArg) {
if (context.checkSelfPermission(Manifest.permission.WRITE_CALL_LOG)
!= PackageManager.PERMISSION_GRANTED) {
L.w(TAG, "Missing WRITE_CALL_LOG permission; not marking missed calls as read.");
@@ -525,21 +542,22 @@
where.append(CallLog.Calls.TYPE);
where.append(" = ?");
selectionArgs.add(Integer.toString(CallLog.Calls.MISSED_TYPE));
- if (!TextUtils.isEmpty(phoneNumberString)) {
+ if (!TextUtils.isEmpty(columnName) && !TextUtils.isEmpty(selectionArg)) {
where.append(" AND ");
- where.append(CallLog.Calls.NUMBER);
+ where.append(columnName);
where.append(" = ?");
- selectionArgs.add(phoneNumberString);
+ selectionArgs.add(selectionArg);
}
String[] selectionArgsArray = new String[0];
try {
- context
- .getContentResolver()
- .update(
- CallLog.Calls.CONTENT_URI,
- contentValues,
- where.toString(),
- selectionArgs.toArray(selectionArgsArray));
+ ContentResolver contentResolver = context.getContentResolver();
+ contentResolver.update(
+ CallLog.Calls.CONTENT_URI,
+ contentValues,
+ where.toString(),
+ selectionArgs.toArray(selectionArgsArray));
+ // #update doesn't notify change any more. Notify change to rerun query from database.
+ contentResolver.notifyChange(CallLog.Calls.CONTENT_URI, null);
} catch (IllegalArgumentException e) {
L.e(TAG, "markCallLogAsRead failed", e);
}
diff --git a/car-ui-lib/.gitignore b/car-ui-lib/.gitignore
index a6be7ed..57df0a4 100644
--- a/car-ui-lib/.gitignore
+++ b/car-ui-lib/.gitignore
@@ -16,3 +16,6 @@
# Android studio's layout inspector captures
captures/
+
+# A file created when launching android emulators
+read-snapshot.txt
diff --git a/car-ui-lib/build.gradle b/car-ui-lib/build.gradle
index 9744622..28633d9 100644
--- a/car-ui-lib/build.gradle
+++ b/car-ui-lib/build.gradle
@@ -23,7 +23,7 @@
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.6.1'
+ classpath 'com.android.tools.build:gradle:4.0.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/car-ui-lib/car-ui-lib/AndroidManifest.xml b/car-ui-lib/car-ui-lib/AndroidManifest.xml
index 5136dc2..a85261a 100644
--- a/car-ui-lib/car-ui-lib/AndroidManifest.xml
+++ b/car-ui-lib/car-ui-lib/AndroidManifest.xml
@@ -28,5 +28,11 @@
android:directBootAware="true"
android:exported="false"
android:process="@string/car_ui_installer_process_name"/>
+
+ <provider
+ android:name="com.android.car.ui.core.SearchResultsProvider"
+ android:authorities="${applicationId}.SearchResultsProvider"
+ android:exported="true"
+ android:process="@string/car_ui_installer_process_name"/>
</application>
</manifest>
diff --git a/car-ui-lib/car-ui-lib/build.gradle b/car-ui-lib/car-ui-lib/build.gradle
index 5c6ce32..201a8d6 100644
--- a/car-ui-lib/car-ui-lib/build.gradle
+++ b/car-ui-lib/car-ui-lib/build.gradle
@@ -60,20 +60,20 @@
api 'androidx.core:core:1.2.0'
implementation 'com.android.support:support-annotations:28.0.0'
- testImplementation "androidx.test.ext:junit:1.1.1"
- testImplementation 'org.robolectric:robolectric:4.4'
+ testImplementation "androidx.test.ext:junit:1.1.2"
+ testImplementation 'org.robolectric:robolectric:4.3.1'
testImplementation "org.mockito:mockito-core:2.19.0"
testImplementation "com.google.truth:truth:0.29"
testImplementation "org.testng:testng:6.9.9"
androidTestImplementation 'org.hamcrest:hamcrest-library:1.3'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
- androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0'
androidTestImplementation "com.google.truth:truth:0.29"
- androidTestImplementation "androidx.test.ext:junit:1.1.1"
+ androidTestImplementation "androidx.test.ext:junit:1.1.2"
androidTestImplementation "org.mockito:mockito-core:2.19.0"
- androidTestImplementation 'androidx.test:runner:1.1.0'
- androidTestImplementation 'androidx.test:rules:1.1.0'
+ androidTestImplementation 'androidx.test:runner:1.3.0'
+ androidTestImplementation 'androidx.test:rules:1.3.0'
// This is needed to be able to spy certain classes with Mockito
// It's major/minor version must match Mockito's.
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito-inline:2.19.0'
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/AndroidManifest.xml b/car-ui-lib/car-ui-lib/src/androidTest/AndroidManifest.xml
index 81df306..79b51bb 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/AndroidManifest.xml
+++ b/car-ui-lib/car-ui-lib/src/androidTest/AndroidManifest.xml
@@ -16,14 +16,19 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.car.ui.test">
- <application android:debuggable="true" android:theme="@style/Theme.CarUi">
+ <application android:debuggable="true" android:theme="@style/Theme.CarUi.NoToolbar">
<uses-library android:name="android.test.runner" />
<activity android:name="com.android.car.ui.TestActivity" />
<activity android:name="com.android.car.ui.recyclerview.CarUiRecyclerViewTestActivity" />
+ <activity android:name="com.android.car.ui.imewidescreen.CarUiImeWideScreenTestActivity" />
<activity android:name="com.android.car.ui.FocusAreaTestActivity" />
<activity android:name="com.android.car.ui.FocusParkingViewTestActivity" />
<activity android:name="com.android.car.ui.preference.PreferenceTestActivity" />
<activity
+ android:name="com.android.car.ui.preference.NonFullscreenPreferenceFragmentTest$MyActivity"
+ android:theme="@style/Theme.CarUi.WithToolbar"/>
+ <activity android:name="com.android.car.ui.utils.ViewUtilsTestActivity" />
+ <activity
android:name="com.android.car.ui.toolbar.ToolbarTestActivity"
android:theme="@style/Theme.CarUi.WithToolbar"/>
</application>
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/AlertDialogBuilderTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/AlertDialogBuilderTest.java
new file mode 100644
index 0000000..0aaa250
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/AlertDialogBuilderTest.java
@@ -0,0 +1,256 @@
+/*
+ * 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.ui;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+import android.app.AlertDialog;
+import android.database.Cursor;
+import android.view.View;
+
+import androidx.test.espresso.Root;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.car.ui.recyclerview.CarUiContentListItem;
+import com.android.car.ui.recyclerview.CarUiListItemAdapter;
+import com.android.car.ui.recyclerview.CarUiRadioButtonListItem;
+import com.android.car.ui.recyclerview.CarUiRadioButtonListItemAdapter;
+import com.android.car.ui.test.R;
+
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class AlertDialogBuilderTest {
+
+ @Rule
+ public ActivityTestRule<TestActivity> mActivityRule =
+ new ActivityTestRule<>(TestActivity.class);
+
+ @Test
+ public void test_AlertDialogBuilder_works() throws Throwable {
+ String title = "Test message from AlertDialogBuilder";
+ String subtitle = "Subtitle from AlertDialogBuilder";
+ mActivityRule.runOnUiThread(() ->
+ new AlertDialogBuilder(mActivityRule.getActivity())
+ .setMessage(title)
+ .setSubtitle(subtitle)
+ .show());
+
+ AlertDialog dialog = checkDefaultButtonExists(true,
+ new AlertDialogBuilder(mActivityRule.getActivity())
+ .setMessage(title)
+ .setSubtitle(subtitle));
+ onView(withText(title))
+ .inRoot(new RootWithDecorMatcher(dialog.getWindow().getDecorView()))
+ .check(matches(isDisplayed()));
+ onView(withText(subtitle))
+ .inRoot(new RootWithDecorMatcher(dialog.getWindow().getDecorView()))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void test_showSingleListChoiceItem_StringArray_hidesDefaultButton() throws Throwable {
+ AlertDialogBuilder builder = new AlertDialogBuilder(mActivityRule.getActivity())
+ .setAllowDismissButton(false)
+ .setSingleChoiceItems(new CharSequence[]{"Item 1", "Item 2"}, 0,
+ ((dialog, which) -> {
+ }));
+
+ checkDefaultButtonExists(false, builder);
+ }
+
+ @Test
+ public void test_showSingleListChoiceItem_StringArrayResource_hidesDefaultButton()
+ throws Throwable {
+ AlertDialogBuilder builder = new AlertDialogBuilder(mActivityRule.getActivity())
+ .setAllowDismissButton(false)
+ .setSingleChoiceItems(R.array.test_string_array, 0, ((dialog, which) -> {
+ }));
+
+ checkDefaultButtonExists(false, builder);
+ }
+
+ @Test
+ public void test_showSingleListChoiceItem_CarUiRadioButtonListItemAdapter_forcesDefaultButton()
+ throws Throwable {
+ CarUiRadioButtonListItem item1 = new CarUiRadioButtonListItem();
+ item1.setTitle("Item 1");
+ CarUiRadioButtonListItem item2 = new CarUiRadioButtonListItem();
+ item2.setTitle("Item 2");
+ CarUiRadioButtonListItem item3 = new CarUiRadioButtonListItem();
+ item3.setTitle("Item 3");
+
+ CarUiRadioButtonListItemAdapter adapter = new CarUiRadioButtonListItemAdapter(
+ Arrays.asList(item1, item2, item3));
+ AlertDialogBuilder builder = new AlertDialogBuilder(mActivityRule.getActivity())
+ .setAllowDismissButton(false)
+ .setSingleChoiceItems(adapter);
+
+ checkDefaultButtonExists(true, builder);
+ }
+
+ @Test
+ public void test_showSingleListChoiceItem_cursor_hidesDefaultButton() throws Throwable {
+ Cursor cursor = new FakeCursor(Arrays.asList("Item 1", "Item 2"), "ColumnName");
+ AlertDialogBuilder builder = new AlertDialogBuilder(mActivityRule.getActivity())
+ .setTitle("Title")
+ .setAllowDismissButton(false)
+ .setSingleChoiceItems(cursor, 0, "ColumnName", ((dialog, which) -> {
+ }));
+
+ checkDefaultButtonExists(false, builder);
+ }
+
+ @Test
+ public void test_setItems_StringArrayResource_hidesDefaultButton() throws Throwable {
+ AlertDialogBuilder builder = new AlertDialogBuilder(mActivityRule.getActivity())
+ .setAllowDismissButton(false)
+ .setItems(R.array.test_string_array, ((dialog, which) -> {
+ }));
+
+ checkDefaultButtonExists(false, builder);
+ }
+
+ @Test
+ public void test_setItems_StringArray_hidesDefaultButton() throws Throwable {
+ AlertDialogBuilder builder = new AlertDialogBuilder(mActivityRule.getActivity())
+ .setAllowDismissButton(false)
+ .setItems(new CharSequence[]{"Item 1", "Item 2"}, ((dialog, which) -> {
+ }));
+
+ checkDefaultButtonExists(false, builder);
+ }
+
+ @Test
+ public void test_setAdapter_hidesDefaultButton()
+ throws Throwable {
+ CarUiContentListItem item1 = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
+ item1.setTitle("Item 1");
+ CarUiContentListItem item2 = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
+ item2.setTitle("Item 2");
+ CarUiContentListItem item3 = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
+ item3.setTitle("Item 3");
+
+ CarUiListItemAdapter adapter = new CarUiListItemAdapter(
+ Arrays.asList(item1, item2, item3));
+ AlertDialogBuilder builder = new AlertDialogBuilder(mActivityRule.getActivity())
+ .setAllowDismissButton(false)
+ .setAdapter(adapter);
+
+ checkDefaultButtonExists(false, builder);
+ }
+
+ @Test
+ public void test_multichoiceItems_StringArrayResource_forcesDefaultButton()
+ throws Throwable {
+ AlertDialogBuilder builder = new AlertDialogBuilder(mActivityRule.getActivity())
+ .setAllowDismissButton(false)
+ .setMultiChoiceItems(R.array.test_string_array, null,
+ ((dialog, which, isChecked) -> {
+ }));
+
+ checkDefaultButtonExists(true, builder);
+ }
+
+ @Test
+ public void test_multichoiceItems_StringArray_forcesDefaultButton()
+ throws Throwable {
+ AlertDialogBuilder builder = new AlertDialogBuilder(mActivityRule.getActivity())
+ .setAllowDismissButton(false)
+ .setMultiChoiceItems(new CharSequence[]{"Test 1", "Test 2"}, null,
+ ((dialog, which, isChecked) -> {
+ }));
+
+ checkDefaultButtonExists(true, builder);
+ }
+
+ @Test
+ public void test_multichoiceItems_Cursor_forcesDefaultButton()
+ throws Throwable {
+ Cursor cursor = new FakeCursor(Arrays.asList("Item 1", "Item 2"), "Label");
+ AlertDialogBuilder builder = new AlertDialogBuilder(mActivityRule.getActivity())
+ .setAllowDismissButton(false)
+ .setMultiChoiceItems(cursor, "isChecked", "Label",
+ ((dialog, which, isChecked) -> {
+ }));
+
+ checkDefaultButtonExists(true, builder);
+ }
+
+ private AlertDialog checkDefaultButtonExists(boolean shouldExist, AlertDialogBuilder builder)
+ throws Throwable {
+ AtomicBoolean done = new AtomicBoolean(false);
+ AlertDialog[] result = new AlertDialog[1];
+ mActivityRule.runOnUiThread(() -> {
+ try {
+ result[0] = builder.create();
+ result[0].show();
+ } catch (RuntimeException e) {
+ assert e.getMessage() != null;
+ assert e.getMessage().contains(
+ "must have at least one button to disable the dismiss button");
+
+ assert shouldExist;
+ done.set(true);
+ }
+ });
+
+ if (done.get()) {
+ return result[0];
+ }
+
+ if (shouldExist) {
+ onView(withText(R.string.car_ui_alert_dialog_default_button))
+ .inRoot(new RootWithDecorMatcher(result[0].getWindow().getDecorView()))
+ .check(matches(isDisplayed()));
+ } else {
+ onView(withText(R.string.car_ui_alert_dialog_default_button))
+ .inRoot(new RootWithDecorMatcher(result[0].getWindow().getDecorView()))
+ .check(doesNotExist());
+ }
+
+ return result[0];
+ }
+
+ private static class RootWithDecorMatcher extends TypeSafeMatcher<Root> {
+
+ private View mView;
+
+ RootWithDecorMatcher(View view) {
+ mView = view;
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("is a root with a certain decor");
+ }
+
+ @Override
+ protected boolean matchesSafely(Root item) {
+ return item.getDecorView() == mView;
+ }
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FakeCursor.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FakeCursor.java
new file mode 100644
index 0000000..d6bc4e1
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FakeCursor.java
@@ -0,0 +1,77 @@
+/*
+ * 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.ui;
+
+import android.database.AbstractCursor;
+
+import java.util.List;
+
+public class FakeCursor extends AbstractCursor {
+
+ private List<String> mRows;
+ private String mColumnName;
+
+ public FakeCursor(List<String> rows, String columnName) {
+ mRows = rows;
+ mColumnName = columnName;
+ }
+
+ @Override
+ public int getCount() {
+ return mRows.size();
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ return new String[] { mColumnName };
+ }
+
+ @Override
+ public String getString(int column) {
+ return mRows.get(getPosition());
+ }
+
+ @Override
+ public short getShort(int column) {
+ return 0;
+ }
+
+ @Override
+ public int getInt(int column) {
+ return 0;
+ }
+
+ @Override
+ public long getLong(int column) {
+ return 0;
+ }
+
+ @Override
+ public float getFloat(int column) {
+ return 0;
+ }
+
+ @Override
+ public double getDouble(int column) {
+ return 0;
+ }
+
+ @Override
+ public boolean isNull(int column) {
+ return mRows.get(getPosition()) == null;
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusAreaTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusAreaTest.java
index 9c1ce88..b0e7c88 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusAreaTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusAreaTest.java
@@ -16,24 +16,36 @@
package com.android.car.ui;
+import static android.view.View.FOCUS_DOWN;
+import static android.view.View.FOCUS_LEFT;
+import static android.view.View.FOCUS_RIGHT;
+import static android.view.View.FOCUS_UP;
import static android.view.View.LAYOUT_DIRECTION_LTR;
import static android.view.View.LAYOUT_DIRECTION_RTL;
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS;
+import static com.android.car.ui.RotaryCache.CACHE_TYPE_DISABLED;
+import static com.android.car.ui.RotaryCache.CACHE_TYPE_NEVER_EXPIRE;
+import static com.android.car.ui.utils.RotaryConstants.ACTION_NUDGE_SHORTCUT;
+import static com.android.car.ui.utils.RotaryConstants.ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA;
import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_BOTTOM_BOUND_OFFSET;
import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_LEFT_BOUND_OFFSET;
import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_RIGHT_BOUND_OFFSET;
import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_TOP_BOUND_OFFSET;
+import static com.android.car.ui.utils.RotaryConstants.NUDGE_DIRECTION;
import static com.google.common.truth.Truth.assertThat;
import android.os.Bundle;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.test.rule.ActivityTestRule;
+import com.android.car.ui.test.R;
+
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -41,7 +53,7 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-/** Unit tests for {@link FocusArea}. */
+/** Unit tests for {@link FocusArea} not in touch mode. */
public class FocusAreaTest {
private static final long WAIT_TIME_MS = 3000;
@@ -50,82 +62,365 @@
new ActivityTestRule<>(FocusAreaTestActivity.class);
private FocusAreaTestActivity mActivity;
- private TestFocusArea mFocusArea;
+ private TestFocusArea mFocusArea1;
private TestFocusArea mFocusArea2;
- private View mChild;
- private View mDefaultFocus;
- private View mNonChild;
- private View mChild1;
- private View mChild2;
+ private TestFocusArea mFocusArea3;
+ private TestFocusArea mFocusArea4;
+ private FocusParkingView mFpv;
+ private View mView1;
+ private Button mButton1;
+ private View mView2;
+ private View mDefaultFocus2;
+ private View mView3;
+ private View mNudgeShortcut3;
+ private View mView4;
@Before
public void setUp() {
mActivity = mActivityRule.getActivity();
- mFocusArea = mActivity.findViewById(R.id.focus_area);
- mFocusArea.enableForegroundHighlight();
+ mFocusArea1 = mActivity.findViewById(R.id.focus_area1);
mFocusArea2 = mActivity.findViewById(R.id.focus_area2);
- mChild = mActivity.findViewById(R.id.child);
- mDefaultFocus = mActivity.findViewById(R.id.default_focus);
- mNonChild = mActivity.findViewById(R.id.non_child);
- mChild1 = mActivity.findViewById(R.id.child1);
- mChild2 = mActivity.findViewById(R.id.child2);
+ mFocusArea3 = mActivity.findViewById(R.id.focus_area3);
+ mFocusArea4 = mActivity.findViewById(R.id.focus_area4);
+ mFpv = mActivity.findViewById(R.id.fpv);
+ mView1 = mActivity.findViewById(R.id.view1);
+ mButton1 = mActivity.findViewById(R.id.button1);
+ mView2 = mActivity.findViewById(R.id.view2);
+ mDefaultFocus2 = mActivity.findViewById(R.id.default_focus2);
+ mView3 = mActivity.findViewById(R.id.view3);
+ mNudgeShortcut3 = mActivity.findViewById(R.id.nudge_shortcut3);
+ mView4 = mActivity.findViewById(R.id.view4);
}
@Test
- public void testLoseFocus() throws Exception {
- mChild.post(() -> {
- mChild.requestFocus();
- });
- mFocusArea.setOnDrawCalled(false);
- mFocusArea.setDrawCalled(false);
-
- // FocusArea lost focus.
+ public void testDrawMethodsCalled() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
- mNonChild.post(() -> {
- mNonChild.requestFocus();
- mNonChild.post(() -> {
- latch.countDown();
- });
+ mView1.post(() -> {
+ mView1.requestFocus();
+ mFocusArea1.enableForegroundHighlight();
+ mFocusArea2.enableForegroundHighlight();
+ mFocusArea1.setOnDrawCalled(false);
+ mFocusArea1.setDrawCalled(false);
+ mFocusArea2.setOnDrawCalled(false);
+ mFocusArea2.setDrawCalled(false);
+
+ mView2.requestFocus();
+ mView2.post(() -> latch.countDown());
});
- assertDrawMethodsCalled(latch);
+
+ // The methods should be called when a FocusArea gains or loses focus.
+ assertDrawMethodsCalled(mFocusArea1, latch);
+ assertDrawMethodsCalled(mFocusArea2, latch);
}
@Test
- public void testGetFocus() throws Exception {
- mNonChild.post(() -> {
- mNonChild.requestFocus();
- });
- mFocusArea.setOnDrawCalled(false);
- mFocusArea.setDrawCalled(false);
+ public void testPerformAccessibilityAction_actionNudgeShortcut() {
+ mFocusArea1.post(() -> {
+ // Nudge to the nudgeShortcut view.
+ mView3.requestFocus();
+ assertThat(mView3.isFocused()).isTrue();
+ Bundle arguments = new Bundle();
+ arguments.putInt(NUDGE_DIRECTION, FOCUS_RIGHT);
+ mFocusArea3.performAccessibilityAction(ACTION_NUDGE_SHORTCUT, arguments);
+ assertThat(mNudgeShortcut3.isFocused()).isTrue();
- // FocusArea got focus.
- CountDownLatch latch = new CountDownLatch(1);
- mChild.post(() -> {
- mChild.requestFocus();
- mChild.post(() -> {
- latch.countDown();
- });
+ // nudgeShortcutDirection doesn't match. The focus should stay the same.
+ mView3.requestFocus();
+ assertThat(mView3.isFocused()).isTrue();
+ arguments.putInt(NUDGE_DIRECTION, FOCUS_DOWN);
+ mFocusArea3.performAccessibilityAction(ACTION_NUDGE_SHORTCUT, arguments);
+ assertThat(mView3.isFocused()).isTrue();
+
+ // No nudgeShortcut view in the current FocusArea. The focus should stay the same.
+ mView1.requestFocus();
+ assertThat(mView1.isFocused()).isTrue();
+ arguments.putInt(NUDGE_DIRECTION, FOCUS_RIGHT);
+ mFocusArea1.performAccessibilityAction(ACTION_NUDGE_SHORTCUT, arguments);
+ assertThat(mView1.isFocused()).isTrue();
});
- assertDrawMethodsCalled(latch);
+ }
+
+
+ @Test
+ public void testPerformAccessibilityAction_actionFocus() {
+ mFocusArea1.post(() -> {
+ mFocusArea1.performAccessibilityAction(ACTION_FOCUS, null);
+ assertThat(mView1.isFocused()).isTrue();
+
+ // It should focus on the default or the first view in the FocusArea.
+ mFocusArea2.performAccessibilityAction(ACTION_FOCUS, null);
+ assertThat(mDefaultFocus2.isFocused()).isTrue();
+ });
}
@Test
- public void testFocusOnDefaultFocus() throws Exception {
- assertThat(mDefaultFocus.isFocused()).isFalse();
+ public void testPerformAccessibilityAction_actionFocus_enabledFocusCache() {
+ mFocusArea1.post(() -> {
+ RotaryCache cache =
+ new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+ mFocusArea1.setRotaryCache(cache);
- Bundle bundle = new Bundle();
- CountDownLatch latch = new CountDownLatch(1);
- mFocusArea.post(() -> {
- mFocusArea.performAccessibilityAction(ACTION_FOCUS, bundle);
- latch.countDown();
+ mButton1.requestFocus();
+ assertThat(mButton1.isFocused()).isTrue();
+ mView2.requestFocus();
+ assertThat(mView2.isFocused()).isTrue();
+
+ // With cache, it should focus on the lastly focused view in the FocusArea.
+ mFocusArea1.performAccessibilityAction(ACTION_FOCUS, null);
+ assertThat(mButton1.isFocused()).isTrue();
});
- latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS);
- assertThat(mDefaultFocus.isFocused()).isTrue();
+ }
+
+ @Test
+ public void testPerformAccessibilityAction_actionFocus_disabledFocusCache() {
+ mFocusArea1.post(() -> {
+ RotaryCache cache = new RotaryCache(CACHE_TYPE_DISABLED, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+ mFocusArea1.setRotaryCache(cache);
+
+ mButton1.requestFocus();
+ assertThat(mButton1.isFocused()).isTrue();
+ mView2.requestFocus();
+ assertThat(mView2.isFocused()).isTrue();
+
+ // Without cache, it should focus on the default or the first view in the FocusArea.
+ mFocusArea1.performAccessibilityAction(ACTION_FOCUS, null);
+ assertThat(mView1.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testPerformAccessibilityAction_actionFocus_lastFocusedViewRemoved() {
+ mFocusArea1.post(() -> {
+ // Focus on mDefaultFocus2 in mFocusArea2, then mView1 in mFocusArea21.
+ mDefaultFocus2.requestFocus();
+ assertThat(mDefaultFocus2.isFocused()).isTrue();
+ mView1.requestFocus();
+ assertThat(mView1.isFocused()).isTrue();
+
+ // Remove mDefaultFocus2, then Perform ACTION_FOCUS on mFocusArea2.
+ mFocusArea2.removeView(mDefaultFocus2);
+ mFocusArea2.performAccessibilityAction(ACTION_FOCUS, null);
+
+ // mView2 in mFocusArea2 should get focused.
+ assertThat(mView2.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testPerformAccessibilityAction_actionNudgeToAnotherFocusArea_enabledCache() {
+ mFocusArea1.post(() -> {
+ RotaryCache cache1 =
+ new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+ mFocusArea1.setRotaryCache(cache1);
+ RotaryCache cache2 =
+ new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+ mFocusArea2.setRotaryCache(cache2);
+
+ // Focus on the second view in mFocusArea1, then nudge to mFocusArea2.
+ mButton1.requestFocus();
+ assertThat(mButton1.isFocused()).isTrue();
+ Bundle arguments = new Bundle();
+ arguments.putInt(NUDGE_DIRECTION, FOCUS_DOWN);
+ mFocusArea2.performAccessibilityAction(ACTION_FOCUS, arguments);
+ assertThat(mDefaultFocus2.isFocused()).isTrue();
+
+ // Nudge back. It should focus on the cached view (mButton1) in the cached
+ // FocusArea (mFocusArea1).
+ arguments.putInt(NUDGE_DIRECTION, FOCUS_UP);
+ mFocusArea2.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+ assertThat(mButton1.isFocused()).isTrue();
+
+ // Nudge back. It should fail and the focus should stay the same because of one-way
+ // nudge history.
+ arguments.putInt(NUDGE_DIRECTION, FOCUS_DOWN);
+ mFocusArea1.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+ assertThat(mButton1.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testPerformAccessibilityAction_actionNudgeToAnotherFocusArea_mixedCache() {
+ mFocusArea1.post(() -> {
+ // Disabled FocusCache but enabled FocusAreaCache.
+ RotaryCache cache1 =
+ new RotaryCache(CACHE_TYPE_DISABLED, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+ mFocusArea1.setRotaryCache(cache1);
+ RotaryCache cache2 =
+ new RotaryCache(CACHE_TYPE_DISABLED, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+ mFocusArea2.setRotaryCache(cache2);
+
+ // Focus on the second view in mFocusArea1, then nudge to mFocusArea2.
+ mButton1.requestFocus();
+ assertThat(mButton1.isFocused()).isTrue();
+ Bundle arguments = new Bundle();
+ arguments.putInt(NUDGE_DIRECTION, FOCUS_DOWN);
+ mFocusArea2.performAccessibilityAction(ACTION_FOCUS, arguments);
+ assertThat(mDefaultFocus2.isFocused()).isTrue();
+
+ // Nudge back. Since FocusCache is disabled, it should focus on the default or the first
+ // view (mView1) in the cached FocusArea (mFocusArea1).
+ arguments.putInt(NUDGE_DIRECTION, FOCUS_UP);
+ mFocusArea2.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+ assertThat(mView1.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testPerformAccessibilityAction_actionNudgeToAnotherFocusArea_mixedCache2() {
+ mFocusArea1.post(() -> {
+ // Enabled FocusCache but disabled FocusAreaCache.
+ RotaryCache cache1 =
+ new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_DISABLED, 0);
+ mFocusArea1.setRotaryCache(cache1);
+ RotaryCache cache2 =
+ new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_DISABLED, 0);
+ mFocusArea2.setRotaryCache(cache2);
+
+ // Focus on the second view in mFocusArea1, then nudge to mFocusArea2.
+ mButton1.requestFocus();
+ assertThat(mButton1.isFocused()).isTrue();
+ Bundle arguments = new Bundle();
+ arguments.putInt(NUDGE_DIRECTION, FOCUS_DOWN);
+ mFocusArea2.performAccessibilityAction(ACTION_FOCUS, arguments);
+ assertThat(mDefaultFocus2.isFocused()).isTrue();
+
+ // Nudge back. Since FocusAreaCache is disabled, nudge should fail and the focus should
+ // stay the same.
+ arguments.putInt(NUDGE_DIRECTION, FOCUS_UP);
+ mFocusArea2.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+ assertThat(mDefaultFocus2.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testPerformAccessibilityAction_actionNudgeToAnotherFocusArea_specifiedTarget() {
+ mFocusArea1.post(() -> {
+ // Nudge to specified FocusArea.
+ mView4.requestFocus();
+ assertThat(mView4.isFocused()).isTrue();
+ Bundle arguments = new Bundle();
+ arguments.putInt(NUDGE_DIRECTION, FOCUS_LEFT);
+ mFocusArea4.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+ assertThat(mDefaultFocus2.isFocused()).isTrue();
+
+ // Direction doesn't match specified FocusArea. The focus should stay the same.
+ mView4.requestFocus();
+ assertThat(mView4.isFocused()).isTrue();
+ arguments.putInt(NUDGE_DIRECTION, FOCUS_UP);
+ mFocusArea4.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+ assertThat(mView4.isFocused()).isTrue();
+
+ // The FocusArea doesn't specify a target FocusArea. The focus should stay the same.
+ mView1.requestFocus();
+ assertThat(mView1.isFocused()).isTrue();
+ arguments.putInt(NUDGE_DIRECTION, FOCUS_LEFT);
+ mFocusArea1.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+ assertThat(mView1.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testDefaultFocusOverridesHistory_override() {
+ mFocusArea1.post(() -> {
+ RotaryCache cache =
+ new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+ mFocusArea2.setRotaryCache(cache);
+ mFocusArea2.setDefaultFocusOverridesHistory(true);
+
+ mView2.requestFocus();
+ assertThat(mView2.isFocused()).isTrue();
+ mView1.requestFocus();
+ assertThat(mView1.isFocused()).isTrue();
+
+ // The focused view should be the default focus view rather than the cached view.
+ mFocusArea2.performAccessibilityAction(ACTION_FOCUS, null);
+ assertThat(mDefaultFocus2.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testDefaultFocusOverridesHistory_notOverride() {
+ mFocusArea1.post(() -> {
+ RotaryCache cache =
+ new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+ mFocusArea2.setRotaryCache(cache);
+ mFocusArea2.setDefaultFocusOverridesHistory(false);
+
+ mView2.requestFocus();
+ assertThat(mView2.isFocused()).isTrue();
+ mView1.requestFocus();
+ assertThat(mView1.isFocused()).isTrue();
+
+ // The focused view should be the cached view rather than the default focus view.
+ mFocusArea2.performAccessibilityAction(ACTION_FOCUS, null);
+ assertThat(mView2.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testClearFocusAreaHistoryWhenRotating_clear() {
+ mFocusArea1.post(() -> {
+ RotaryCache cache1 =
+ new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+ mFocusArea1.setRotaryCache(cache1);
+ mFocusArea1.setClearFocusAreaHistoryWhenRotating(true);
+ RotaryCache cache2 =
+ new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+ mFocusArea2.setRotaryCache(cache2);
+ mFocusArea2.setClearFocusAreaHistoryWhenRotating(true);
+
+ mView1.requestFocus();
+ assertThat(mView1.isFocused()).isTrue();
+
+ // Nudging down from mFocusArea1 to mFocusArea2.
+ Bundle arguments = new Bundle();
+ arguments.putInt(NUDGE_DIRECTION, FOCUS_DOWN);
+ mFocusArea2.performAccessibilityAction(ACTION_FOCUS, arguments);
+ assertThat(mDefaultFocus2.isFocused()).isTrue();
+ // Rotate.
+ mView2.requestFocus();
+ assertThat(mView2.isFocused()).isTrue();
+ // Since nudge history is cleared, nudging up should fail and the focus should stay
+ // the same.
+ arguments.putInt(NUDGE_DIRECTION, FOCUS_UP);
+ mFocusArea2.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+ assertThat(mView2.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testClearFocusAreaHistoryWhenRotating_notClear() {
+ mFocusArea1.post(() -> {
+ RotaryCache cache1 =
+ new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+ mFocusArea1.setRotaryCache(cache1);
+ mFocusArea1.setClearFocusAreaHistoryWhenRotating(false);
+ RotaryCache cache2 =
+ new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+ mFocusArea2.setRotaryCache(cache2);
+ mFocusArea2.setClearFocusAreaHistoryWhenRotating(false);
+
+ mView1.requestFocus();
+ assertThat(mView1.isFocused()).isTrue();
+
+ // Nudging down from mFocusArea1 to mFocusArea2.
+ Bundle arguments = new Bundle();
+ arguments.putInt(NUDGE_DIRECTION, FOCUS_DOWN);
+ mFocusArea2.performAccessibilityAction(ACTION_FOCUS, arguments);
+ assertThat(mDefaultFocus2.isFocused()).isTrue();
+ // Rotate.
+ mView2.requestFocus();
+ assertThat(mView2.isFocused()).isTrue();
+ // Nudging up should move focus to mFocusArea1 according to nudge history.
+ arguments.putInt(NUDGE_DIRECTION, FOCUS_UP);
+ mFocusArea2.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+ assertThat(mView1.isFocused()).isTrue();
+ });
}
@Test
public void testBoundsOffset() {
- assertThat(mFocusArea.getLayoutDirection()).isEqualTo(LAYOUT_DIRECTION_LTR);
+ assertThat(mFocusArea1.getLayoutDirection()).isEqualTo(LAYOUT_DIRECTION_LTR);
// FocusArea's bounds offset specified in layout file:
// 10dp(start), 20dp(end), 30dp(top), 40dp(bottom).
@@ -133,36 +428,33 @@
int right = dp2Px(20);
int top = dp2Px(30);
int bottom = dp2Px(40);
- AccessibilityNodeInfo node = mFocusArea.createAccessibilityNodeInfo();
+ AccessibilityNodeInfo node = mFocusArea1.createAccessibilityNodeInfo();
assertBoundsOffset(node, left, top, right, bottom);
node.recycle();
}
@Test
- public void testBoundsOffsetWithRtl() throws Exception {
- CountDownLatch latch = new CountDownLatch(1);
- mFocusArea.post(() -> {
- mFocusArea.setLayoutDirection(LAYOUT_DIRECTION_RTL);
- latch.countDown();
- });
- latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS);
- assertThat(mFocusArea.getLayoutDirection()).isEqualTo(LAYOUT_DIRECTION_RTL);
+ public void testBoundsOffsetWithRtl() {
+ mFocusArea1.post(() -> {
+ mFocusArea1.setLayoutDirection(LAYOUT_DIRECTION_RTL);
+ assertThat(mFocusArea1.getLayoutDirection()).isEqualTo(LAYOUT_DIRECTION_RTL);
- // FocusArea highlight padding specified in layout file:
- // 10dp(start), 20dp(end), 30dp(top), 40dp(bottom).
- int left = dp2Px(20);
- int right = dp2Px(10);
- int top = dp2Px(30);
- int bottom = dp2Px(40);
- AccessibilityNodeInfo node = mFocusArea.createAccessibilityNodeInfo();
- assertBoundsOffset(node, left, top, right, bottom);
- node.recycle();
+ // FocusArea highlight padding specified in layout file:
+ // 10dp(start), 20dp(end), 30dp(top), 40dp(bottom).
+ int left = dp2Px(20);
+ int right = dp2Px(10);
+ int top = dp2Px(30);
+ int bottom = dp2Px(40);
+ AccessibilityNodeInfo node = mFocusArea1.createAccessibilityNodeInfo();
+ assertBoundsOffset(node, left, top, right, bottom);
+ node.recycle();
+ });
}
@Test
public void testSetBoundsOffset() {
- mFocusArea.setBoundsOffset(50, 60, 70, 80);
- AccessibilityNodeInfo node = mFocusArea.createAccessibilityNodeInfo();
+ mFocusArea1.setBoundsOffset(50, 60, 70, 80);
+ AccessibilityNodeInfo node = mFocusArea1.createAccessibilityNodeInfo();
assertBoundsOffset(node, 50, 60, 70, 80);
node.recycle();
}
@@ -181,20 +473,37 @@
}
@Test
- public void testLastFocusedViewRemoved() {
- mChild1.post(() -> {
- // Focus on mChild1 in mFocusArea2, then mChild in mFocusArea .
- mChild1.requestFocus();
- assertThat(mChild1.isFocused()).isTrue();
- mChild.requestFocus();
- assertThat(mChild.isFocused()).isTrue();
+ public void testBug170423337() {
+ mFocusArea1.post(() -> {
+ // Focus on app bar (assume mFocusArea1 is app bar).
+ mView1.requestFocus();
- // Remove mChild1 in mFocusArea2, then Perform ACTION_FOCUS on mFocusArea2.
- mFocusArea2.removeView(mChild1);
- mFocusArea2.performAccessibilityAction(ACTION_FOCUS, null);
+ // Nudge down to browse list (assume mFocusArea2 is browse list).
+ Bundle arguments = new Bundle();
+ arguments.putInt(NUDGE_DIRECTION, FOCUS_DOWN);
+ mFocusArea2.performAccessibilityAction(ACTION_FOCUS, arguments);
+ assertThat(mDefaultFocus2.isFocused()).isTrue();
- // mChild2 in mFocusArea2 should get focused.
- assertThat(mChild2.isFocused()).isTrue();
+ // Nudge down to playback control bar (assume mFocusArea3 is playback control bar).
+ mFocusArea3.performAccessibilityAction(ACTION_FOCUS, arguments);
+ assertThat(mView3.isFocused()).isTrue();
+
+ // Nudge down to navigation bar (navigation bar is in system window without FocusAreas).
+ mFpv.performAccessibilityAction(ACTION_FOCUS, null);
+
+ // Nudge up to playback control bar.
+ arguments.putInt(NUDGE_DIRECTION, FOCUS_UP);
+ mFocusArea3.performAccessibilityAction(ACTION_FOCUS, arguments);
+ assertThat(mView3.isFocused()).isTrue();
+
+ // Nudge up to browse list.
+ arguments.putInt(NUDGE_DIRECTION, FOCUS_UP);
+ mFocusArea3.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+ assertThat(mDefaultFocus2.isFocused()).isTrue();
+
+ // Nudge up, and it should focus on app bar.
+ mFocusArea2.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+ assertThat(mView1.isFocused()).isTrue();
});
}
@@ -212,9 +521,10 @@
return (int) (dp * mActivity.getResources().getDisplayMetrics().density + 0.5f);
}
- private void assertDrawMethodsCalled(CountDownLatch latch) throws Exception {
+ private void assertDrawMethodsCalled(@NonNull TestFocusArea focusArea, CountDownLatch latch)
+ throws Exception {
latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS);
- assertThat(mFocusArea.onDrawCalled()).isTrue();
- assertThat(mFocusArea.drawCalled()).isTrue();
+ assertThat(focusArea.onDrawCalled()).isTrue();
+ assertThat(focusArea.drawCalled()).isTrue();
}
}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusAreaTouchModeTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusAreaTouchModeTest.java
new file mode 100644
index 0000000..5c06f84
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusAreaTouchModeTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.ui;
+
+import static android.view.View.FOCUS_RIGHT;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.graphics.Rect;
+import android.view.View;
+
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.car.ui.test.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+/** Unit tests for {@link FocusArea} in touch mode. */
+public class FocusAreaTouchModeTest {
+ @Rule
+ public ActivityTestRule<FocusAreaTestActivity> mActivityRule =
+ new ActivityTestRule<>(FocusAreaTestActivity.class, /* initialTouchMode= */ true);
+
+ private FocusAreaTestActivity mActivity;
+ private TestFocusArea mFocusArea2;
+ private View mView1;
+
+ @Before
+ public void setUp() {
+ mActivity = mActivityRule.getActivity();
+ mFocusArea2 = mActivity.findViewById(R.id.focus_area2);
+ mView1 = mActivity.findViewById(R.id.view1);
+ }
+
+ @Test
+ public void testOnRequestFocusInDescendants_doesNothing() {
+ mFocusArea2.post(() -> {
+ Rect previouslyFocusedRect = new Rect();
+ previouslyFocusedRect.left = mView1.getLeft();
+ previouslyFocusedRect.top = mView1.getTop();
+ previouslyFocusedRect.right = previouslyFocusedRect.left + mView1.getWidth();
+ previouslyFocusedRect.bottom = previouslyFocusedRect.top + mView1.getHeight();
+ boolean focusTaken =
+ mFocusArea2.onRequestFocusInDescendants(FOCUS_RIGHT, previouslyFocusedRect);
+
+ assertWithMessage("onRequestFocusInDescendants returned").that(focusTaken).isFalse();
+ assertWithMessage("No view should be focused")
+ .that(mFocusArea2.getRootView().findFocus()).isNull();
+ });
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusParkingViewTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusParkingViewTest.java
index 887ac8e..e706ae1 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusParkingViewTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusParkingViewTest.java
@@ -16,64 +16,242 @@
package com.android.car.ui;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS;
+
+import static com.android.car.ui.utils.RotaryConstants.ACTION_RESTORE_DEFAULT_FOCUS;
+
import static com.google.common.truth.Truth.assertThat;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
import androidx.test.rule.ActivityTestRule;
+import com.android.car.ui.recyclerview.TestContentLimitingAdapter;
import com.android.car.ui.test.R;
+import com.android.car.ui.utils.CarUiUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/** Unit test for {@link FocusParkingView}. */
+/** Unit test for {@link FocusParkingView} not in touch mode. */
public class FocusParkingViewTest {
- private static final long WAIT_TIME_MS = 3000;
+ private static final int NUM_ITEMS = 40;
@Rule
public ActivityTestRule<FocusParkingViewTestActivity> mActivityRule =
new ActivityTestRule<>(FocusParkingViewTestActivity.class);
private FocusParkingViewTestActivity mActivity;
+ private FocusParkingView mFpv;
+ private ViewGroup mParent1;
+ private View mView1;
+ private View mFocusedByDefault;
+ private RecyclerView mList;
@Before
public void setUp() {
mActivity = mActivityRule.getActivity();
+ mFpv = mActivity.findViewById(R.id.fpv);
+ mParent1 = mActivity.findViewById(R.id.parent1);
+ mView1 = mActivity.findViewById(R.id.view1);
+ mFocusedByDefault = mActivity.findViewById(R.id.focused_by_default);
+ mList = mActivity.findViewById(R.id.list);
+
+ mList.post(() -> {
+ mList.setLayoutManager(new LinearLayoutManager(mActivity));
+ mList.setAdapter(new TestContentLimitingAdapter(NUM_ITEMS));
+ CarUiUtils.setRotaryScrollEnabled(mList, /* isVertical= */ true);
+ });
}
@Test
- public void testFocusParkingViewCanTakeFocus() throws Exception {
- FocusParkingView focusParkingView = mActivity.findViewById(R.id.focus_parking);
-
- CountDownLatch latch = new CountDownLatch(1);
- focusParkingView.post(() -> {
- focusParkingView.requestFocus();
- focusParkingView.post(() -> {
- latch.countDown();
- });
- });
- latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS);
-
- assertThat(focusParkingView.isFocused()).isTrue();
+ public void testGetWidthAndHeight() {
+ assertThat(mFpv.getWidth()).isEqualTo(1);
+ assertThat(mFpv.getHeight()).isEqualTo(1);
}
+
@Test
- public void testFocusParkingViewFocusedWhenWindowLostFocus() throws Exception {
- FocusParkingView focusParkingView = mActivity.findViewById(R.id.focus_parking);
- assertThat(focusParkingView.isFocused()).isFalse();
+ public void testRequestFocus_focusOnDefaultFocus() {
+ mFpv.post(() -> {
+ mView1.requestFocus();
+ assertThat(mView1.isFocused()).isTrue();
- CountDownLatch latch = new CountDownLatch(1);
- focusParkingView.post(() -> {
- focusParkingView.onWindowFocusChanged(false);
- focusParkingView.post(() -> {
- latch.countDown();
- });
+ mFpv.requestFocus();
+ assertThat(mFocusedByDefault.isFocused()).isTrue();
});
- latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS);
+ }
- assertThat(focusParkingView.isFocused()).isTrue();
+ @Test
+ public void testRestoreDefaultFocus_focusOnDefaultFocus() {
+ mFpv.post(() -> {
+ mView1.requestFocus();
+ assertThat(mView1.isFocused()).isTrue();
+
+ mFpv.restoreDefaultFocus();
+ assertThat(mFocusedByDefault.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testOnWindowFocusChanged_loseFocus() {
+ mFpv.post(() -> {
+ mView1.requestFocus();
+ assertThat(mView1.isFocused()).isTrue();
+
+ mFpv.onWindowFocusChanged(false);
+ assertThat(mFpv.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testOnWindowFocusChanged_focusOnDefaultFocus() {
+ mFpv.post(() -> {
+ mFpv.performAccessibilityAction(ACTION_FOCUS, null);
+ assertThat(mFpv.isFocused()).isTrue();
+
+ mFpv.onWindowFocusChanged(true);
+ assertThat(mFocusedByDefault.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testPerformAccessibilityAction_actionRestoreDefaultFocus() {
+ mFpv.post(() -> {
+ mView1.requestFocus();
+ assertThat(mView1.isFocused()).isTrue();
+
+ mFpv.performAccessibilityAction(ACTION_RESTORE_DEFAULT_FOCUS, null);
+ assertThat(mFocusedByDefault.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testPerformAccessibilityAction_actionFocus() {
+ mFpv.post(() -> {
+ mView1.requestFocus();
+ assertThat(mView1.isFocused()).isTrue();
+
+ mFpv.performAccessibilityAction(ACTION_FOCUS, null);
+ assertThat(mFpv.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testRestoreFocusInRoot_recyclerViewItemRemoved() {
+ mList.post(() -> mList.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mList.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ View firstItem = mList.getLayoutManager().findViewByPosition(0);
+ firstItem.requestFocus();
+ assertThat(firstItem.isFocused()).isTrue();
+
+ ViewGroup parent = (ViewGroup) firstItem.getParent();
+ parent.removeView(firstItem);
+ assertThat(mFocusedByDefault.isFocused()).isTrue();
+ }
+ })
+ );
+ }
+
+ @Test
+ public void testRestoreFocusInRoot_recyclerViewItemScrolledOffScreen() {
+ mList.post(() -> mList.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mList.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ View firstItem = mList.getLayoutManager().findViewByPosition(0);
+ firstItem.requestFocus();
+ assertThat(firstItem.isFocused()).isTrue();
+
+ mList.scrollToPosition(NUM_ITEMS - 1);
+ mList.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mList.getViewTreeObserver()
+ .removeOnGlobalLayoutListener(this);
+ assertThat(mList.isFocused()).isTrue();
+ }
+ });
+ }
+ }));
+ }
+
+ @Test
+ public void testRestoreFocusInRoot_focusedViewRemoved() {
+ mFpv.post(() -> {
+ mView1.requestFocus();
+ assertThat(mView1.isFocused()).isTrue();
+
+ ViewGroup parent = (ViewGroup) mView1.getParent();
+ parent.removeView(mView1);
+ assertThat(mFocusedByDefault.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testRestoreFocusInRoot_focusedViewDisabled() {
+ mFpv.post(() -> {
+ mView1.requestFocus();
+ assertThat(mView1.isFocused()).isTrue();
+
+ mView1.setEnabled(false);
+ assertThat(mFocusedByDefault.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testRestoreFocusInRoot_focusedViewBecomesInvisible() {
+ mFpv.post(() -> {
+ mView1.requestFocus();
+ assertThat(mView1.isFocused()).isTrue();
+
+ mView1.setVisibility(View.INVISIBLE);
+ assertThat(mFocusedByDefault.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testRestoreFocusInRoot_focusedViewParentBecomesInvisible() {
+ mFpv.post(() -> {
+ mView1.requestFocus();
+ assertThat(mView1.isFocused()).isTrue();
+
+ mParent1.setVisibility(View.INVISIBLE);
+ assertThat(mFocusedByDefault.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testRequestFocus_focusesFpvWhenShouldRestoreFocusIsFalse() {
+ mFpv.post(() -> {
+ mView1.requestFocus();
+ assertThat(mView1.isFocused()).isTrue();
+ mFpv.setShouldRestoreFocus(false);
+
+ mFpv.requestFocus();
+ assertThat(mFpv.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testRestoreDefaultFocus_focusesFpvWhenShouldRestoreFocusIsFalse() {
+ mFpv.post(() -> {
+ mView1.requestFocus();
+ assertThat(mView1.isFocused()).isTrue();
+ mFpv.setShouldRestoreFocus(false);
+
+ mFpv.restoreDefaultFocus();
+ assertThat(mFpv.isFocused()).isTrue();
+ });
}
}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusParkingViewTouchModeTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusParkingViewTouchModeTest.java
new file mode 100644
index 0000000..d872bb6
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusParkingViewTouchModeTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.ui;
+
+import static com.android.car.ui.utils.RotaryConstants.ACTION_RESTORE_DEFAULT_FOCUS;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.view.View;
+
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.car.ui.test.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+/** Unit test for {@link FocusParkingView} in touch mode. */
+public class FocusParkingViewTouchModeTest {
+
+ @Rule
+ public ActivityTestRule<FocusParkingViewTestActivity> mActivityRule =
+ new ActivityTestRule<>(FocusParkingViewTestActivity.class,
+ /* initialTouchMode= */ true);
+
+ private FocusParkingView mFpv;
+
+ @Before
+ public void setUp() {
+ FocusParkingViewTestActivity activity = mActivityRule.getActivity();
+ mFpv = activity.findViewById(R.id.fpv);
+ }
+
+ @Test
+ public void testRestoreDefaultFocus_doesNothing() {
+ mFpv.post(() -> {
+ assertThat(mFpv.getRootView().findFocus()).isNull();
+
+ boolean result = mFpv.restoreDefaultFocus();
+
+ assertWithMessage("restoreDefaultFocus returned").that(result).isFalse();
+ assertWithMessage("No view should be focused")
+ .that(mFpv.getRootView().findFocus()).isNull();
+ });
+ }
+
+ @Test
+ public void testRequestFocus_doesNothing() {
+ mFpv.post(() -> {
+ assertThat(mFpv.getRootView().findFocus()).isNull();
+
+ boolean result = mFpv.requestFocus(View.FOCUS_DOWN, /* previouslyFocusedRect= */ null);
+
+ assertWithMessage("requestFocus returned").that(result).isFalse();
+ assertWithMessage("No view should be focused")
+ .that(mFpv.getRootView().findFocus()).isNull();
+ });
+ }
+
+ @Test
+ public void testPerformActionRestoreDefaultFocus_exitsTouchMode() {
+ mFpv.post(() -> {
+ assertThat(mFpv.getRootView().findFocus()).isNull();
+
+ boolean result = mFpv.performAccessibilityAction(
+ ACTION_RESTORE_DEFAULT_FOCUS, /* arguments= */ null);
+
+ assertWithMessage("performAccessibilityAction returned").that(result).isTrue();
+ assertWithMessage("A view should be focused")
+ .that(mFpv.getRootView().findFocus()).isNotNull();
+ });
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/RotaryCacheTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/RotaryCacheTest.java
new file mode 100644
index 0000000..b1b9b59
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/RotaryCacheTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.ui;
+
+import static com.android.car.ui.RotaryCache.CACHE_TYPE_EXPIRED_AFTER_SOME_TIME;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link RotaryCache}. */
+@RunWith(AndroidJUnit4.class)
+public class RotaryCacheTest {
+ private static final int CACHE_TIME_OUT_MS = 10000;
+
+ private RotaryCache mRotaryCache;
+ private long mValidTime;
+ private long mExpiredTime;
+ private Context mContext;
+ private FocusArea mFocusArea;
+ private View mFocusedView;
+
+ @Before
+ public void setUp() {
+ mRotaryCache = new RotaryCache(CACHE_TYPE_EXPIRED_AFTER_SOME_TIME, CACHE_TIME_OUT_MS,
+ CACHE_TYPE_EXPIRED_AFTER_SOME_TIME, CACHE_TIME_OUT_MS);
+ mValidTime = CACHE_TIME_OUT_MS - 1;
+ mExpiredTime = CACHE_TIME_OUT_MS + 1;
+ mContext = ApplicationProvider.getApplicationContext();
+ mFocusArea = new FocusArea(mContext);
+ mFocusedView = new View(mContext);
+ }
+
+ @Test
+ public void testGetFocusedView_inTheCache() {
+ mRotaryCache.saveFocusedView(mFocusedView, 0);
+ View view = mRotaryCache.getFocusedView(mValidTime);
+ assertThat(view).isEqualTo(mFocusedView);
+ }
+
+ @Test
+ public void testGetFocusedView_notInTheCache() {
+ View view = mRotaryCache.getFocusedView(mValidTime);
+ assertThat(view).isNull();
+ }
+
+ @Test
+ public void testGetFocusedView_expiredCache() {
+ mRotaryCache.saveFocusedView(mFocusedView, 0);
+ View view = mRotaryCache.getFocusedView(mExpiredTime);
+ assertThat(view).isNull();
+ }
+
+ @Test
+ public void testGetCachedFocusArea_inTheCache() {
+ int direction = View.FOCUS_LEFT;
+ mRotaryCache.saveFocusArea(direction, mFocusArea, 0);
+ FocusArea focusArea = mRotaryCache.getCachedFocusArea(direction, mValidTime);
+ assertThat(focusArea).isEqualTo(mFocusArea);
+ }
+
+ @Test
+ public void testGetCachedFocusArea_notInTheCache() {
+ int direction = View.FOCUS_LEFT;
+ mRotaryCache.saveFocusArea(direction, mFocusArea, 0);
+
+ FocusArea focusArea = mRotaryCache.getCachedFocusArea(View.FOCUS_RIGHT, mValidTime);
+ assertThat(focusArea).isNull();
+ focusArea = mRotaryCache.getCachedFocusArea(View.FOCUS_UP, mValidTime);
+ assertThat(focusArea).isNull();
+ }
+
+ @Test
+ public void testGetCachedFocusArea_expiredCache() {
+ int direction = View.FOCUS_LEFT;
+ mRotaryCache.saveFocusArea(direction, mFocusArea, 0);
+ FocusArea focusArea = mRotaryCache.getCachedFocusArea(direction, mExpiredTime);
+ assertThat(focusArea).isNull();
+ }
+
+ @Test
+ public void testClearFocusAreaHistory() {
+ mRotaryCache.saveFocusArea(View.FOCUS_UP, mFocusArea, 0);
+ assertThat(mRotaryCache.getCachedFocusArea(View.FOCUS_UP, mValidTime)).isEqualTo(
+ mFocusArea);
+
+ mRotaryCache.clearFocusAreaHistory();
+ assertThat(mRotaryCache.getCachedFocusArea(View.FOCUS_UP, mValidTime)).isNull();
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenControllerTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenControllerTest.java
new file mode 100644
index 0000000..11d33f4
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenControllerTest.java
@@ -0,0 +1,171 @@
+/*
+ * 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.ui.imewidescreen;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.assertThat;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.REQUEST_RENDER_CONTENT_AREA;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.WIDE_SCREEN_ACTION;
+
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.inputmethodservice.ExtractEditText;
+import android.inputmethodservice.InputMethodService;
+import android.inputmethodservice.InputMethodService.Insets;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.Window;
+import android.widget.FrameLayout;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.car.ui.test.R;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for {@link CarUiImeWideScreenController}.
+ */
+public class CarUiImeWideScreenControllerTest {
+
+ private Context mContext = ApplicationProvider.getApplicationContext();
+
+ @Mock
+ Context mMockContext;
+
+ @Mock
+ InputMethodService mInputMethodService;
+
+ @Mock
+ Dialog mDialog;
+
+ @Mock
+ Window mWindow;
+
+ private CarUiImeWideScreenTestActivity mActivity;
+
+ @Rule
+ public ActivityTestRule<CarUiImeWideScreenTestActivity> mActivityRule =
+ new ActivityTestRule<>(CarUiImeWideScreenTestActivity.class);
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mActivity = mActivityRule.getActivity();
+ }
+
+ @After
+ public void destroy() {
+ mActivity.finish();
+ }
+
+ @Test
+ public void createWideScreenImeView_shouldWrapTheViewInTemplate() {
+ // make sure view is wrapped in the template.
+ assertNotNull(mActivity.findViewById(R.id.test_ime_input_view_id));
+
+ // check all views in template default visibility.
+ onView(withId(R.id.car_ui_wideScreenDescriptionTitle)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.car_ui_wideScreenDescription)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.car_ui_inputExtractActionAutomotive)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.car_ui_wideScreenSearchResultList)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.car_ui_wideScreenErrorMessage)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.car_ui_wideScreenError)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.car_ui_contentAreaAutomotive)).check(matches(not(isDisplayed())));
+
+ onView(withId(R.id.car_ui_wideScreenExtractedTextIcon)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_wideScreenClearData)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_fullscreenArea)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_inputExtractEditTextContainer)).check(matches(isDisplayed()));
+
+ // check if the click listener is installed on the image to clear data.
+ View clearDataIcon = mActivity.findViewById(R.id.car_ui_wideScreenClearData);
+ assertTrue(clearDataIcon.hasOnClickListeners());
+ }
+
+ @Test
+ public void onComputeInsets_showContentArea_shouldUpdateEntireAreaAsTouchable() {
+ when(mInputMethodService.getWindow()).thenReturn(mDialog);
+ when(mDialog.getWindow()).thenReturn(mWindow);
+ View view = new FrameLayout(mContext);
+ view.setTop(0);
+ view.setBottom(200);
+ when(mWindow.getDecorView()).thenReturn(view);
+
+ InputMethodService.Insets outInsets = new Insets();
+ CarUiImeWideScreenController carUiImeWideScreenController = getController();
+ carUiImeWideScreenController.onComputeInsets(outInsets);
+
+ assertThat(outInsets.touchableInsets, is(InputMethodService.Insets.TOUCHABLE_INSETS_FRAME));
+ assertThat(outInsets.contentTopInsets, is(200));
+ assertThat(outInsets.visibleTopInsets, is(200));
+ }
+
+ @Test
+ public void onComputeInsets_hideContentArea_shouldUpdateRegionAsTouchable() {
+ when(mInputMethodService.getWindow()).thenReturn(mDialog);
+ when(mDialog.getWindow()).thenReturn(mWindow);
+ View view = new FrameLayout(mContext);
+ view.setTop(0);
+ view.setBottom(200);
+ when(mWindow.getDecorView()).thenReturn(view);
+
+ View imeInputView = LayoutInflater.from(mContext)
+ .inflate(R.layout.test_ime_input_view, null, false);
+ CarUiImeWideScreenController carUiImeWideScreenController = getController();
+ carUiImeWideScreenController.setExtractEditText(new ExtractEditText(mContext));
+ carUiImeWideScreenController.createWideScreenImeView(imeInputView);
+
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(REQUEST_RENDER_CONTENT_AREA, false);
+ carUiImeWideScreenController.onAppPrivateCommand(WIDE_SCREEN_ACTION, bundle);
+
+ InputMethodService.Insets outInsets = new Insets();
+ carUiImeWideScreenController.onComputeInsets(outInsets);
+
+ assertThat(outInsets.touchableInsets,
+ is(InputMethodService.Insets.TOUCHABLE_INSETS_REGION));
+ assertThat(outInsets.contentTopInsets, is(200));
+ assertThat(outInsets.visibleTopInsets, is(200));
+ }
+
+ private CarUiImeWideScreenController getController() {
+ return new CarUiImeWideScreenController(mContext, mInputMethodService) {
+ @Override
+ public boolean isWideScreenMode() {
+ return true;
+ }
+ };
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenTestActivity.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenTestActivity.java
new file mode 100644
index 0000000..8ffd5a2
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenTestActivity.java
@@ -0,0 +1,54 @@
+/*
+ * 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.ui.imewidescreen;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.car.ui.test.R;
+
+/**
+ * An {@link Activity} that mimics a wide screen IME and displays the template for testing.
+ */
+public class CarUiImeWideScreenTestActivity extends Activity {
+ public static CarUiImeWideScreenController sCarUiImeWideScreenController;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.car_ui_ime_wide_screen_test_activity);
+
+ FrameLayout root = findViewById(R.id.test_activity);
+
+ sCarUiImeWideScreenController = new CarUiImeWideScreenController(this, null) {
+ @Override
+ public boolean isWideScreenMode() {
+ return true;
+ }
+ };
+
+ View imeInputView = LayoutInflater.from(this)
+ .inflate(R.layout.test_ime_input_view, null, false);
+
+ View templateView = sCarUiImeWideScreenController.createWideScreenImeView(imeInputView);
+
+ root.addView(templateView);
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/matchers/PaddingMatcher.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/matchers/PaddingMatcher.java
new file mode 100644
index 0000000..5ab1a86
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/matchers/PaddingMatcher.java
@@ -0,0 +1,81 @@
+/*
+ * 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.ui.matchers;
+
+import android.view.View;
+
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+
+public class PaddingMatcher extends TypeSafeMatcher<View> {
+
+ public enum Side {
+ TOP,
+ BOTTOM,
+ LEFT,
+ RIGHT,
+ START,
+ END
+ }
+
+ private Side mSide;
+ private int mMin;
+ private int mMax;
+
+ public PaddingMatcher(Side side, int min, int max) {
+ mSide = side;
+ mMin = min;
+ mMax = max;
+ }
+
+ @Override
+ protected boolean matchesSafely(View item) {
+ int padding = 0;
+ switch (mSide) {
+ case TOP:
+ padding = item.getPaddingTop();
+ break;
+ case BOTTOM:
+ padding = item.getPaddingBottom();
+ break;
+ case LEFT:
+ padding = item.getPaddingLeft();
+ break;
+ case RIGHT:
+ padding = item.getPaddingRight();
+ break;
+ case START:
+ padding = item.getPaddingStart();
+ break;
+ case END:
+ padding = item.getPaddingEnd();
+ break;
+ }
+
+ if (mMin >= 0 && padding < mMin) {
+ return false;
+ }
+
+ return mMax < 0 || padding <= mMax;
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description
+ .appendText("with " + mSide.toString() + " padding between " + mMin + " and " + mMax);
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/matchers/ViewMatchers.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/matchers/ViewMatchers.java
index 74ce4fa..cdef653 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/matchers/ViewMatchers.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/matchers/ViewMatchers.java
@@ -18,6 +18,8 @@
import android.view.View;
+import com.android.car.ui.matchers.PaddingMatcher.Side;
+
import org.hamcrest.Matcher;
public class ViewMatchers {
@@ -32,4 +34,12 @@
public static Matcher<View> withIndex(Matcher<View> matcher, int index) {
return new IndexMatcher(matcher, index);
}
+
+ public static Matcher<View> withPadding(Side side, int exactly) {
+ return new PaddingMatcher(side, exactly, exactly);
+ }
+
+ public static Matcher<View> withPaddingAtLeast(Side side, int min) {
+ return new PaddingMatcher(side, min, -1);
+ }
}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/preference/NonFullscreenPreferenceFragmentTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/preference/NonFullscreenPreferenceFragmentTest.java
new file mode 100644
index 0000000..33aaa18
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/preference/NonFullscreenPreferenceFragmentTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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.ui.preference;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.Espresso.pressBack;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+import static com.android.car.ui.matchers.ViewMatchers.withPadding;
+import static com.android.car.ui.matchers.ViewMatchers.withPaddingAtLeast;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.preference.ListPreference;
+import androidx.preference.MultiSelectListPreference;
+import androidx.preference.PreferenceScreen;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.car.ui.baselayout.Insets;
+import com.android.car.ui.baselayout.InsetsChangedListener;
+import com.android.car.ui.core.CarUi;
+import com.android.car.ui.matchers.PaddingMatcher.Side;
+import com.android.car.ui.toolbar.ToolbarController;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+public class NonFullscreenPreferenceFragmentTest {
+
+ private static final String EXTRA_FULLSCREEN = "fullscreen";
+ private static final String TOOLBAR_DEFAULT_TEXT = "Test!";
+ private static final String PREFERENCE_SCREEN_TITLE = "PreferenceScreen Title";
+ private static final String LIST_PREFERENCE_TITLE = "List Preference";
+ private static final String MULTI_SELECT_LIST_PREFERENCE_TITLE = "MultiSelect List Preference";
+ private static final String BACK_CONTENT_DESCRIPTION = "Back";
+ private static final String[] ITEMS = { "Item 1", "Item 2", "Item 3" };
+
+ @Rule
+ public ActivityScenarioRule<PreferenceTestActivity> mActivityRule =
+ new ActivityScenarioRule<>(PreferenceTestActivity.class);
+
+ @Test
+ public void test_fullscreen_changesTitle() {
+ try (ActivityScenario<MyActivity> scenario =
+ ActivityScenario.launch(MyActivity.newIntent(true))) {
+
+ onView(withText(TOOLBAR_DEFAULT_TEXT)).check(doesNotExist());
+ onView(withText(PREFERENCE_SCREEN_TITLE)).check(matches(isDisplayed()));
+ onView(isAssignableFrom(RecyclerView.class)).check(
+ matches(withPaddingAtLeast(Side.TOP, 1)));
+
+ onView(withText(MULTI_SELECT_LIST_PREFERENCE_TITLE)).perform(click());
+ onView(withText(MULTI_SELECT_LIST_PREFERENCE_TITLE)).check(matches(isDisplayed()));
+ onView(withText(ITEMS[0])).check(matches(isDisplayed()));
+ onView(isAssignableFrom(RecyclerView.class)).check(
+ matches(withPaddingAtLeast(Side.TOP, 1)));
+ onView(withContentDescription(BACK_CONTENT_DESCRIPTION)).perform(click());
+
+ onView(withText(LIST_PREFERENCE_TITLE)).perform(click());
+ onView(withText(LIST_PREFERENCE_TITLE)).check(matches(isDisplayed()));
+ onView(withText(ITEMS[0])).check(matches(isDisplayed()));
+ onView(isAssignableFrom(RecyclerView.class)).check(
+ matches(withPaddingAtLeast(Side.TOP, 1)));
+ onView(withContentDescription(BACK_CONTENT_DESCRIPTION)).perform(click());
+ }
+ }
+
+ @Test
+ public void test_nonFullscreen_doesntChangeTitle() {
+ try (ActivityScenario<MyActivity> scenario =
+ ActivityScenario.launch(MyActivity.newIntent(false))) {
+
+ onView(withText(TOOLBAR_DEFAULT_TEXT)).check(matches(isDisplayed()));
+ onView(withText(PREFERENCE_SCREEN_TITLE)).check(doesNotExist());
+ onView(isAssignableFrom(RecyclerView.class)).check(matches(withPadding(Side.TOP, 0)));
+
+ onView(withText(MULTI_SELECT_LIST_PREFERENCE_TITLE)).perform(click());
+ onView(withText(MULTI_SELECT_LIST_PREFERENCE_TITLE)).check(doesNotExist());
+ onView(withText(TOOLBAR_DEFAULT_TEXT)).check(matches(isDisplayed()));
+ onView(withText(ITEMS[0])).check(matches(isDisplayed()));
+ onView(isAssignableFrom(RecyclerView.class)).check(matches(withPadding(Side.TOP, 0)));
+ onView(withContentDescription(BACK_CONTENT_DESCRIPTION)).check(doesNotExist());
+ pressBack();
+
+ onView(withText(LIST_PREFERENCE_TITLE)).perform(click());
+ onView(withText(LIST_PREFERENCE_TITLE)).check(doesNotExist());
+ onView(withText(TOOLBAR_DEFAULT_TEXT)).check(matches(isDisplayed()));
+ onView(withText(ITEMS[0])).check(matches(isDisplayed()));
+ onView(isAssignableFrom(RecyclerView.class)).check(matches(withPadding(Side.TOP, 0)));
+ onView(withContentDescription(BACK_CONTENT_DESCRIPTION)).check(doesNotExist());
+ pressBack();
+ }
+ }
+
+
+ public static class MyActivity extends AppCompatActivity implements InsetsChangedListener {
+
+ private boolean mIsFullScreen = false;
+
+ public static Intent newIntent(boolean isFullScreen) {
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ Intent intent = new Intent(context, MyActivity.class);
+ intent.putExtra(EXTRA_FULLSCREEN, isFullScreen);
+ return intent;
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ ToolbarController toolbar = CarUi.requireToolbar(this);
+ toolbar.setTitle(TOOLBAR_DEFAULT_TEXT);
+
+ mIsFullScreen = getIntent().getBooleanExtra(EXTRA_FULLSCREEN, true);
+ if (savedInstanceState == null) {
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(android.R.id.content, new MyPreferenceFragment(mIsFullScreen))
+ .commitNow();
+ }
+ }
+
+ @Override
+ public void onCarUiInsetsChanged(@NonNull Insets insets) {
+ if (!mIsFullScreen) {
+ requireViewById(android.R.id.content).setPadding(insets.getLeft(), insets.getTop(),
+ insets.getRight(), insets.getBottom());
+ } else {
+ // No-op marker for the preference fragment to handle it
+ }
+ }
+ }
+
+ public static class MyPreferenceFragment extends PreferenceFragment {
+
+ private final boolean mIsFullScreen;
+
+ public MyPreferenceFragment(boolean isFullScreen) {
+ mIsFullScreen = isFullScreen;
+ }
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ PreferenceScreen screen = getPreferenceManager()
+ .createPreferenceScreen(requireContext());
+
+ ListPreference listPreference = new CarUiListPreference(getContext());
+ listPreference.setTitle(LIST_PREFERENCE_TITLE);
+ listPreference.setKey(LIST_PREFERENCE_TITLE);
+ listPreference.setEntries(ITEMS);
+ listPreference.setEntryValues(new CharSequence[]{"1", "2", "3"});
+
+ MultiSelectListPreference multiSelectListPreference =
+ new CarUiMultiSelectListPreference(getContext());
+ multiSelectListPreference.setTitle(MULTI_SELECT_LIST_PREFERENCE_TITLE);
+ multiSelectListPreference.setKey(MULTI_SELECT_LIST_PREFERENCE_TITLE);
+ multiSelectListPreference.setEntries(ITEMS);
+ multiSelectListPreference.setEntryValues(new CharSequence[]{"1", "2", "3"});
+
+ screen.addPreference(listPreference);
+ screen.addPreference(multiSelectListPreference);
+
+ screen.setTitle(PREFERENCE_SCREEN_TITLE);
+ setPreferenceScreen(screen);
+ }
+
+ @Override
+ protected boolean isFullScreenFragment() {
+ return mIsFullScreen;
+ }
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiListItemTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiListItemTest.java
index bd21173..da446e1 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiListItemTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiListItemTest.java
@@ -29,13 +29,10 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import android.view.View;
-
import androidx.test.rule.ActivityTestRule;
import com.android.car.ui.R;
@@ -43,7 +40,6 @@
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
-import org.mockito.ArgumentCaptor;
import java.util.ArrayList;
import java.util.List;
@@ -242,7 +238,8 @@
CarUiContentListItem.OnClickListener clickListener = mock(
CarUiContentListItem.OnClickListener.class);
- View.OnClickListener supplementalIconClickListener = mock(View.OnClickListener.class);
+ CarUiContentListItem.OnClickListener supplementalIconClickListener = mock(
+ CarUiContentListItem.OnClickListener.class);
CarUiContentListItem item = new CarUiContentListItem(
CarUiContentListItem.Action.ICON);
@@ -262,18 +259,14 @@
// listener.
onView(withId(R.id.title)).perform(click());
verify(clickListener, times(1)).onClick(item);
- verify(supplementalIconClickListener, times(0)).onClick(any());
+ verify(supplementalIconClickListener, times(0)).onClick(item);
- ArgumentCaptor<View> iconCaptor = ArgumentCaptor.forClass(View.class);
onView(withId(R.id.supplemental_icon)).perform(click());
// Check that icon is argument for single call to click listener.
- verify(supplementalIconClickListener, times(1)).onClick(iconCaptor.capture());
+ verify(supplementalIconClickListener, times(1)).onClick(item);
// Verify that the standard click listener wasn't also fired.
verify(clickListener, times(1)).onClick(item);
-
- View icon = mCarUiRecyclerView.findViewById(R.id.supplemental_icon);
- assertEquals(icon, iconCaptor.getValue());
}
@Test
@@ -282,7 +275,8 @@
CarUiContentListItem.OnClickListener mockedItemOnClickListener = mock(
CarUiContentListItem.OnClickListener.class);
- View.OnClickListener mockedIconListener = mock(View.OnClickListener.class);
+ CarUiContentListItem.OnClickListener mockedIconListener = mock(
+ CarUiContentListItem.OnClickListener.class);
CarUiContentListItem item = new CarUiContentListItem(
CarUiContentListItem.Action.ICON);
@@ -306,7 +300,7 @@
// Clicks anywhere on the icon should invoke both listeners.
onView(withId(R.id.action_container)).perform(click());
verify(mockedItemOnClickListener, times(1)).onClick(item);
- verify(mockedIconListener, times(1)).onClick(any(View.class));
+ verify(mockedIconListener, times(1)).onClick(item);
}
@Test
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java
index 79a26f1..ea4a8c3 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java
@@ -41,8 +41,10 @@
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThan;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -159,6 +161,9 @@
when(typedArray.getInt(eq(R.styleable.CarUiRecyclerView_numOfColumns), anyInt()))
.thenReturn(3);
+ // Ensure the CarUiRecyclerViewLayout constant matches the styleable attribute enum value
+ assertEquals(CarUiRecyclerView.CarUiRecyclerViewLayout.GRID, 1);
+
CarUiRecyclerView carUiRecyclerView = new CarUiRecyclerView(mTestableContext);
ViewGroup container = mActivity.findViewById(R.id.test_container);
TestAdapter adapter = new TestAdapter(4);
@@ -203,6 +208,9 @@
when(typedArray.getInt(eq(R.styleable.CarUiRecyclerView_layoutStyle), anyInt()))
.thenReturn(CarUiRecyclerView.CarUiRecyclerViewLayout.LINEAR);
+ // Ensure the CarUiRecyclerViewLayout constant matches the styleable attribute enum value
+ assertEquals(CarUiRecyclerView.CarUiRecyclerViewLayout.LINEAR, 0);
+
CarUiRecyclerView carUiRecyclerView = new CarUiRecyclerView(mTestableContext);
ViewGroup container = mActivity.findViewById(R.id.test_container);
TestAdapter adapter = new TestAdapter(4);
@@ -328,7 +336,14 @@
onView(withId(R.id.list)).check(matches(isDisplayed()));
CarUiRecyclerView carUiRecyclerView = mActivity.requireViewById(R.id.list);
- FixedSizeTestAdapter adapter = new FixedSizeTestAdapter(50, carUiRecyclerView.getHeight());
+
+ // Can't use OrientationHelper here, because it returns 0 when calling getTotalSpace methods
+ // until LayoutManager's onLayoutComplete is called. In this case waiting until the first
+ // item of the list is displayed guarantees that OrientationHelper is initialized properly.
+ int totalSpace = carUiRecyclerView.getHeight()
+ - carUiRecyclerView.getPaddingTop()
+ - carUiRecyclerView.getPaddingBottom();
+ PerfectFitTestAdapter adapter = new PerfectFitTestAdapter(5, totalSpace);
mActivity.runOnUiThread(() -> carUiRecyclerView.setAdapter(adapter));
IdlingRegistry.getInstance().register(new ScrollIdlingResource(carUiRecyclerView));
@@ -337,6 +352,9 @@
LinearLayoutManager layoutManager =
(LinearLayoutManager) carUiRecyclerView.getLayoutManager();
+ OrientationHelper orientationHelper = OrientationHelper.createVerticalHelper(layoutManager);
+ assertEquals(totalSpace, orientationHelper.getTotalSpace());
+
// Move down one page so there will be sufficient pages for up and downs.
onView(withId(R.id.car_ui_scrollbar_page_down)).perform(click());
@@ -507,46 +525,48 @@
});
IdlingRegistry.getInstance().register(new ScrollIdlingResource(carUiRecyclerView));
+ onView(withText(adapter.getItemText(0))).check(matches(isDisplayed()));
OrientationHelper orientationHelper =
OrientationHelper.createVerticalHelper(carUiRecyclerView.getLayoutManager());
-
- int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels;
+ int screenHeight = orientationHelper.getTotalSpace();
// Scroll to a position where long item is partially visible.
// Scrolling from top, scrollToPosition() aligns the pos-1 item to bottom.
onView(withId(R.id.list)).perform(scrollToPosition(longItemPosition - 1));
// Scroll by half the height of the screen so the long item is partially visible.
mActivity.runOnUiThread(() -> carUiRecyclerView.scrollBy(0, screenHeight / 2));
-
- onView(withText(adapter.getItemText(longItemPosition))).check(matches(isDisplayed()));
+ // This is needed to make sure scroll is finished before looking for the long item.
+ onView(withText(adapter.getItemText(longItemPosition - 1))).check(matches(isDisplayed()));
// Verify long item is partially shown.
View longItem = getLongItem(carUiRecyclerView);
assertThat(
orientationHelper.getDecoratedStart(longItem),
- is(greaterThan(carUiRecyclerView.getTop())));
+ is(greaterThan(orientationHelper.getStartAfterPadding())));
onView(withId(R.id.car_ui_scrollbar_page_down)).perform(click());
// Verify long item is snapped to top.
- assertThat(orientationHelper.getDecoratedStart(longItem), is(equalTo(0)));
+ assertThat(orientationHelper.getDecoratedStart(longItem),
+ is(equalTo(orientationHelper.getStartAfterPadding())));
assertThat(orientationHelper.getDecoratedEnd(longItem),
- is(greaterThan(carUiRecyclerView.getBottom())));
+ is(greaterThan(orientationHelper.getEndAfterPadding())));
// Set a limit to avoid test stuck in non-moving state.
- while (orientationHelper.getDecoratedEnd(longItem) > carUiRecyclerView.getBottom()) {
+ while (orientationHelper.getDecoratedEnd(longItem)
+ > orientationHelper.getEndAfterPadding()) {
onView(withId(R.id.car_ui_scrollbar_page_down)).perform(click());
}
// Verify long item end is aligned to bottom.
assertThat(orientationHelper.getDecoratedEnd(longItem),
- is(equalTo(carUiRecyclerView.getHeight())));
+ is(equalTo(orientationHelper.getEndAfterPadding())));
onView(withId(R.id.car_ui_scrollbar_page_down)).perform(click());
// Verify that the long item is no longer visible; Should be on the next child
assertThat(
orientationHelper.getDecoratedStart(longItem),
- is(lessThan(carUiRecyclerView.getTop())));
+ is(lessThan(orientationHelper.getStartAfterPadding())));
}
@Test
@@ -575,6 +595,7 @@
});
IdlingRegistry.getInstance().register(new ScrollIdlingResource(carUiRecyclerView));
+ onView(withText(adapter.getItemText(0))).check(matches(isDisplayed()));
OrientationHelper orientationHelper =
OrientationHelper.createVerticalHelper(carUiRecyclerView.getLayoutManager());
@@ -589,9 +610,8 @@
View longItem = getLongItem(carUiRecyclerView);
// Making sure we've reached end of the recyclerview, after
// adding bottom padding
- assertThat(orientationHelper.getDecoratedEnd(longItem)
- + carUiRecyclerView.getPaddingBottom(),
- is(equalTo(carUiRecyclerView.getHeight())));
+ assertThat(orientationHelper.getDecoratedEnd(longItem),
+ is(equalTo(orientationHelper.getEndAfterPadding())));
}
@Test
@@ -615,6 +635,7 @@
});
IdlingRegistry.getInstance().register(new ScrollIdlingResource(carUiRecyclerView));
+ onView(withText(adapter.getItemText(0))).check(matches(isDisplayed()));
OrientationHelper orientationHelper =
OrientationHelper.createVerticalHelper(carUiRecyclerView.getLayoutManager());
@@ -624,27 +645,25 @@
// Verify long item is off-screen.
View longItem = getLongItem(carUiRecyclerView);
+
assertThat(
orientationHelper.getDecoratedEnd(longItem),
- is(greaterThan(carUiRecyclerView.getTop())));
+ is(lessThanOrEqualTo(orientationHelper.getEndAfterPadding())));
- onView(withId(R.id.car_ui_scrollbar_page_up)).perform(click());
-
- // Verify long item is snapped to bottom.
- assertThat(orientationHelper.getDecoratedEnd(longItem),
- is(equalTo(carUiRecyclerView.getHeight())));
- assertThat(orientationHelper.getDecoratedStart(longItem), is(lessThan(0)));
-
-
- int decoratedStart = orientationHelper.getDecoratedStart(longItem);
-
- while (decoratedStart < 0) {
+ if (orientationHelper.getStartAfterPadding() - orientationHelper.getDecoratedStart(longItem)
+ < orientationHelper.getTotalSpace()) {
onView(withId(R.id.car_ui_scrollbar_page_up)).perform(click());
- decoratedStart = orientationHelper.getDecoratedStart(longItem);
- }
+ assertThat(orientationHelper.getDecoratedStart(longItem),
+ is(greaterThanOrEqualTo(orientationHelper.getStartAfterPadding())));
+ } else {
+ int topBeforeClick = orientationHelper.getDecoratedStart(longItem);
- // Verify long item top is aligned to top.
- assertThat(orientationHelper.getDecoratedStart(longItem), is(equalTo(0)));
+ onView(withId(R.id.car_ui_scrollbar_page_up)).perform(click());
+
+ // Verify we scrolled 1 screen
+ assertThat(orientationHelper.getStartAfterPadding() - topBeforeClick,
+ is(equalTo(orientationHelper.getTotalSpace())));
+ }
}
@Test
@@ -668,6 +687,7 @@
});
IdlingRegistry.getInstance().register(new ScrollIdlingResource(carUiRecyclerView));
+ onView(withText(adapter.getItemText(0))).check(matches(isDisplayed()));
OrientationHelper orientationHelper =
OrientationHelper.createVerticalHelper(carUiRecyclerView.getLayoutManager());
@@ -685,20 +705,21 @@
View longItem = getLongItem(carUiRecyclerView);
assertThat(
orientationHelper.getDecoratedStart(longItem),
- is(greaterThan(carUiRecyclerView.getTop())));
+ is(greaterThan(orientationHelper.getStartAfterPadding())));
onView(withId(R.id.car_ui_scrollbar_page_down)).perform(click());
// Verify long item is snapped to top.
- assertThat(orientationHelper.getDecoratedStart(longItem), is(equalTo(0)));
+ assertThat(orientationHelper.getDecoratedStart(longItem),
+ is(equalTo(orientationHelper.getStartAfterPadding())));
assertThat(orientationHelper.getDecoratedEnd(longItem),
- is(greaterThan(carUiRecyclerView.getBottom())));
+ is(greaterThan(orientationHelper.getEndAfterPadding())));
onView(withId(R.id.car_ui_scrollbar_page_down)).perform(click());
// Verify long item does not snap to bottom.
assertThat(orientationHelper.getDecoratedEnd(longItem),
- not(equalTo(carUiRecyclerView.getHeight())));
+ not(equalTo(orientationHelper.getEndAfterPadding())));
}
@Test
@@ -727,6 +748,7 @@
});
IdlingRegistry.getInstance().register(new ScrollIdlingResource(carUiRecyclerView));
+ onView(withText(adapter.getItemText(0))).check(matches(isDisplayed()));
OrientationHelper orientationHelper =
OrientationHelper.createVerticalHelper(carUiRecyclerView.getLayoutManager());
@@ -741,12 +763,10 @@
View longItem = getLongItem(carUiRecyclerView);
// Making sure we've reached end of the recyclerview, after
// adding bottom padding
- assertThat(orientationHelper.getDecoratedEnd(longItem)
- + carUiRecyclerView.getPaddingBottom(),
- is(equalTo(carUiRecyclerView.getHeight())));
+ assertThat(orientationHelper.getDecoratedEnd(longItem),
+ is(equalTo(orientationHelper.getEndAfterPadding())));
}
-
@Test
public void testPageDownMaintainsMinimumScrollThumbTrackHeight() {
mActivity.runOnUiThread(
@@ -849,6 +869,58 @@
assertThat(mCarUiRecyclerView.getPaddingEnd(), is(equalTo(10)));
}
+ @Test
+ public void testSetAlphaToRecyclerViewWithoutScrollbar() {
+ doReturn(false).when(mTestableResources).getBoolean(R.bool.car_ui_scrollbar_enable);
+
+ CarUiRecyclerView mCarUiRecyclerView = new CarUiRecyclerView(mTestableContext);
+
+ assertThat(mCarUiRecyclerView.getAlpha(), is(equalTo(1.0f)));
+
+ mCarUiRecyclerView.setAlpha(0.5f);
+
+ assertThat(mCarUiRecyclerView.getAlpha(), is(equalTo(0.5f)));
+ }
+
+ @Test
+ public void testSetAlphaToRecyclerViewWithScrollbar() {
+ mActivity.runOnUiThread(
+ () -> mActivity.setContentView(
+ R.layout.car_ui_recycler_view_test_activity));
+
+ onView(withId(R.id.list)).check(matches(isDisplayed()));
+
+ CarUiRecyclerView carUiRecyclerView = mActivity.requireViewById(R.id.list);
+
+ ViewGroup container = (ViewGroup) carUiRecyclerView.getParent().getParent();
+
+ assertThat(carUiRecyclerView.getAlpha(), is(equalTo(1.0f)));
+ assertThat(container.getAlpha(), is(equalTo(1.0f)));
+
+ carUiRecyclerView.setAlpha(0.5f);
+
+ assertThat(carUiRecyclerView.getAlpha(), is(equalTo(1.0f)));
+ assertThat(container.getAlpha(), is(equalTo(0.5f)));
+ }
+
+ @Test
+ public void testCallAnimateOnRecyclerViewWithScrollbar() {
+ doReturn(true).when(mTestableResources).getBoolean(R.bool.car_ui_scrollbar_enable);
+ CarUiRecyclerView carUiRecyclerView = new CarUiRecyclerView(mTestableContext);
+
+ ViewGroup container = mActivity.findViewById(R.id.test_container);
+ container.post(() -> {
+ container.addView(carUiRecyclerView);
+ carUiRecyclerView.setAdapter(new TestAdapter(100));
+ });
+
+ onView(withId(R.id.car_ui_scroll_bar)).check(matches(isDisplayed()));
+
+ ViewGroup recyclerViewContainer = (ViewGroup) carUiRecyclerView.getParent().getParent();
+
+ assertThat(carUiRecyclerView.animate(), is(equalTo(recyclerViewContainer.animate())));
+ }
+
/**
* Returns an item in the current list view whose height is taller than that of
* the CarUiRecyclerView. If that item exists, then it is returned; otherwise an {@link
@@ -857,10 +929,12 @@
* @return An item that is taller than the CarUiRecyclerView.
*/
private View getLongItem(CarUiRecyclerView recyclerView) {
- for (int i = 0; i < recyclerView.getChildCount(); i++) {
- View item = recyclerView.getChildAt(i);
+ OrientationHelper orientationHelper =
+ OrientationHelper.createVerticalHelper(recyclerView.getLayoutManager());
+ for (int i = 0; i < recyclerView.getLayoutManager().getChildCount(); i++) {
+ View item = recyclerView.getLayoutManager().getChildAt(i);
- if (item.getHeight() > recyclerView.getHeight()) {
+ if (item.getHeight() > orientationHelper.getTotalSpace()) {
return item;
}
}
@@ -941,16 +1015,29 @@
}
}
- private static class FixedSizeTestAdapter extends RecyclerView.Adapter<TestViewHolder> {
+ private static class PerfectFitTestAdapter extends RecyclerView.Adapter<TestViewHolder> {
- private static final int ITEMS_PER_PAGE = 5;
+ private static final int MIN_HEIGHT = 30;
private final List<String> mData;
private final int mItemHeight;
- FixedSizeTestAdapter(int itemCount, int recyclerViewHeight) {
- mData = new ArrayList<>(itemCount);
- mItemHeight = recyclerViewHeight / ITEMS_PER_PAGE;
+ private int getMinHeightPerItemToFitScreen(int screenHeight) {
+ // When the height is a prime number, there can only be 1 item per page
+ int minHeight = screenHeight;
+ for (int i = screenHeight; i >= 1; i--) {
+ if (screenHeight % i == 0 && screenHeight / i >= MIN_HEIGHT) {
+ minHeight = screenHeight / i;
+ break;
+ }
+ }
+ return minHeight;
+ }
+ PerfectFitTestAdapter(int numOfPages, int recyclerViewHeight) {
+ mItemHeight = getMinHeightPerItemToFitScreen(recyclerViewHeight);
+ int itemsPerPage = recyclerViewHeight / mItemHeight;
+ int itemCount = itemsPerPage * numOfPages;
+ mData = new ArrayList<>(itemCount);
for (int i = 0; i < itemCount; i++) {
mData.add(getItemText(i));
}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/TestContentLimitingAdapter.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/TestContentLimitingAdapter.java
index a65be43..1af9a70 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/TestContentLimitingAdapter.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/TestContentLimitingAdapter.java
@@ -31,7 +31,7 @@
private final List<String> mItems;
- TestContentLimitingAdapter(int numItems) {
+ public TestContentLimitingAdapter(int numItems) {
mItems = new ArrayList<>();
for (int i = 0; i < numItems; i++) {
mItems.add("Item " + i);
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTest.java
new file mode 100644
index 0000000..3cc9b7e
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTest.java
@@ -0,0 +1,485 @@
+/*
+ * 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.ui.utils;
+
+import static android.view.View.GONE;
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+
+import static com.android.car.ui.utils.ViewUtils.DEFAULT_FOCUS;
+import static com.android.car.ui.utils.ViewUtils.FOCUSED_BY_DEFAULT;
+import static com.android.car.ui.utils.ViewUtils.IMPLICIT_DEFAULT_FOCUS;
+import static com.android.car.ui.utils.ViewUtils.NO_FOCUS;
+import static com.android.car.ui.utils.ViewUtils.REGULAR_FOCUS;
+import static com.android.car.ui.utils.ViewUtils.SCROLLABLE_CONTAINER_FOCUS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.view.View;
+import android.view.ViewTreeObserver;
+
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.car.ui.FocusArea;
+import com.android.car.ui.FocusParkingView;
+import com.android.car.ui.recyclerview.CarUiRecyclerView;
+import com.android.car.ui.recyclerview.TestContentLimitingAdapter;
+import com.android.car.ui.test.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+/** Unit tests for {@link ViewUtils}. */
+public class ViewUtilsTest {
+
+ @Rule
+ public ActivityTestRule<ViewUtilsTestActivity> mActivityRule =
+ new ActivityTestRule<>(ViewUtilsTestActivity.class);
+
+ private ViewUtilsTestActivity mActivity;
+ private FocusArea mFocusArea1;
+ private FocusArea mFocusArea2;
+ private FocusArea mFocusArea3;
+ private FocusArea mFocusArea4;
+ private FocusArea mFocusArea5;
+ private FocusParkingView mFpv;
+ private View mView2;
+ private View mFocusedByDefault3;
+ private View mView4;
+ private View mDefaultFocus4;
+ private CarUiRecyclerView mList5;
+ private View mRoot;
+
+ @Before
+ public void setUp() {
+ mActivity = mActivityRule.getActivity();
+ mFocusArea1 = mActivity.findViewById(R.id.focus_area1);
+ mFocusArea2 = mActivity.findViewById(R.id.focus_area2);
+ mFocusArea3 = mActivity.findViewById(R.id.focus_area3);
+ mFocusArea4 = mActivity.findViewById(R.id.focus_area4);
+ mFocusArea5 = mActivity.findViewById(R.id.focus_area5);
+ mFpv = mActivity.findViewById(R.id.fpv);
+ mView2 = mActivity.findViewById(R.id.view2);
+ mFocusedByDefault3 = mActivity.findViewById(R.id.focused_by_default3);
+ mView4 = mActivity.findViewById(R.id.view4);
+ mDefaultFocus4 = mActivity.findViewById(R.id.default_focus4);
+ mList5 = mActivity.findViewById(R.id.list5);
+ mRoot = mFocusArea1.getRootView();
+
+ mRoot.post(() -> {
+ mList5.setLayoutManager(new LinearLayoutManager(mActivity));
+ mList5.setAdapter(new TestContentLimitingAdapter(/* numItems= */ 2));
+ CarUiUtils.setRotaryScrollEnabled(mList5, /* isVertical= */ true);
+ });
+ }
+
+ @Test
+ public void testRootVisible() {
+ mRoot.post(() -> assertThat(mRoot.getVisibility()).isEqualTo(VISIBLE));
+ }
+
+ @Test
+ public void testGetAncestorFocusArea() {
+ mRoot.post(() -> assertThat(ViewUtils.getAncestorFocusArea(mView2)).isEqualTo(mFocusArea2));
+ }
+
+ @Test
+ public void testGetAncestorFocusArea_doesNotReturnItself() {
+ mRoot.post(() -> assertThat(ViewUtils.getAncestorFocusArea(mFocusArea2)).isNull());
+ }
+
+ @Test
+ public void testGetAncestorFocusArea_outsideFocusArea() {
+ mRoot.post(() -> assertThat(ViewUtils.getAncestorFocusArea(mFpv)).isNull());
+ }
+
+ @Test
+ public void testGetAncestorScrollableContainer() {
+ mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ View firstItem = mList5.getLayoutManager().findViewByPosition(0);
+ assertThat(ViewUtils.getAncestorScrollableContainer(firstItem))
+ .isEqualTo(mList5);
+ }
+ }));
+ }
+
+ @Test
+ public void testGetAncestorScrollableContainer_returnNull() {
+ mRoot.post(() -> assertThat(ViewUtils.getAncestorScrollableContainer(mView2)).isNull());
+ }
+
+ @Test
+ public void testFindFocusedByDefaultView() {
+ mRoot.post(() -> {
+ View focusedByDefault = ViewUtils.findFocusedByDefaultView(mRoot);
+ assertThat(focusedByDefault).isEqualTo(mFocusedByDefault3);
+ });
+ }
+
+ @Test
+ public void testFindFocusedByDefaultView_skipNotFocusable() {
+ mRoot.post(() -> {
+ mFocusedByDefault3.setFocusable(false);
+ View focusedByDefault = ViewUtils.findFocusedByDefaultView(mRoot);
+ assertThat(focusedByDefault).isNull();
+ });
+ }
+
+ @Test
+ public void testFindFocusedByDefaultView_skipInvisibleView() {
+ mRoot.post(() -> {
+ mFocusArea3.setVisibility(INVISIBLE);
+ assertThat(mFocusArea3.getVisibility()).isEqualTo(INVISIBLE);
+ View focusedByDefault = ViewUtils.findFocusedByDefaultView(mRoot);
+ assertThat(focusedByDefault).isNull();
+ });
+ }
+
+ @Test
+ public void testFindFocusedByDefaultView_skipInvisibleAncestor() {
+ mRoot.post(() -> {
+ mRoot.setVisibility(INVISIBLE);
+ View focusedByDefault = ViewUtils.findFocusedByDefaultView(mFocusArea3);
+ assertThat(focusedByDefault).isNull();
+ });
+ }
+
+ @Test
+ public void testFindImplicitDefaultFocusView_inRoot() {
+ mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ View firstItem = mList5.getLayoutManager().findViewByPosition(0);
+ View implicitDefaultFocus = ViewUtils.findImplicitDefaultFocusView(mRoot);
+ assertThat(implicitDefaultFocus).isEqualTo(firstItem);
+ }
+ }));
+ }
+
+ @Test
+ public void testFindImplicitDefaultFocusView_inFocusArea() {
+ mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ View firstItem = mList5.getLayoutManager().findViewByPosition(0);
+ View implicitDefaultFocus =
+ ViewUtils.findImplicitDefaultFocusView(mFocusArea5);
+ assertThat(implicitDefaultFocus).isEqualTo(firstItem);
+ }
+ }));
+ }
+
+ @Test
+ public void testFindImplicitDefaultFocusView_skipInvisibleAncestor() {
+ mRoot.post(() -> {
+ mRoot.setVisibility(INVISIBLE);
+ View implicitDefaultFocus = ViewUtils.findImplicitDefaultFocusView(mFocusArea5);
+ assertThat(implicitDefaultFocus).isNull();
+ });
+ }
+
+ @Test
+ public void testFindFirstFocusableDescendant() {
+ mRoot.post(() -> {
+ mFocusArea2.setFocusable(true);
+ View firstFocusable = ViewUtils.findFirstFocusableDescendant(mRoot);
+ assertThat(firstFocusable).isEqualTo(mFocusArea2);
+ });
+ }
+
+ @Test
+ public void testFindFirstFocusableDescendant_skipItself() {
+ mRoot.post(() -> {
+ mFocusArea2.setFocusable(true);
+ View firstFocusable = ViewUtils.findFirstFocusableDescendant(mFocusArea2);
+ assertThat(firstFocusable).isEqualTo(mView2);
+ });
+ }
+
+ @Test
+ public void testFindFirstFocusableDescendant_skipInvisibleAndGoneView() {
+ mRoot.post(() -> {
+ mFocusArea2.setVisibility(INVISIBLE);
+ mFocusArea3.setVisibility(GONE);
+ View firstFocusable = ViewUtils.findFirstFocusableDescendant(mRoot);
+ assertThat(firstFocusable).isEqualTo(mView4);
+ });
+ }
+
+ @Test
+ public void testFindFirstFocusableDescendant_skipInvisibleAncestor() {
+ mRoot.post(() -> {
+ mRoot.setVisibility(INVISIBLE);
+ View firstFocusable = ViewUtils.findFirstFocusableDescendant(mFocusArea2);
+ assertThat(firstFocusable).isNull();
+ });
+ }
+
+ @Test
+ public void testIsImplicitDefaultFocusView_firstItem() {
+ mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ View firstItem = mList5.getLayoutManager().findViewByPosition(0);
+ assertThat(ViewUtils.isImplicitDefaultFocusView(firstItem)).isTrue();
+ }
+ }));
+ }
+
+ @Test
+ public void testIsImplicitDefaultFocusView_secondItem() {
+ mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ View secondItem = mList5.getLayoutManager().findViewByPosition(1);
+ assertThat(ViewUtils.isImplicitDefaultFocusView(secondItem)).isFalse();
+ }
+ }));
+ }
+
+ @Test
+ public void testIsImplicitDefaultFocusView_normalView() {
+ mRoot.post(() -> assertThat(ViewUtils.isImplicitDefaultFocusView(mView2)).isFalse());
+ }
+
+ @Test
+ public void testIsImplicitDefaultFocusView_skipInvisibleAncestor() {
+ mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ mFocusArea5.setVisibility(INVISIBLE);
+ View firstItem = mList5.getLayoutManager().findViewByPosition(0);
+ assertThat(ViewUtils.isImplicitDefaultFocusView(firstItem)).isFalse();
+ }
+ }));
+ }
+
+ @Test
+ public void testRequestFocus() {
+ mRoot.post(() -> assertRequestFocus(mView2, true));
+ }
+
+ @Test
+ public void testRequestFocus_nullView() {
+ mRoot.post(() -> assertRequestFocus(null, false));
+ }
+
+ @Test
+ public void testRequestFocus_alreadyFocused() {
+ mRoot.post(() -> {
+ assertRequestFocus(mView2, true);
+ // mView2 is already focused before requesting focus.
+ assertRequestFocus(mView2, true);
+ });
+ }
+
+ @Test
+ public void testRequestFocus_notFocusable() {
+ mRoot.post(() -> {
+ mView2.setFocusable(false);
+ assertRequestFocus(mView2, false);
+ });
+ }
+
+ @Test
+ public void testRequestFocus_disabled() {
+ mRoot.post(() -> {
+ mView2.setEnabled(false);
+ assertRequestFocus(mView2, false);
+ });
+ }
+
+ @Test
+ public void testRequestFocus_notVisible() {
+ mRoot.post(() -> {
+ mView2.setVisibility(View.INVISIBLE);
+ assertRequestFocus(mView2, false);
+ });
+ }
+
+ @Test
+ public void testRequestFocus_skipInvisibleAncestor() {
+ mRoot.post(() -> {
+ mFocusArea2.setVisibility(View.INVISIBLE);
+ assertRequestFocus(mView2, false);
+ });
+ }
+
+ @Test
+ public void testRequestFocus_zeroWidth() {
+ mRoot.post(() -> {
+ mView2.setRight(mView2.getLeft());
+ assertThat(mView2.getWidth()).isEqualTo(0);
+ assertRequestFocus(mView2, false);
+ });
+ }
+
+ @Test
+ public void testRequestFocus_detachedFromWindow() {
+ mRoot.post(() -> {
+ mFocusArea2.removeView(mView2);
+ assertRequestFocus(mView2, false);
+ });
+ }
+
+ @Test
+ public void testRequestFocus_FocusParkingView() {
+ mRoot.post(() -> {
+ assertRequestFocus(mView2, true);
+ assertRequestFocus(mFpv, false);
+ });
+ }
+
+ @Test
+ public void testRequestFocus_rotaryContainer() {
+ mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ assertRequestFocus(mList5, false);
+ }
+ }));
+ }
+
+ @Test
+ public void testRequestFocus_scrollableContainer() {
+ mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ assertRequestFocus(mList5, false);
+ }
+ }));
+ }
+
+ @Test
+ public void testAdjustFocus_inRoot() {
+ mRoot.post(() -> {
+ assertRequestFocus(mView2, true);
+ ViewUtils.adjustFocus(mRoot, null);
+ assertThat(mFocusedByDefault3.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testAdjustFocus_inFocusAreaWithDefaultFocus() {
+ mRoot.post(() -> {
+ assertRequestFocus(mView2, true);
+ ViewUtils.adjustFocus(mFocusArea3, null);
+ assertThat(mFocusedByDefault3.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testAdjustFocus_inFocusAreaWithoutDefaultFocus() {
+ mRoot.post(() -> {
+ assertRequestFocus(mView4, true);
+ ViewUtils.adjustFocus(mFocusArea2, null);
+ assertThat(mView2.isFocused()).isTrue();
+ });
+ }
+
+ @Test
+ public void testAdjustFocus_inFocusAreaWithoutFocusableDescendant() {
+ mRoot.post(() -> {
+ assertRequestFocus(mView2, true);
+ boolean success = ViewUtils.adjustFocus(mFocusArea1, null);
+ assertThat(mFocusArea1.hasFocus()).isFalse();
+ assertThat(success).isFalse();
+ });
+ }
+
+ @Test
+ public void testAdjustFocus_differentFocusLevels() {
+ mRoot.post(() -> {
+ assertThat(ViewUtils.adjustFocus(mFocusArea2, SCROLLABLE_CONTAINER_FOCUS)).isTrue();
+ assertThat(ViewUtils.adjustFocus(mFocusArea2, REGULAR_FOCUS)).isFalse();
+
+ assertThat(ViewUtils.adjustFocus(mFocusArea5, REGULAR_FOCUS)).isTrue();
+ assertThat(ViewUtils.adjustFocus(mFocusArea5, IMPLICIT_DEFAULT_FOCUS)).isFalse();
+
+ assertThat(ViewUtils.adjustFocus(mFocusArea4, IMPLICIT_DEFAULT_FOCUS)).isTrue();
+ assertThat(ViewUtils.adjustFocus(mFocusArea4, DEFAULT_FOCUS)).isFalse();
+
+ assertThat(ViewUtils.adjustFocus(mFocusArea3, DEFAULT_FOCUS)).isTrue();
+ assertThat(ViewUtils.adjustFocus(mFocusArea3, FOCUSED_BY_DEFAULT)).isFalse();
+
+ View firstItem = mList5.getLayoutManager().findViewByPosition(0);
+ firstItem.setFocusable(false);
+ View secondItem = mList5.getLayoutManager().findViewByPosition(1);
+ secondItem.setFocusable(false);
+ assertThat(ViewUtils.adjustFocus(mFocusArea5, NO_FOCUS)).isTrue();
+ assertThat(ViewUtils.adjustFocus(mFocusArea5, SCROLLABLE_CONTAINER_FOCUS)).isFalse();
+ });
+ }
+
+ @Test
+ public void testGetFocusLevel() {
+ mRoot.post(() -> {
+ assertThat(ViewUtils.getFocusLevel(null)).isEqualTo(NO_FOCUS);
+ assertThat(ViewUtils.getFocusLevel(mFpv)).isEqualTo(NO_FOCUS);
+ mFocusArea2.setVisibility(INVISIBLE);
+ assertThat(ViewUtils.getFocusLevel(mView2)).isEqualTo(NO_FOCUS);
+
+ assertThat(ViewUtils.getFocusLevel(mList5)).isEqualTo(SCROLLABLE_CONTAINER_FOCUS);
+
+ assertThat(ViewUtils.getFocusLevel(mView4)).isEqualTo(REGULAR_FOCUS);
+
+ mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ View firstItem = mList5.getLayoutManager().findViewByPosition(0);
+ assertThat(ViewUtils.getFocusLevel(firstItem))
+ .isEqualTo(IMPLICIT_DEFAULT_FOCUS);
+ }
+ }));
+
+ assertThat(ViewUtils.getFocusLevel(mDefaultFocus4)).isEqualTo(DEFAULT_FOCUS);
+
+ assertThat(ViewUtils.getFocusLevel(mFocusedByDefault3)).isEqualTo(FOCUSED_BY_DEFAULT);
+ });
+ }
+
+ private static void assertRequestFocus(@Nullable View view, boolean focused) {
+ boolean result = ViewUtils.requestFocus(view);
+ assertThat(result).isEqualTo(focused);
+ if (view != null) {
+ assertThat(view.isFocused()).isEqualTo(focused);
+ }
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTestActivity.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTestActivity.java
new file mode 100644
index 0000000..87434d4
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTestActivity.java
@@ -0,0 +1,32 @@
+/*
+ * 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.ui.utils;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import com.android.car.ui.test.R;
+
+/** An activity used for testing {@link ViewUtils}. */
+public class ViewUtilsTestActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.view_utils_test_activity);
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/car_ui_ime_wide_screen_test_activity.xml b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/car_ui_ime_wide_screen_test_activity.xml
new file mode 100644
index 0000000..e260300
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/car_ui_ime_wide_screen_test_activity.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/test_activity"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+</FrameLayout>
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_area_test_activity.xml b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_area_test_activity.xml
index 524de8d..da1255d 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_area_test_activity.xml
+++ b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_area_test_activity.xml
@@ -18,52 +18,79 @@
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="match_parent">
- <View
- android:focusable="true"
- android:layout_width="100dp"
- android:layout_height="100dp"/>
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <com.android.car.ui.FocusParkingView
+ android:id="@+id/fpv"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
<com.android.car.ui.TestFocusArea
- android:id="@+id/focus_area"
+ android:id="@+id/focus_area1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- app:defaultFocus="@+id/default_focus"
app:startBoundOffset="10dp"
app:endBoundOffset="20dp"
app:topBoundOffset="30dp"
app:bottomBoundOffset="40dp">
<View
- android:id="@+id/child"
+ android:id="@+id/view1"
android:focusable="true"
android:layout_width="100dp"
android:layout_height="100dp"/>
- <View
- android:id="@+id/default_focus"
- android:focusable="true"
+ <Button
+ android:id="@+id/button1"
android:layout_width="100dp"
android:layout_height="100dp"/>
</com.android.car.ui.TestFocusArea>
- <View
- android:id="@+id/non_child"
- android:focusable="true"
- android:layout_width="100dp"
- android:layout_height="100dp"/>
<com.android.car.ui.TestFocusArea
android:id="@+id/focus_area2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ app:defaultFocus="@+id/default_focus2"
app:highlightPaddingHorizontal="10dp"
app:highlightPaddingVertical="20dp"
app:highlightPaddingStart="30dp"
app:highlightPaddingTop="40dp"
app:startBoundOffset="50dp">
<View
- android:id="@+id/child1"
+ android:id="@+id/view2"
android:focusable="true"
android:layout_width="100dp"
android:layout_height="100dp"/>
<View
- android:id="@+id/child2"
+ android:id="@+id/default_focus2"
+ android:focusable="true"
+ android:layout_width="100dp"
+ android:layout_height="100dp"/>
+ </com.android.car.ui.TestFocusArea>
+ <com.android.car.ui.TestFocusArea
+ android:id="@+id/focus_area3"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:nudgeShortcut="@+id/nudge_shortcut3"
+ app:nudgeShortcutDirection="right">
+ <View
+ android:id="@+id/view3"
+ android:focusable="true"
+ android:layout_width="100dp"
+ android:layout_height="100dp"/>
+ <View
+ android:focusable="true"
+ android:layout_width="100dp"
+ android:layout_height="100dp"/>
+ <View
+ android:id="@+id/nudge_shortcut3"
+ android:focusable="true"
+ android:layout_width="100dp"
+ android:layout_height="100dp"/>
+ </com.android.car.ui.TestFocusArea>
+ <com.android.car.ui.TestFocusArea
+ android:id="@+id/focus_area4"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:nudgeLeft="@+id/focus_area2">
+ <View
+ android:id="@+id/view4"
android:focusable="true"
android:layout_width="100dp"
android:layout_height="100dp"/>
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_parking_view_test_activity.xml b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_parking_view_test_activity.xml
index f3f623d..02d7326 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_parking_view_test_activity.xml
+++ b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_parking_view_test_activity.xml
@@ -14,18 +14,32 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<FrameLayout
+<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
- <!-- In some cases Android will focus the first focusable view automatically. To prevent the
- FocusParkingView getting focused unintentionally, we put a focusable Button above the
- FocusParkingView. -->
- <Button
- android:layout_width="100dp"
- android:layout_height="40dp"/>
<com.android.car.ui.FocusParkingView
- android:id="@+id/focus_parking"
+ android:id="@+id/fpv"
+ android:layout_width="10dp"
+ android:layout_height="10dp"/>
+ <LinearLayout
+ android:id="@+id/parent1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <View
+ android:id="@+id/view1"
+ android:layout_width="100dp"
+ android:layout_height="40dp"
+ android:focusable="true"/>
+ </LinearLayout>
+ <View
+ android:id="@+id/focused_by_default"
+ android:layout_width="100dp"
+ android:layout_height="40dp"
+ android:focusable="true"
+ android:focusedByDefault="true"/>
+ <com.android.car.ui.recyclerview.CarUiRecyclerView
+ android:id="@+id/list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
-</FrameLayout>
+</LinearLayout>
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/test_car_ui_recycler_view_list_item.xml b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/test_car_ui_recycler_view_list_item.xml
index 7e8717e..8297f6b 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/test_car_ui_recycler_view_list_item.xml
+++ b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/test_car_ui_recycler_view_list_item.xml
@@ -18,7 +18,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="horizontal">
+ android:orientation="horizontal"
+ android:focusable="true">
<TextView
android:layout_width="wrap_content"
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/test_ime_input_view.xml b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/test_ime_input_view.xml
new file mode 100644
index 0000000..6b1fcb8
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/test_ime_input_view.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/test_ime_input_view_id"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+</FrameLayout>
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/test_list_item.xml b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/test_list_item.xml
index e789145..1fab70e 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/test_list_item.xml
+++ b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/test_list_item.xml
@@ -18,7 +18,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="horizontal">
+ android:orientation="horizontal"
+ android:focusable="true">
<TextView
android:layout_width="wrap_content"
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/view_utils_test_activity.xml b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/view_utils_test_activity.xml
new file mode 100644
index 0000000..72c2366
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/view_utils_test_activity.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<LinearLayout
+ 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="match_parent">
+ <com.android.car.ui.FocusParkingView
+ android:id="@+id/fpv"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+ <com.android.car.ui.FocusArea
+ android:id="@+id/focus_area1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <View
+ android:layout_width="100dp"
+ android:layout_height="100dp"/>
+ </com.android.car.ui.FocusArea>
+ <com.android.car.ui.FocusArea
+ android:id="@+id/focus_area2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <View
+ android:layout_width="100dp"
+ android:layout_height="100dp"/>
+ <View
+ android:id="@+id/view2"
+ android:layout_width="100dp"
+ android:layout_height="100dp"
+ android:focusable="true"/>
+ </com.android.car.ui.FocusArea>
+ <com.android.car.ui.FocusArea
+ android:id="@+id/focus_area3"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <View
+ android:layout_width="100dp"
+ android:layout_height="100dp"
+ android:focusable="true"/>
+ <View
+ android:id="@+id/focused_by_default3"
+ android:layout_width="100dp"
+ android:layout_height="100dp"
+ android:focusable="true"
+ android:focusedByDefault="true"/>
+ </com.android.car.ui.FocusArea>
+ <com.android.car.ui.FocusArea
+ android:id="@+id/focus_area4"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:defaultFocus="@+id/default_focus4">
+ <View
+ android:id="@+id/view4"
+ android:layout_width="100dp"
+ android:layout_height="100dp"
+ android:focusable="true"/>
+ <View
+ android:id="@+id/default_focus4"
+ android:layout_width="100dp"
+ android:layout_height="100dp"
+ android:focusable="true"/>
+ </com.android.car.ui.FocusArea>
+ <com.android.car.ui.FocusArea
+ android:id="@+id/focus_area5"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <com.android.car.ui.recyclerview.CarUiRecyclerView
+ android:id="@+id/list5"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+ </com.android.car.ui.FocusArea>
+</LinearLayout>
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/res/values/strings.xml b/car-ui-lib/car-ui-lib/src/androidTest/res/values/strings.xml
index 21f4ee4..7926d9c 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/res/values/strings.xml
+++ b/car-ui-lib/car-ui-lib/src/androidTest/res/values/strings.xml
@@ -31,4 +31,10 @@
<string name="title_dropdown_preference">Dropdown preference</string>
<string name="title_twoaction_preference">TwoAction preference</string>
<string name="summary_twoaction_preference">A widget should be visible on the right</string>
+
+ <string-array name="test_string_array">
+ <item>Item 1</item>
+ <item>Item 2</item>
+ <item>Item 3</item>
+ </string-array>
</resources>
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/AlertDialogBuilder.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/AlertDialogBuilder.java
index a025e9e..ed508d3 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/AlertDialogBuilder.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/AlertDialogBuilder.java
@@ -15,12 +15,20 @@
*/
package com.android.car.ui;
+import static android.view.WindowInsets.Type.ime;
+
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.ADD_DESC_TITLE_TO_CONTENT_AREA;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.ADD_DESC_TO_CONTENT_AREA;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.WIDE_SCREEN_ACTION;
+
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
import android.text.InputFilter;
import android.text.TextUtils;
import android.text.TextWatcher;
@@ -28,6 +36,7 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.ImageView;
@@ -60,6 +69,41 @@
private CharSequence mSubtitle;
private Drawable mIcon;
private boolean mIconTinted;
+ private boolean mAllowDismissButton = true;
+ private boolean mHasSingleChoiceBodyButton = false;
+ private EditText mCarUiEditText;
+ private InputMethodManager mInputMethodManager;
+ private String mWideScreenTitle;
+ private String mWideScreenTitleDesc;
+ private ViewGroup mRoot;
+
+ // Whenever the IME is closed and opened again, the title and desc information needs to be
+ // passed to the IME to be rendered. If the information is not passed to the IME the content
+ // area of the IME will render nothing into the content area.
+ private final View.OnApplyWindowInsetsListener mOnApplyWindowInsetsListener = (v, insets) -> {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ // WindowInsets.isVisible() is only available on R or above
+ return v.onApplyWindowInsets(insets);
+ }
+
+ if (insets.isVisible(ime())) {
+ Bundle bundle = new Bundle();
+ String title = mWideScreenTitle != null ? mWideScreenTitle : mTitle.toString();
+ bundle.putString(ADD_DESC_TITLE_TO_CONTENT_AREA, title);
+ if (mWideScreenTitleDesc != null) {
+ bundle.putString(ADD_DESC_TO_CONTENT_AREA, mWideScreenTitleDesc);
+ }
+ mInputMethodManager.sendAppPrivateCommand(mCarUiEditText, WIDE_SCREEN_ACTION,
+ bundle);
+ }
+ return v.onApplyWindowInsets(insets);
+ };
+
+ private final AlertDialog.OnDismissListener mOnDismissListener = dialog -> {
+ if (mRoot != null) {
+ mRoot.setOnApplyWindowInsetsListener(null);
+ }
+ };
public AlertDialogBuilder(Context context) {
// Resource id specified as 0 uses the parent contexts resolved value for alertDialogTheme.
@@ -68,6 +112,8 @@
public AlertDialogBuilder(Context context, int themeResId) {
mBuilder = new AlertDialog.Builder(context, themeResId);
+ mInputMethodManager = (InputMethodManager)
+ context.getSystemService(Context.INPUT_METHOD_SERVICE);
mContext = context;
}
@@ -329,6 +375,7 @@
public AlertDialogBuilder setItems(@ArrayRes int itemsId,
final DialogInterface.OnClickListener listener) {
mBuilder.setItems(itemsId, listener);
+ mHasSingleChoiceBodyButton = true;
return this;
}
@@ -341,6 +388,7 @@
public AlertDialogBuilder setItems(CharSequence[] items,
final DialogInterface.OnClickListener listener) {
mBuilder.setItems(items, listener);
+ mHasSingleChoiceBodyButton = true;
return this;
}
@@ -353,6 +401,7 @@
public AlertDialogBuilder setAdapter(final ListAdapter adapter,
final DialogInterface.OnClickListener listener) {
mBuilder.setAdapter(adapter, listener);
+ mHasSingleChoiceBodyButton = true;
return this;
}
@@ -363,6 +412,7 @@
*/
public AlertDialogBuilder setAdapter(final CarUiListItemAdapter adapter) {
setCustomList(adapter);
+ mHasSingleChoiceBodyButton = true;
return this;
}
@@ -390,6 +440,7 @@
final DialogInterface.OnClickListener listener,
String labelColumn) {
mBuilder.setCursor(cursor, listener, labelColumn);
+ mHasSingleChoiceBodyButton = true;
return this;
}
@@ -413,6 +464,7 @@
public AlertDialogBuilder setMultiChoiceItems(@ArrayRes int itemsId, boolean[] checkedItems,
final DialogInterface.OnMultiChoiceClickListener listener) {
mBuilder.setMultiChoiceItems(itemsId, checkedItems, listener);
+ mHasSingleChoiceBodyButton = false;
return this;
}
@@ -435,6 +487,7 @@
public AlertDialogBuilder setMultiChoiceItems(CharSequence[] items, boolean[] checkedItems,
final DialogInterface.OnMultiChoiceClickListener listener) {
mBuilder.setMultiChoiceItems(items, checkedItems, listener);
+ mHasSingleChoiceBodyButton = false;
return this;
}
@@ -460,6 +513,7 @@
String labelColumn,
final DialogInterface.OnMultiChoiceClickListener listener) {
mBuilder.setMultiChoiceItems(cursor, isCheckedColumn, labelColumn, listener);
+ mHasSingleChoiceBodyButton = true;
return this;
}
@@ -480,6 +534,7 @@
public AlertDialogBuilder setSingleChoiceItems(@ArrayRes int itemsId, int checkedItem,
final DialogInterface.OnClickListener listener) {
mBuilder.setSingleChoiceItems(itemsId, checkedItem, listener);
+ mHasSingleChoiceBodyButton = true;
return this;
}
@@ -502,6 +557,7 @@
String labelColumn,
final DialogInterface.OnClickListener listener) {
mBuilder.setSingleChoiceItems(cursor, checkedItem, labelColumn, listener);
+ mHasSingleChoiceBodyButton = true;
return this;
}
@@ -521,6 +577,7 @@
public AlertDialogBuilder setSingleChoiceItems(CharSequence[] items, int checkedItem,
final DialogInterface.OnClickListener listener) {
mBuilder.setSingleChoiceItems(items, checkedItem, listener);
+ mHasSingleChoiceBodyButton = true;
return this;
}
@@ -534,6 +591,7 @@
public AlertDialogBuilder setSingleChoiceItems(ListAdapter adapter, int checkedItem,
final DialogInterface.OnClickListener listener) {
mBuilder.setSingleChoiceItems(adapter, checkedItem, listener);
+ mHasSingleChoiceBodyButton = true;
return this;
}
@@ -548,13 +606,13 @@
* dismissed when an item is clicked. It will only be dismissed if clicked on a
* button, if no buttons are supplied it's up to the user to dismiss the dialog.
* @return This Builder object to allow for chaining of calls to set methods
- *
* @deprecated Use {@link #setSingleChoiceItems(CarUiRadioButtonListItemAdapter)} instead.
*/
@Deprecated
public AlertDialogBuilder setSingleChoiceItems(CarUiRadioButtonListItemAdapter adapter,
final DialogInterface.OnClickListener listener) {
setCustomList(adapter);
+ mHasSingleChoiceBodyButton = false;
return this;
}
@@ -570,6 +628,7 @@
*/
public AlertDialogBuilder setSingleChoiceItems(CarUiRadioButtonListItemAdapter adapter) {
setCustomList(adapter);
+ mHasSingleChoiceBodyButton = false;
return this;
}
@@ -583,6 +642,7 @@
public AlertDialogBuilder setOnItemSelectedListener(
final AdapterView.OnItemSelectedListener listener) {
mBuilder.setOnItemSelectedListener(listener);
+ mHasSingleChoiceBodyButton = true;
return this;
}
@@ -602,19 +662,19 @@
View contentView = LayoutInflater.from(mContext).inflate(
R.layout.car_ui_alert_dialog_edit_text, null);
- EditText editText = CarUiUtils.requireViewByRefId(contentView, R.id.textbox);
- editText.setText(prompt);
+ mCarUiEditText = CarUiUtils.requireViewByRefId(contentView, R.id.textbox);
+ mCarUiEditText.setText(prompt);
if (textChangedListener != null) {
- editText.addTextChangedListener(textChangedListener);
+ mCarUiEditText.addTextChangedListener(textChangedListener);
}
if (inputFilters != null) {
- editText.setFilters(inputFilters);
+ mCarUiEditText.setFilters(inputFilters);
}
if (inputType != 0) {
- editText.setInputType(inputType);
+ mCarUiEditText.setInputType(inputType);
}
mBuilder.setView(contentView);
@@ -635,6 +695,35 @@
return setEditBox(prompt, textChangedListener, inputFilters, 0);
}
+ /**
+ * By default, the AlertDialogBuilder may add a "Dismiss" button if you don't provide
+ * a positive/negative/neutral button. This is so that the dialog is still dismissible
+ * using the rotary controller. If however, you add buttons that can close the dialog via
+ * {@link #setAdapter(CarUiListItemAdapter)} or a similar method, then you may wish to
+ * suppress the addition of the dismiss button, which this method allows for.
+ *
+ * @param allowDismissButton If true, a "Dismiss" button may be added to the dialog.
+ * If false, it will never be added.
+ * @return this Builder object to allow for chaining of calls to set methods
+ */
+ public AlertDialogBuilder setAllowDismissButton(boolean allowDismissButton) {
+ mAllowDismissButton = allowDismissButton;
+ return this;
+ }
+
+ /**
+ * Sets the title and desc related to the dialog within the IMS templates.
+ *
+ * @param title title to be set.
+ * @param desc description related to the dialog.
+ * @return this Builder object to allow for chaining of calls to set methods
+ */
+ public AlertDialogBuilder setEditTextTitleAndDescForWideScreen(String title, String desc) {
+ mWideScreenTitle = title;
+ mWideScreenTitleDesc = desc;
+
+ return this;
+ }
/** Final steps common to both {@link #create()} and {@link #show()} */
private void prepareDialog() {
@@ -659,7 +748,14 @@
}
mBuilder.setCustomTitle(customTitle);
- if (!mNeutralButtonSet && !mNegativeButtonSet && !mPositiveButtonSet) {
+ if (!mAllowDismissButton && !mHasSingleChoiceBodyButton
+ && !mNeutralButtonSet && !mNegativeButtonSet && !mPositiveButtonSet) {
+ throw new RuntimeException(
+ "The dialog must have at least one button to disable the dismiss button");
+ }
+ if (mContext.getResources().getBoolean(R.bool.car_ui_alert_dialog_force_dismiss_button)
+ && !mNeutralButtonSet && !mNegativeButtonSet && !mPositiveButtonSet
+ && mAllowDismissButton) {
String mDefaultButtonText = mContext.getString(
R.string.car_ui_alert_dialog_default_button);
mBuilder.setNegativeButton(mDefaultButtonText, (dialog, which) -> {
@@ -683,9 +779,13 @@
// wrap-around. Android will focus on the first view automatically when the dialog is shown,
// and we want it to focus on the title instead of the FocusParkingView, so we put the
// FocusParkingView at the end of dialog window.
- ViewGroup root = (ViewGroup) alertDialog.getWindow().getDecorView().getRootView();
+ mRoot = (ViewGroup) alertDialog.getWindow().getDecorView().getRootView();
FocusParkingView fpv = new FocusParkingView(mContext);
- root.addView(fpv);
+ mRoot.addView(fpv);
+
+ // apply window insets listener to know when IME is visible so we can set title and desc.
+ mRoot.setOnApplyWindowInsetsListener(mOnApplyWindowInsetsListener);
+ setOnDismissListener(mOnDismissListener);
return alertDialog;
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusArea.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusArea.java
index f5c6c1d..0488d28 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusArea.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusArea.java
@@ -25,11 +25,14 @@
import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_RIGHT_BOUND_OFFSET;
import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_TOP_BOUND_OFFSET;
import static com.android.car.ui.utils.RotaryConstants.NUDGE_DIRECTION;
+import static com.android.car.ui.utils.ViewUtils.NO_FOCUS;
+import static com.android.car.ui.utils.ViewUtils.REGULAR_FOCUS;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
+import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.SystemClock;
@@ -38,7 +41,6 @@
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewParent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.LinearLayout;
@@ -282,13 +284,14 @@
/**
* Updates {@link #mPreviousFocusArea} when the focus has moved from another FocusArea to this
- * FocusArea.
+ * FocusArea, and sets it to {@code null} in any other cases.
*/
private void maybeUpdatePreviousFocusArea(boolean hasFocus, View oldFocus) {
- if (mHasFocus || !hasFocus || oldFocus == null) {
+ if (mHasFocus || !hasFocus || oldFocus == null || oldFocus instanceof FocusParkingView) {
+ mPreviousFocusArea = null;
return;
}
- mPreviousFocusArea = getAncestorFocusArea(oldFocus);
+ mPreviousFocusArea = ViewUtils.getAncestorFocusArea(oldFocus);
if (mPreviousFocusArea == null) {
Log.w(TAG, "No parent FocusArea for " + oldFocus);
}
@@ -305,7 +308,7 @@
if (!hasFocus || oldFocus == null) {
return;
}
- FocusArea oldFocusArea = getAncestorFocusArea(oldFocus);
+ FocusArea oldFocusArea = ViewUtils.getAncestorFocusArea(oldFocus);
if (oldFocusArea != this) {
return;
}
@@ -456,6 +459,28 @@
}
@Override
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ // To ensure the focus is initialized properly in rotary mode when there is a window focus
+ // change, this FocusArea will grab the focus from the currently focused view if one of this
+ // FocusArea's descendants is a better focus candidate than the currently focused view.
+ if (hasWindowFocus && !isInTouchMode()) {
+ maybeAdjustFocus();
+ }
+ super.onWindowFocusChanged(hasWindowFocus);
+ }
+
+ /**
+ * Focuses on another view in this FocusArea if the view is a better focus candidate than the
+ * currently focused view.
+ */
+ private boolean maybeAdjustFocus() {
+ View root = getRootView();
+ View focus = root.findFocus();
+ return ViewUtils.adjustFocus(root, focus);
+ }
+
+
+ @Override
public boolean performAccessibilityAction(int action, Bundle arguments) {
switch (action) {
case ACTION_FOCUS:
@@ -480,12 +505,6 @@
}
private boolean focusOnDescendant() {
- if (focusOnFocusedByDefaultView()) {
- return true;
- }
- if (focusOnPrimaryFocusView()) {
- return true;
- }
if (mDefaultFocusOverridesHistory) {
// Check mDefaultFocus before last focused view.
if (focusDefaultFocusView() || focusOnLastFocusedView()) {
@@ -500,28 +519,26 @@
return focusOnFirstFocusableView();
}
- private boolean focusOnFocusedByDefaultView() {
- View focusedByDefaultView = ViewUtils.findFocusedByDefaultView(this);
- return requestFocus(focusedByDefaultView);
- }
-
- private boolean focusOnPrimaryFocusView() {
- View primaryFocus = ViewUtils.findPrimaryFocusView(this);
- return requestFocus(primaryFocus);
- }
-
private boolean focusDefaultFocusView() {
- return requestFocus(mDefaultFocusView);
+ return ViewUtils.adjustFocus(this, /* currentLevel= */ REGULAR_FOCUS);
+ }
+
+ /**
+ * Gets the {@code app:defaultFocus} view.
+ *
+ * @hidden
+ */
+ public View getDefaultFocusView() {
+ return mDefaultFocusView;
}
private boolean focusOnLastFocusedView() {
View lastFocusedView = mRotaryCache.getFocusedView(SystemClock.uptimeMillis());
- return requestFocus(lastFocusedView);
+ return ViewUtils.requestFocus(lastFocusedView);
}
private boolean focusOnFirstFocusableView() {
- View firstFocusableView = ViewUtils.findFocusableDescendant(this);
- return requestFocus(firstFocusableView);
+ return ViewUtils.adjustFocus(this, /* currentLevel= */ NO_FOCUS);
}
private boolean nudgeToShortcutView(Bundle arguments) {
@@ -540,7 +557,7 @@
// nudge to another FocusArea.
return false;
}
- return requestFocus(mNudgeShortcutView);
+ return ViewUtils.requestFocus(mNudgeShortcutView);
}
private boolean nudgeToAnotherFocusArea(Bundle arguments) {
@@ -557,9 +574,6 @@
success = targetFocusArea != null && targetFocusArea.focusOnDescendant();
}
- if (success) {
- saveFocusAreaHistory(direction, this, targetFocusArea, elapsedRealtime);
- }
return success;
}
@@ -569,14 +583,15 @@
: arguments.getInt(NUDGE_DIRECTION, INVALID_DIRECTION);
}
- /** Saves bidirectional FocusArea nudge history. */
private void saveFocusAreaHistory(int direction, @NonNull FocusArea sourceFocusArea,
@NonNull FocusArea targetFocusArea, long elapsedRealtime) {
- sourceFocusArea.mRotaryCache.saveFocusArea(direction, targetFocusArea, elapsedRealtime);
-
- int oppositeDirection = getOppositeDirection(direction);
- targetFocusArea.mRotaryCache.saveFocusArea(oppositeDirection, sourceFocusArea,
- elapsedRealtime);
+ // Save one-way rather than two-way nudge history to avoid infinite nudge loop.
+ if (sourceFocusArea.mRotaryCache.getCachedFocusArea(direction, elapsedRealtime) == null) {
+ // Save reversed nudge history so that the users can nudge back to where they were.
+ int oppositeDirection = getOppositeDirection(direction);
+ targetFocusArea.mRotaryCache.saveFocusArea(oppositeDirection, sourceFocusArea,
+ elapsedRealtime);
+ }
}
/** Returns the direction opposite the given {@code direction} */
@@ -596,17 +611,6 @@
+ "FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, or FOCUS_RIGHT.");
}
- private static FocusArea getAncestorFocusArea(@NonNull View view) {
- ViewParent parent = view.getParent();
- while (parent != null) {
- if (parent instanceof FocusArea) {
- return (FocusArea) parent;
- }
- parent = parent.getParent();
- }
- return null;
- }
-
@Nullable
private FocusArea getSpecifiedFocusArea(int direction) {
maybeInitializeSpecifiedFocusAreas();
@@ -660,6 +664,19 @@
bundle.putInt(FOCUS_AREA_BOTTOM_BOUND_OFFSET, mBottomOffset);
}
+ @Override
+ protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
+ if (isInTouchMode()) {
+ return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
+ }
+ return maybeAdjustFocus();
+ }
+
+ @Override
+ public boolean restoreDefaultFocus() {
+ return maybeAdjustFocus();
+ }
+
private void maybeInitializeSpecifiedFocusAreas() {
if (mSpecifiedNudgeFocusAreaMap != null) {
return;
@@ -672,16 +689,11 @@
}
}
- private boolean requestFocus(@Nullable View view) {
- if (view == null || !view.isAttachedToWindow()) {
- return false;
- }
- // Exit touch mode and focus the view. The view may not be focusable in touch mode, so we
- // need to exit touch mode before focusing it.
- return view.performAccessibilityAction(ACTION_FOCUS, null);
- }
-
- /** Sets the padding (in pixels) of the FocusArea highlight. */
+ /**
+ * Sets the padding (in pixels) of the FocusArea highlight.
+ * <p>
+ * It doesn't affect other values, such as the paddings on its child views.
+ */
public void setHighlightPadding(int left, int top, int right, int bottom) {
if (mPaddingLeft == left && mPaddingTop == top && mPaddingRight == right
&& mPaddingBottom == bottom) {
@@ -694,7 +706,13 @@
invalidate();
}
- /** Sets the offset (in pixels) of the FocusArea's bounds. */
+ /**
+ * Sets the offset (in pixels) of the FocusArea's bounds.
+ * <p>
+ * It only affects the perceived bounds for the purposes of finding the nudge target. It doesn't
+ * affect the FocusArea's view bounds or highlight bounds. The offset should only be used when
+ * FocusAreas are overlapping and nudge interaction is ambiguous.
+ */
public void setBoundsOffset(int left, int top, int right, int bottom) {
mLeftOffset = left;
mTopOffset = top;
@@ -702,8 +720,28 @@
mBottomOffset = bottom;
}
+ /** Sets the default focus view in this FocusArea. */
+ public void setDefaultFocus(@NonNull View defaultFocus) {
+ mDefaultFocusView = defaultFocus;
+ }
+
@VisibleForTesting
void enableForegroundHighlight() {
mEnableForegroundHighlight = true;
}
+
+ @VisibleForTesting
+ void setDefaultFocusOverridesHistory(boolean override) {
+ mDefaultFocusOverridesHistory = override;
+ }
+
+ @VisibleForTesting
+ void setRotaryCache(@NonNull RotaryCache rotaryCache) {
+ mRotaryCache = rotaryCache;
+ }
+
+ @VisibleForTesting
+ void setClearFocusAreaHistoryWhenRotating(boolean clear) {
+ mClearFocusAreaHistoryWhenRotating = clear;
+ }
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusParkingView.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusParkingView.java
index 8a42cea..9d5cfeb 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusParkingView.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusParkingView.java
@@ -15,28 +15,34 @@
*/
package com.android.car.ui;
+import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS;
import static com.android.car.ui.utils.RotaryConstants.ACTION_HIDE_IME;
import static com.android.car.ui.utils.RotaryConstants.ACTION_RESTORE_DEFAULT_FOCUS;
import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
import android.os.Bundle;
import android.util.AttributeSet;
-import android.util.Log;
import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
import android.view.inputmethod.InputMethodManager;
import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
import com.android.car.ui.utils.ViewUtils;
/**
* A transparent {@link View} that can take focus. It's used by {@link
- * com.android.car.rotary.RotaryService} to support rotary controller navigation. Each {@link
- * android.view.Window} should have one FocusParkingView as the first focusable view in the view
- * tree, and outside of all {@link FocusArea}s. If multiple FocusParkingView are added in the
- * window, only the first one will be focusable.
+ * com.android.car.rotary.RotaryService} to support rotary controller navigation. It's also used to
+ * initialize the focus when in rotary mode.
+ * <p>
+ * To support the rotary controller, each {@link android.view.Window} must have a FocusParkingView
+ * as the first focusable view in the view tree, and outside of all {@link FocusArea}s.
* <p>
* Android doesn't clear focus automatically when focus is set in another window. If we try to clear
* focus in the previous window, Android will re-focus a view in that window, resulting in two
@@ -45,41 +51,67 @@
* matter whether it's focused or not. It can take focus so that RotaryService can "park" the focus
* on it to remove the focus highlight.
* <p>
- * If the focused view is scrolled off the screen, Android will refocus the first focusable view in
- * the window. The FocusParkingView should be the first view so that it gets focus. The
- * RotaryService detects this and moves focus to the scrolling container.
- * <p>
* If there is only one focus area in the current window, rotating the controller within the focus
* area will cause RotaryService to move the focus around from the view on the right to the view on
* the left or vice versa. Adding this view to each window can fix this issue. When RotaryService
* finds out the focus target is a FocusParkingView, it will know a wrap-around is going to happen.
* Then it will avoid the wrap-around by not moving focus.
+ * <p>
+ * To ensure the focus is initialized properly when there is a window change, the FocusParkingView
+ * will not get focused when the framework wants to focus on it. Instead, it will try to find a
+ * better focus target in the window and focus on the target. That said, the FocusParkingView can
+ * still be focused in order to clear focus highlight in the window, such as when RotaryService
+ * performs {@link android.view.accessibility.AccessibilityNodeInfo#ACTION_FOCUS} on the
+ * FocusParkingView, or the window has lost focus.
*/
public class FocusParkingView extends View {
- private static final String TAG = "FocusParkingView";
+
+ /**
+ * The focused view in the window containing this FocusParkingView. It's null if no view is
+ * focused, or the focused view is a FocusParkingView.
+ */
+ @Nullable
+ private View mFocusedView;
+
+ /** The scrollable container that contains the {@link #mFocusedView}, if any. */
+ @Nullable
+ ViewGroup mScrollableContainer;
+
+ /**
+ * Whether to restore focus when the frameworks wants to focus this view. When false, this view
+ * allows itself to be focused instead. This should be false for the {@code FocusParkingView} in
+ * an {@code ActivityView}. The default value is true.
+ */
+ private boolean mShouldRestoreFocus;
public FocusParkingView(Context context) {
super(context);
- init();
+ init(context, /* attrs= */ null);
}
public FocusParkingView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
- init();
+ init(context, attrs);
}
public FocusParkingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
- init();
+ init(context, attrs);
}
public FocusParkingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- init();
+ init(context, attrs);
}
- private void init() {
+ private void init(Context context, @Nullable AttributeSet attrs) {
+ if (attrs != null) {
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FocusParkingView);
+ mShouldRestoreFocus = a.getBoolean(R.styleable.FocusParkingView_shouldRestoreFocus,
+ /* defValue= */ true);
+ }
+
// This view is focusable, visible and enabled so it can take focus.
setFocusable(View.FOCUSABLE);
setVisibility(VISIBLE);
@@ -94,6 +126,12 @@
// Prevent Android from drawing the default focus highlight for this view when it's focused.
setDefaultFocusHighlightEnabled(false);
+
+ // Keep track of the focused view so that we can recover focus when it's removed.
+ getViewTreeObserver().addOnGlobalFocusChangeListener((oldFocus, newFocus) -> {
+ mFocusedView = newFocus instanceof FocusParkingView ? null : newFocus;
+ mScrollableContainer = ViewUtils.getAncestorScrollableContainer(mFocusedView);
+ });
}
@Override
@@ -107,12 +145,21 @@
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
if (!hasWindowFocus) {
- // We need to clear the focus (by parking the focus on the FocusParkingView) once the
- // current window goes to background. This can't be done by RotaryService because
- // RotaryService sees the window as removed, thus can't perform any action (such as
- // focus, clear focus) on the nodes in the window. So FocusParkingView has to grab the
- // focus proactively.
- requestFocus();
+ // We need to clear the focus highlight(by parking the focus on the FocusParkingView)
+ // once the current window goes to background. This can't be done by RotaryService
+ // because RotaryService sees the window as removed, thus can't perform any action
+ // (such as focus, clear focus) on the nodes in the window. So FocusParkingView has to
+ // grab the focus proactively.
+ super.requestFocus(FOCUS_DOWN, null);
+
+ // OnGlobalFocusChangeListener won't be triggered when the window lost focus, so reset
+ // the focused view here.
+ mFocusedView = null;
+ mScrollableContainer = null;
+ } else if (isFocused()) {
+ // When FocusParkingView is focused and the window just gets focused, transfer the view
+ // focus to a non-FocusParkingView in the window.
+ restoreFocusInRoot(/* checkForTouchMode= */ true);
}
super.onWindowFocusChanged(hasWindowFocus);
}
@@ -126,21 +173,7 @@
public boolean performAccessibilityAction(int action, Bundle arguments) {
switch (action) {
case ACTION_RESTORE_DEFAULT_FOCUS:
- View root = getRootView();
-
- // If there is a view focused by default and it can take focus, move focus to it.
- View defaultFocus = ViewUtils.findFocusedByDefaultView(root);
- if (defaultFocus != null) {
- return defaultFocus.requestFocus();
- }
-
- // If there is a primary focus view, move focus to it.
- View primaryFocus = ViewUtils.findPrimaryFocusView(root);
- if (primaryFocus != null) {
- return primaryFocus.requestFocus();
- }
-
- return false;
+ return restoreFocusInRoot(/* checkForTouchMode= */ false);
case ACTION_HIDE_IME:
InputMethodManager inputMethodManager =
getContext().getSystemService(InputMethodManager.class);
@@ -149,33 +182,93 @@
case ACTION_FOCUS:
// Don't leave this to View to handle as it will exit touch mode.
if (!hasFocus()) {
- return requestFocus();
+ return super.requestFocus(FOCUS_DOWN, null);
}
- break;
+ return false;
}
return super.performAccessibilityAction(action, arguments);
}
@Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
+ public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
+ if (!mShouldRestoreFocus) {
+ return super.requestFocus(direction, previouslyFocusedRect);
+ }
+ // Find a better target to focus instead of focusing this FocusParkingView when the
+ // framework wants to focus it.
+ return restoreFocusInRoot(/* checkForTouchMode= */ true);
+ }
- // If there is a FocusParkingView already, make the one after in the view tree
- // non-focusable.
- boolean []isBefore = new boolean[1];
- View anotherFpv = ViewUtils.depthFirstSearch(getRootView(), v -> {
- if (this == v) {
- isBefore[0] = true;
- }
- return v != this && v instanceof FocusParkingView && v.isFocusable();
- });
- if (anotherFpv != null) {
- Log.w(TAG, "There should be only one FocusParkingView in the window");
- if (isBefore[0]) {
- anotherFpv.setFocusable(false);
- } else {
- setFocusable(false);
+ @Override
+ public boolean restoreDefaultFocus() {
+ if (!mShouldRestoreFocus) {
+ return super.restoreDefaultFocus();
+ }
+ // Find a better target to focus instead of focusing this FocusParkingView when the
+ // framework wants to focus it.
+ return restoreFocusInRoot(/* checkForTouchMode= */ true);
+ }
+
+ /**
+ * Sets whether this view should restore focus when the framework wants to focus this view. When
+ * set to false, this view allows itself to be focused instead. This should be set to false for
+ * the {@code FocusParkingView} in an {@code ActivityView}. The default value is true.
+ */
+ public void setShouldRestoreFocus(boolean shouldRestoreFocus) {
+ mShouldRestoreFocus = shouldRestoreFocus;
+ }
+
+ private boolean restoreFocusInRoot(boolean checkForTouchMode) {
+ // Don't do anything in touch mode if checkForTouchMode is true.
+ if (checkForTouchMode && isInTouchMode()) {
+ return false;
+ }
+ // The focused view was in a scrollable container and the Framework unfocused it because it
+ // was scrolled off the screen. In this case focus on the scrollable container so that the
+ // rotary controller can scroll the scrollable container.
+ if (maybeFocusOnScrollableContainer()) {
+ return true;
+ }
+ // Otherwise try to find the best target view to focus.
+ if (ViewUtils.adjustFocus(getRootView(), /* currentFocus= */ null)) {
+ return true;
+ }
+ // It failed to find a target view (e.g., all the views are not shown), so focus on this
+ // FocusParkingView as fallback.
+ return super.requestFocus(FOCUS_DOWN, /* previouslyFocusedRect= */ null);
+ }
+
+ private boolean maybeFocusOnScrollableContainer() {
+ // If the focused view was in a scrollable container and it was scrolled off the screen,
+ // focus on the scrollable container. When a view is scrolled off the screen, it is no
+ // longer attached to window and its parent is not null. When a view is removed, its parent
+ // is null. There is no need to focus on the scrollable container when its focused element
+ // is removed.
+ if (mFocusedView != null && !mFocusedView.isAttachedToWindow()
+ && mFocusedView.getParent() != null && mScrollableContainer != null
+ && mScrollableContainer.isAttachedToWindow() && mScrollableContainer.isShown()) {
+ RecyclerView recyclerView = mScrollableContainer instanceof RecyclerView
+ ? (RecyclerView) mScrollableContainer
+ : null;
+ if (mScrollableContainer.requestFocus()) {
+ if (recyclerView != null && recyclerView.isComputingLayout()) {
+ // When a RecyclerView gains focus, it won't dispatch AccessibilityEvent if its
+ // layout is not ready. So wait until its layout is ready then dispatch the
+ // event.
+ getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ // At this point the layout is complete and the dimensions of
+ // recyclerView and any child views are known.
+ recyclerView.sendAccessibilityEvent(TYPE_VIEW_FOCUSED);
+ getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ }
+ });
+ }
+ return true;
}
}
+ return false;
}
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/BaseLayoutController.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/BaseLayoutController.java
index 1a8360e..1e5a46c 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/BaseLayoutController.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/BaseLayoutController.java
@@ -23,7 +23,6 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import androidx.annotation.LayoutRes;
@@ -47,7 +46,7 @@
* It also exposes a {@link ToolbarController} to access the toolbar. This may be null if
* used with a base layout without a Toolbar.
*/
-public class BaseLayoutController {
+final class BaseLayoutController {
private static final Map<Activity, BaseLayoutController> sBaseLayoutMap = new WeakHashMap<>();
@@ -180,7 +179,6 @@
}
InsetsUpdater insetsUpdater = new InsetsUpdater(activity, baseLayout, contentView);
- insetsUpdater.installListeners();
return Pair.create(toolbarController, insetsUpdater);
}
@@ -208,7 +206,7 @@
* none of the Activity/Fragments implement {@link InsetsChangedListener}, it will set
* padding on the content view equal to the insets.
*/
- public static final class InsetsUpdater implements ViewTreeObserver.OnGlobalLayoutListener {
+ static final class InsetsUpdater {
// These tags mark views that should overlay the content view in the base layout.
// OEMs should add them to views in their base layout, ie: android:tag="car_ui_left_inset"
// Apps will then be able to draw under these views, but will be encouraged to not put
@@ -228,7 +226,6 @@
private final View mBottomInsetView;
private InsetsChangedListener mInsetsChangedListenerDelegate;
- private boolean mInsetsDirty = true;
@NonNull
private Insets mInsets = new Insets();
@@ -240,7 +237,7 @@
* @param baseLayout The root view of the base layout
* @param contentView The android.R.id.content View
*/
- public InsetsUpdater(
+ InsetsUpdater(
@Nullable Activity activity,
@NonNull View baseLayout,
@NonNull View contentView) {
@@ -259,7 +256,7 @@
int oldLeft, int oldTop, int oldRight, int oldBottom) -> {
if (left != oldLeft || top != oldTop
|| right != oldRight || bottom != oldBottom) {
- mInsetsDirty = true;
+ recalcInsets();
}
};
@@ -279,17 +276,6 @@
mContentViewContainer.addOnLayoutChangeListener(layoutChangeListener);
}
- /**
- * Install a global layout listener, during which the insets will be recalculated and
- * dispatched.
- */
- public void installListeners() {
- // The global layout listener will run after all the individual layout change listeners
- // so that we only updateInsets once per layout, even if multiple inset views changed
- mContentView.getRootView().getViewTreeObserver()
- .addOnGlobalLayoutListener(this);
- }
-
@NonNull
Insets getInsets() {
return mInsets;
@@ -300,13 +286,9 @@
}
/**
- * onGlobalLayout() should recalculate the amount of insets we need, and then dispatch them.
+ * Recalculate the amount of insets we need, and then dispatch them.
*/
- @Override
- public void onGlobalLayout() {
- if (!mInsetsDirty) {
- return;
- }
+ public void recalcInsets() {
// Calculate how much each inset view overlays the content view
@@ -339,7 +321,6 @@
}
Insets insets = new Insets(left, top, right, bottom);
- mInsetsDirty = false;
if (!insets.equals(mInsets)) {
mInsets = insets;
dispatchNewInsets(insets);
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/SearchResultsProvider.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/SearchResultsProvider.java
new file mode 100644
index 0000000..f20c6b4
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/SearchResultsProvider.java
@@ -0,0 +1,114 @@
+/*
+ * 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.ui.core;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Content provider for displaying search results. The url looks like:
+ * "content://${applicationId}.SearchResultsProvide/search_results"
+ */
+public class SearchResultsProvider extends ContentProvider {
+ public static final String ITEM_ID = "primaryId";
+ public static final String SECONDARY_IMAGE_ID = "secondary";
+ public static final String PRIMARY_IMAGE_BLOB = "primary_image";
+ public static final String SECONDARY_IMAGE_BLOB = "secondary_image";
+ public static final String TITLE = "title";
+ public static final String SUBTITLE = "subtitle";
+ public static final String CONTENT = "content://";
+ public static final String SEARCH_RESULTS_PROVIDER = ".SearchResultsProvider";
+
+ private Uri mContentUri;
+ private List<ContentValues> mSearchResults = new ArrayList<>();
+
+ /**
+ * Database specific constant declarations
+ */
+ public static final String SEARCH_RESULTS_TABLE_NAME = "search_results";
+
+ @Override
+ public boolean onCreate() {
+ String url = CONTENT + getProviderName() + "/" + SEARCH_RESULTS_TABLE_NAME;
+ mContentUri = Uri.parse(url);
+
+ return true;
+ }
+
+ private String getProviderName() {
+ return getContext().getPackageName() + SEARCH_RESULTS_PROVIDER;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ mSearchResults.add(values);
+
+ Uri contentUri = ContentUris.withAppendedId(mContentUri, mSearchResults.size() - 1);
+ getContext().getContentResolver().notifyChange(contentUri, null);
+ return contentUri;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder) {
+ MatrixCursor cursor = new MatrixCursor(new String[]{
+ ITEM_ID,
+ SECONDARY_IMAGE_ID,
+ PRIMARY_IMAGE_BLOB,
+ SECONDARY_IMAGE_BLOB,
+ TITLE,
+ SUBTITLE
+ });
+
+ for (ContentValues values : mSearchResults) {
+ cursor.addRow(new Object[]{
+ values.get(ITEM_ID),
+ values.get(SECONDARY_IMAGE_ID),
+ values.get(PRIMARY_IMAGE_BLOB),
+ values.get(SECONDARY_IMAGE_BLOB),
+ values.get(TITLE),
+ values.get(SUBTITLE),
+ });
+ }
+ return cursor;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ getContext().getContentResolver().notifyChange(uri, null);
+ mSearchResults.clear();
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values,
+ String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return null;
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/imewidescreen/CarUiImeSearchListItem.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/imewidescreen/CarUiImeSearchListItem.java
new file mode 100644
index 0000000..8476869
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/imewidescreen/CarUiImeSearchListItem.java
@@ -0,0 +1,102 @@
+/*
+ * 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.ui.imewidescreen;
+
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.Nullable;
+
+import com.android.car.ui.recyclerview.CarUiContentListItem;
+import com.android.car.ui.recyclerview.CarUiListItemAdapter;
+
+/**
+ * Definition of list items that can be inserted into {@link CarUiListItemAdapter}. This class is
+ * used to display the search items in the template for wide screen mode.
+ *
+ * The class is used to pass application icon resources ids to the IME for rendering in its
+ * process. Applications can also pass a unique id for each item and supplemental icon that will be
+ * used by the IME to notify the application when a click action is taken on them.
+ */
+public class CarUiImeSearchListItem extends CarUiContentListItem {
+
+ private int mIconResId;
+ private int mSupplementalIconResId;
+
+ public CarUiImeSearchListItem(Action action) {
+ super(action);
+ }
+
+ /**
+ * Sets the icon of the item. Icon must be a BitmapDrawable.
+ *
+ * @param icon the icon to display.
+ */
+ @Override
+ public void setIcon(@Nullable Drawable icon) {
+ if (icon instanceof BitmapDrawable || icon == null) {
+ super.setIcon(icon);
+ return;
+ }
+ throw new RuntimeException("icon should be of type BitmapDrawable");
+ }
+
+ /**
+ * Sets supplemental icon to be displayed in a list item. Icon must be a BitmapDrawable.
+ *
+ * @param icon the Drawable to set as the icon, or null to clear the content.
+ * @param listener the callback that is invoked when the icon is clicked.
+ */
+ @Override
+ public void setSupplementalIcon(@Nullable Drawable icon,
+ @Nullable OnClickListener listener) {
+ if (icon instanceof BitmapDrawable || icon == null) {
+ super.setSupplementalIcon(icon, listener);
+ return;
+ }
+ throw new RuntimeException("icon should be of type BitmapDrawable");
+ }
+
+
+ /**
+ * Returns the icons resource of the item.
+ */
+ public int getIconResId() {
+ return mIconResId;
+ }
+
+ /**
+ * Sets the icons resource of the item.
+ */
+ public void setIconResId(int iconResId) {
+ mIconResId = iconResId;
+ }
+
+ /**
+ * Returns the supplemental icon resource id of the item.
+ */
+ public int getSupplementalIconResId() {
+ return mSupplementalIconResId;
+ }
+
+ /**
+ * Sets supplemental icon resource id.
+ */
+ public void setSupplementalIconResId(int supplementalIconResId) {
+ mSupplementalIconResId = supplementalIconResId;
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenController.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenController.java
new file mode 100644
index 0000000..be8aeee
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenController.java
@@ -0,0 +1,784 @@
+/*
+ * 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.ui.imewidescreen;
+
+import static com.android.car.ui.core.SearchResultsProvider.CONTENT;
+import static com.android.car.ui.core.SearchResultsProvider.SEARCH_RESULTS_PROVIDER;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.inputmethodservice.ExtractEditText;
+import android.inputmethodservice.InputMethodService;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.text.InputType;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.SurfaceControlViewHost.SurfacePackage;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.ui.R;
+import com.android.car.ui.core.SearchResultsProvider;
+import com.android.car.ui.recyclerview.CarUiContentListItem;
+import com.android.car.ui.recyclerview.CarUiListItemAdapter;
+import com.android.car.ui.utils.CarUiUtils;
+
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Helper class to build an IME that support widescreen mode.
+ *
+ * <p> This class provides helper methods that should be invoked during the lifecycle of an IME.
+ * Usage of these methods are listed below.
+ * <ul>
+ * <li>create an instance of {@link CarUiImeWideScreenController} in
+ * {@link InputMethodService#onCreate()}</li>
+ * <li>return {@link #onEvaluateFullscreenMode(boolean)} from
+ * {@link InputMethodService#onEvaluateFullscreenMode()}</li>
+ * <li>return the view created by
+ * {@link #createWideScreenImeView(View)}
+ * from {@link InputMethodService#onCreateInputView()}</li>
+ * <li>{@link #onComputeInsets(InputMethodService.Insets) should be called from
+ * {@link InputMethodService#onComputeInsets(InputMethodService.Insets)}</li>
+ * <li>{@link #onAppPrivateCommand(String, Bundle) should be called from {
+ * @link InputMethodService#onAppPrivateCommand(String, Bundle)}}</li>
+ * <li>{@link #setExtractViewShown(boolean)} should be called from
+ * {@link InputMethodService#setExtractViewShown(boolean)}</li>
+ * <li>{@link #onStartInputView(EditorInfo, InputConnection, CharSequence)} should be called
+ * from {@link InputMethodService#onStartInputView(EditorInfo, boolean)}</li>
+ * <li>{@link #onFinishInputView()} should be called from
+ * {@link InputMethodService#onFinishInputView(boolean)}</li>
+ * </ul>
+ *
+ * <p> All the methods in this class are guarded with a check {@link #isWideScreenMode()}. If
+ * wide screen mode is disabled all the method would return without doing anything. Also, IME
+ * should check for {@link #isWideScreenMode()} in
+ * {@link InputMethodService#setExtractViewShown(boolean)} and return the original value instead
+ * of false. for more info see {@link #setExtractViewShown(boolean)}
+ */
+public class CarUiImeWideScreenController {
+
+ private static final String TAG = "ImeWideScreenController";
+ private static final String NOT_ASTERISK_OR_CAPTURED_ASTERISK = "[^*]+|(\\*)";
+
+ // Automotive wide screen mode bundle keys.
+
+ // Action name of the action to support wide screen mode templates data.
+ public static final String WIDE_SCREEN_ACTION = "automotive_wide_screen";
+ // Action name of action that will be used by IMS to notify the application to clear the data
+ // in the EditText.
+ public static final String WIDE_SCREEN_CLEAR_DATA_ACTION = "automotive_wide_screen_clear_data";
+ // Action name used by applications to notify that new search results are available.
+ public static final String WIDE_SCREEN_SEARCH_RESULTS = "wide_screen_search_results";
+ // Key to provide the resource id for the icon that will be displayed in the input area. If
+ // this is not provided applications icon will be used. Value format is int.
+ public static final String WIDE_SCREEN_EXTRACTED_TEXT_ICON_RES_ID =
+ "extracted_text_icon_res_id";
+ // Key to determine if IME should display the content area or not. Content area is referred to
+ // the area used by IME to display search results, description title and description
+ // provided by the application. By default it will be shown but this value could be ignored
+ // if bool/car_ui_ime_wide_screen_allow_app_hide_content_area is set to false. Value format
+ // is boolean.
+ public static final String REQUEST_RENDER_CONTENT_AREA = "request_render_content_area";
+ // Key used to provide the description title to be rendered in the content area. Value format
+ // is String.
+ public static final String ADD_DESC_TITLE_TO_CONTENT_AREA = "add_desc_title_to_content_area";
+ // Key used to provide the description to be rendered in the content area. Value format is
+ // String.
+ public static final String ADD_DESC_TO_CONTENT_AREA = "add_desc_to_content_area";
+ // Key used to provide the error description to be rendered in the input area. Value format
+ // is String.
+ public static final String ADD_ERROR_DESC_TO_INPUT_AREA = "add_error_desc_to_input_area";
+
+ // wide screen search item keys. Each search item contains a title, sub-title, primary image
+ // and an secondary image. Click actions can be performed on item and secondary image.
+ // Application will be notified with the Ids of item clicked.
+
+ // Each key below represents a list. Search results will be displayed in the same order as
+ // the list provided by the application. For example, to create the search item at index 0
+ // controller will get the information from each lists index 0.
+
+ // Key used to provide list of unique id for each item. This same id will be sent back to
+ // the application when the item is clicked. Value format is ArrayList<String>
+ public static final String SEARCH_RESULT_ITEM_ID_LIST = "search_result_item_id_list";
+
+ public static final String SEARCH_RESULT_SUPPLEMENTAL_ICON_ID_LIST =
+ "search_result_supplemental_icon_id_list";
+ // key used to provide the surface package information by the application to the IME. IME
+ // will send the surface info each time its being displayed.
+ public static final String CONTENT_AREA_SURFACE_PACKAGE = "content_area_surface_package";
+ // key to provide the host token of surface view by IME to the application.
+ public static final String CONTENT_AREA_SURFACE_HOST_TOKEN = "content_area_surface_host_token";
+ // key to provide the display id of surface view by IME to the application.
+ public static final String CONTENT_AREA_SURFACE_DISPLAY_ID = "content_area_surface_display_id";
+ // key to provide the height of surface view by IME to the application.
+ public static final String CONTENT_AREA_SURFACE_HEIGHT = "content_area_surface_height";
+ // key to provide the width of surface view by IME to the application.
+ public static final String CONTENT_AREA_SURFACE_WIDTH = "content_area_surface_width";
+
+ private View mRootView;
+ private final Context mContext;
+ @Nullable
+ private View mExtractActionAutomotive;
+ @NonNull
+ private View mContentAreaAutomotive;
+ // whether to render the content area for automotive when in wide screen mode.
+ private boolean mImeRendersAllContent = true;
+ private boolean mAllowAppToHideContentArea;
+ @Nullable
+ private ArrayList<CarUiContentListItem> mAutomotiveSearchItems;
+ @NonNull
+ private TextView mWideScreenDescriptionTitle;
+ @NonNull
+ private TextView mWideScreenDescription;
+ @NonNull
+ private TextView mWideScreenErrorMessage;
+ @NonNull
+ private ImageView mWideScreenErrorImage;
+ @NonNull
+ private ImageView mWideScreenClearData;
+ @NonNull
+ private RecyclerView mRecyclerView;
+ @Nullable
+ private ImageView mWideScreenExtractedTextIcon;
+ private boolean mIsExtractIconProvidedByApp;
+ @NonNull
+ private FrameLayout mInputFrame;
+ @NonNull
+ private ExtractEditText mExtractEditText;
+ private EditorInfo mInputEditorInfo;
+ private InputConnection mInputConnection;
+ private boolean mExtractViewHidden;
+ @NonNull
+ private View mFullscreenArea;
+ @NonNull
+ private SurfaceView mContentAreaSurfaceView;
+ @NonNull
+ private FrameLayout mInputExtractEditTextContainer;
+ private final InputMethodService mInputMethodService;
+
+ public CarUiImeWideScreenController(@NonNull Context context, @NonNull InputMethodService ims) {
+ mContext = context;
+ mInputMethodService = ims;
+ }
+
+ /**
+ * Create and return the view hierarchy used for the input area in wide screen mode. This method
+ * will inflate the templates with the inputView provided.
+ *
+ * @param inputView view of the keyboard created by application.
+ * @return view to be used by {@link InputMethodService}.
+ */
+ public View createWideScreenImeView(@NonNull View inputView) {
+ if (!isWideScreenMode()) {
+ return inputView;
+ }
+ mRootView = View.inflate(mContext, R.layout.car_ui_ims_wide_screen_input_view, null);
+
+ mInputFrame = mRootView.requireViewById(R.id.car_ui_wideScreenInputArea);
+ mInputFrame.addView(inputView, new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+
+ mAllowAppToHideContentArea =
+ mContext.getResources().getBoolean(
+ R.bool.car_ui_ime_wide_screen_allow_app_hide_content_area);
+
+ mContentAreaSurfaceView = mRootView.requireViewById(R.id.car_ui_ime_surface);
+ mContentAreaSurfaceView.setZOrderOnTop(true);
+ mWideScreenDescriptionTitle =
+ mRootView.requireViewById(R.id.car_ui_wideScreenDescriptionTitle);
+ mWideScreenDescription = mRootView.requireViewById(R.id.car_ui_wideScreenDescription);
+ mExtractActionAutomotive =
+ mRootView.findViewById(R.id.car_ui_inputExtractActionAutomotive);
+ mContentAreaAutomotive = mRootView.requireViewById(R.id.car_ui_contentAreaAutomotive);
+ mRecyclerView = mRootView.requireViewById(R.id.car_ui_wideScreenSearchResultList);
+ mWideScreenErrorMessage = mRootView.requireViewById(R.id.car_ui_wideScreenErrorMessage);
+ mWideScreenExtractedTextIcon =
+ mRootView.findViewById(R.id.car_ui_wideScreenExtractedTextIcon);
+ mWideScreenErrorImage = mRootView.requireViewById(R.id.car_ui_wideScreenError);
+ mWideScreenClearData = mRootView.requireViewById(R.id.car_ui_wideScreenClearData);
+ mFullscreenArea = mRootView.requireViewById(R.id.car_ui_fullscreenArea);
+ mInputExtractEditTextContainer = mRootView.requireViewById(
+ R.id.car_ui_inputExtractEditTextContainer);
+ mWideScreenClearData.setOnClickListener(
+ v -> {
+ // notify the app to clear the data.
+ mInputConnection.performPrivateCommand(WIDE_SCREEN_CLEAR_DATA_ACTION, null);
+ });
+ mExtractViewHidden = false;
+
+ return mRootView;
+ }
+
+ /**
+ * Compute the interesting insets into your UI. When the content view is shown the default
+ * touchable insets are {@link InputMethodService.Insets#TOUCHABLE_INSETS_FRAME}. When content
+ * view is hidden then that area of the application is interactable by user.
+ *
+ * @param outInsets Fill in with the current UI insets.
+ */
+ public void onComputeInsets(@NonNull InputMethodService.Insets outInsets) {
+ if (!isWideScreenMode()) {
+ return;
+ }
+ Rect tempRect = new Rect();
+ int[] tempLocation = new int[2];
+ outInsets.contentTopInsets = outInsets.visibleTopInsets =
+ mInputMethodService.getWindow().getWindow().getDecorView().getHeight();
+ if (mImeRendersAllContent) {
+ outInsets.touchableRegion.setEmpty();
+ outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_FRAME;
+ } else {
+ mInputFrame.getLocationOnScreen(tempLocation);
+ tempRect.set(/* left= */0, /* top= */ 0,
+ tempLocation[0] + mInputFrame.getWidth(),
+ tempLocation[1] + mInputFrame.getHeight());
+ outInsets.touchableRegion.set(tempRect);
+ outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
+ }
+ }
+
+ /**
+ * Actions passed by the application must be "automotive_wide_screen" with the corresponding
+ * data that application wants to display. See the comments associated with each bundle key to
+ * know what view is rendered.
+ *
+ * <p> Each bundle key renders or updates/controls a particular view in the template. For
+ * example, if application rendered the description title and later also wanted to render an
+ * actual description with it then application should use both "add_desc_title_to_content_area"
+ * and "add_desc_to_content_area" to provide the data. Sending action with only
+ * "add_desc_to_content_area" bundle key will not add an extra view but will display only the
+ * description and not the title.
+ *
+ * When the IME window is closed all the views are reset. For the default view visibility see
+ * {@link #resetAutomotiveWideScreenViews()}.
+ *
+ * @param action Name of the command to be performed.
+ * @param data Any data to include with the command.
+ */
+ public void onAppPrivateCommand(String action, Bundle data) {
+ if (!isWideScreenMode()) {
+ return;
+ }
+ resetAutomotiveWideScreenViews();
+ if (data == null) {
+ return;
+ }
+ if (mAllowAppToHideContentArea || (mInputEditorInfo != null && isPackageAuthorized(
+ mInputEditorInfo.packageName))) {
+ mImeRendersAllContent = data.getBoolean(REQUEST_RENDER_CONTENT_AREA, true);
+ if (!mImeRendersAllContent) {
+ mContentAreaAutomotive.setVisibility(View.GONE);
+ } else {
+ mContentAreaAutomotive.setVisibility(View.VISIBLE);
+ }
+ }
+
+ if (data.getParcelable(CONTENT_AREA_SURFACE_PACKAGE) != null
+ && Build.VERSION.SDK_INT >= VERSION_CODES.R) {
+ SurfacePackage surfacePackage = (SurfacePackage) data.getParcelable(
+ CONTENT_AREA_SURFACE_PACKAGE);
+ mContentAreaSurfaceView.setChildSurfacePackage(surfacePackage);
+ mContentAreaSurfaceView.setVisibility(View.VISIBLE);
+ mContentAreaAutomotive.setVisibility(View.GONE);
+ }
+
+ String discTitle = data.getString(ADD_DESC_TITLE_TO_CONTENT_AREA);
+ if (!TextUtils.isEmpty(discTitle)) {
+ mWideScreenDescriptionTitle.setText(discTitle);
+ mWideScreenDescriptionTitle.setVisibility(View.VISIBLE);
+ mContentAreaAutomotive.setBackground(
+ mContext.getDrawable(R.drawable.car_ui_ime_wide_screen_background));
+ }
+
+ String disc = data.getString(ADD_DESC_TO_CONTENT_AREA);
+ if (!TextUtils.isEmpty(disc)) {
+ mWideScreenDescription.setText(disc);
+ mWideScreenDescription.setVisibility(View.VISIBLE);
+ mContentAreaAutomotive.setBackground(
+ mContext.getDrawable(R.drawable.car_ui_ime_wide_screen_background));
+ }
+
+ String errorMessage = data.getString(ADD_ERROR_DESC_TO_INPUT_AREA);
+ if (!TextUtils.isEmpty(errorMessage)) {
+ mWideScreenErrorMessage.setVisibility(View.VISIBLE);
+ mWideScreenClearData.setVisibility(View.GONE);
+ mWideScreenErrorImage.setVisibility(View.VISIBLE);
+ setExtractedEditTextBackground(
+ R.drawable.car_ui_ime_wide_screen_input_area_tint_error_color);
+ mWideScreenErrorMessage.setText(errorMessage);
+ mContentAreaAutomotive.setBackground(
+ mContext.getDrawable(R.drawable.car_ui_ime_wide_screen_background));
+ }
+
+ if (TextUtils.isEmpty(errorMessage)) {
+ mWideScreenErrorMessage.setVisibility(View.INVISIBLE);
+ mWideScreenErrorMessage.setText("");
+ mWideScreenClearData.setVisibility(View.VISIBLE);
+ mWideScreenErrorImage.setVisibility(View.GONE);
+ setExtractedEditTextBackground(
+ R.drawable.car_ui_ime_wide_screen_input_area_tint_color);
+ }
+
+ int extractedTextIcon = data.getInt(WIDE_SCREEN_EXTRACTED_TEXT_ICON_RES_ID);
+ if (extractedTextIcon != 0) {
+ setWideScreenExtractedIcon(extractedTextIcon);
+ }
+
+ if (WIDE_SCREEN_SEARCH_RESULTS.equals(action)) {
+ loadSearchItems();
+ }
+
+ if (mExtractActionAutomotive != null) {
+ mExtractActionAutomotive.setVisibility(View.VISIBLE);
+ }
+ if (mAutomotiveSearchItems != null) {
+ mRecyclerView.setLayoutManager(new LinearLayoutManager(mContext));
+ mRecyclerView.setVerticalScrollBarEnabled(true);
+ mRecyclerView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_LEFT);
+ mRecyclerView.setVisibility(View.VISIBLE);
+ mRecyclerView.setAdapter(new CarUiListItemAdapter(mAutomotiveSearchItems));
+ mContentAreaAutomotive.setBackground(
+ mContext.getDrawable(R.drawable.car_ui_ime_wide_screen_background));
+ if (mExtractActionAutomotive != null) {
+ mExtractActionAutomotive.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ private void loadSearchItems() {
+ if (mInputEditorInfo == null) {
+ Log.w(TAG, "Result can't be loaded, input InputEditorInfo not available ");
+ return;
+ }
+ String url = CONTENT + mInputEditorInfo.packageName + SEARCH_RESULTS_PROVIDER;
+ Uri contentUrl = Uri.parse(url);
+ ContentResolver cr = mContext.getContentResolver();
+ Cursor c = cr.query(contentUrl, null, null, null, null);
+ mAutomotiveSearchItems = new ArrayList<>();
+ if (c != null && c.moveToFirst()) {
+ do {
+ CarUiContentListItem searchItem = new CarUiContentListItem(
+ CarUiContentListItem.Action.ICON);
+ searchItem.setOnItemClickedListener(v -> {
+ Bundle bundle = new Bundle();
+ bundle.putString(SEARCH_RESULT_ITEM_ID_LIST,
+ c.getString(c.getColumnIndex(SearchResultsProvider.ITEM_ID)));
+ mInputConnection.performPrivateCommand(WIDE_SCREEN_ACTION, bundle);
+ });
+ searchItem.setTitle(c.getString(c.getColumnIndex(SearchResultsProvider.TITLE)));
+ searchItem.setBody(c.getString(c.getColumnIndex(SearchResultsProvider.SUBTITLE)));
+ searchItem.setPrimaryIconType(CarUiContentListItem.IconType.CONTENT);
+ byte[] primaryBlob = c.getBlob(
+ c.getColumnIndex(SearchResultsProvider.PRIMARY_IMAGE_BLOB));
+ if (primaryBlob != null) {
+ Bitmap primaryBitmap = Bitmap.CREATOR.createFromParcel(
+ byteArrayToParcel(primaryBlob));
+ searchItem.setIcon(
+ new BitmapDrawable(mContext.getResources(), primaryBitmap));
+ }
+ byte[] secondaryBlob = c.getBlob(
+ c.getColumnIndex(SearchResultsProvider.SECONDARY_IMAGE_BLOB));
+
+ if (secondaryBlob != null) {
+ Bitmap secondaryBitmap = Bitmap.CREATOR.createFromParcel(
+ byteArrayToParcel(secondaryBlob));
+ searchItem.setSupplementalIcon(
+ new BitmapDrawable(mContext.getResources(), secondaryBitmap), v -> {
+ Bundle bundle = new Bundle();
+ bundle.putString(SEARCH_RESULT_SUPPLEMENTAL_ICON_ID_LIST,
+ c.getString(c.getColumnIndex(
+ SearchResultsProvider.SECONDARY_IMAGE_ID)));
+ mInputConnection.performPrivateCommand(WIDE_SCREEN_ACTION, bundle);
+ });
+ }
+ mAutomotiveSearchItems.add(searchItem);
+ } while (c.moveToNext());
+ }
+ // delete the results.
+ cr.delete(contentUrl, null, null);
+ }
+
+ private static Parcel byteArrayToParcel(byte[] bytes) {
+ Parcel parcel = Parcel.obtain();
+ parcel.unmarshall(bytes, 0, bytes.length);
+ parcel.setDataPosition(0);
+ return parcel;
+ }
+
+ /**
+ * Evaluate if IME should launch in a fullscreen mode. In wide screen mode IME should always
+ * launch in a fullscreen mode so that {@link ExtractEditText} is inflated. Later the controller
+ * will detach the {@link ExtractEditText} from its original parent and inflate into the
+ * appropriate container in wide screen.
+ *
+ * @param isFullScreen value evaluated to be in fullscreen mode or not by the app.
+ */
+ public boolean onEvaluateFullscreenMode(boolean isFullScreen) {
+ return isWideScreenMode() || isFullScreen;
+ }
+
+ /**
+ * Initialize the view in the wide screen template based on the data provided by the app through
+ * {@link #onAppPrivateCommand(String, Bundle)}
+ */
+ public void onStartInputView(@NonNull EditorInfo editorInfo,
+ @Nullable InputConnection inputConnection, @Nullable CharSequence textForImeAction) {
+ if (!isWideScreenMode()) {
+ return;
+ }
+ mInputEditorInfo = editorInfo;
+ mInputConnection = inputConnection;
+ View header = mRootView.requireViewById(R.id.car_ui_imeWideScreenInputArea);
+
+ header.setVisibility(View.VISIBLE);
+ if (mExtractViewHidden) {
+ mFullscreenArea.setVisibility(View.INVISIBLE);
+ } else {
+ mFullscreenArea.setVisibility(View.VISIBLE);
+ }
+
+ // This view is rendered by the framework when IME is in full screen mode. For more info
+ // see {@link #onEvaluateFullscreenMode}
+ mExtractEditText = mRootView.getRootView().requireViewById(
+ android.R.id.inputExtractEditText);
+
+ mExtractEditText.setPadding(
+ mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_ui_ime_wide_screen_input_edit_text_padding_left),
+ /* top= */0,
+ mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_ui_ime_wide_screen_input_edit_text_padding_right),
+ /* bottom= */0);
+ mExtractEditText.setTextSize(mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_ui_ime_wide_screen_input_edit_text_size));
+ mExtractEditText.setGravity(Gravity.START | Gravity.CENTER);
+
+ ViewGroup parent = (ViewGroup) mExtractEditText.getParent();
+ parent.removeViewInLayout(mExtractEditText);
+
+ FrameLayout.LayoutParams params =
+ new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+
+ mInputExtractEditTextContainer.addView(mExtractEditText, params);
+
+ ImageView close = mRootView.findViewById(R.id.car_ui_closeKeyboard);
+ if (close != null) {
+ close.setOnClickListener(
+ (v) -> {
+ mInputMethodService.requestHideSelf(0);
+ });
+ }
+
+ if (!mIsExtractIconProvidedByApp) {
+ setWideScreenExtractedIcon(/* iconResId= */0);
+ }
+
+ boolean hasAction = (mInputEditorInfo.imeOptions & EditorInfo.IME_MASK_ACTION)
+ != EditorInfo.IME_ACTION_NONE;
+ boolean hasInputType = mInputEditorInfo.inputType != InputType.TYPE_NULL;
+ boolean hasNoAccessoryAction =
+ (mInputEditorInfo.imeOptions & EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION) == 0;
+
+ boolean hasLabel =
+ mInputEditorInfo.actionLabel != null || (hasAction && hasNoAccessoryAction
+ && hasInputType);
+
+ if (hasLabel) {
+ intiExtractAction(textForImeAction);
+ }
+
+ sendSurfaceInfo();
+ }
+
+ /**
+ * Sends the information for surface view to the application on which they can draw on. This
+ * information will ONLY be sent if OEM allows an application to hide the content area and let
+ * it draw its own content.
+ */
+ private void sendSurfaceInfo() {
+ if (!mAllowAppToHideContentArea && mContentAreaSurfaceView.getDisplay() == null
+ && !(mInputEditorInfo != null
+ && isPackageAuthorized(mInputEditorInfo.packageName))) {
+ return;
+ }
+ int displayId = mContentAreaSurfaceView.getDisplay().getDisplayId();
+ IBinder hostToken = mContentAreaSurfaceView.getHostToken();
+
+ Bundle bundle = new Bundle();
+ bundle.putBinder(CONTENT_AREA_SURFACE_HOST_TOKEN, hostToken);
+ bundle.putInt(CONTENT_AREA_SURFACE_DISPLAY_ID, displayId);
+ bundle.putInt(CONTENT_AREA_SURFACE_HEIGHT,
+ mContentAreaSurfaceView.getHeight() + getNavBarHeight());
+ bundle.putInt(CONTENT_AREA_SURFACE_WIDTH, mContentAreaSurfaceView.getWidth());
+
+ mInputConnection.performPrivateCommand(WIDE_SCREEN_ACTION, bundle);
+ }
+
+ private boolean isPackageAuthorized(String packageName) {
+ String[] packages = mContext.getResources()
+ .getStringArray(R.array.car_ui_ime_wide_screen_allowed_package_list);
+
+ PackageInfo packageInfo = getPackageInfo(mContext, packageName);
+ // Checks if the application of the given context is installed in the system image. I.e.
+ // if it's a bundled app.
+ if (packageInfo != null && (packageInfo.applicationInfo.flags & (ApplicationInfo.FLAG_SYSTEM
+ | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) != 0) {
+ return true;
+ }
+
+ for (String pattern : packages) {
+ String regex = createRegexFromGlob(pattern);
+ if (packageName.matches(regex)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return the package info for a particular package.
+ */
+ @Nullable
+ private static PackageInfo getPackageInfo(Context context,
+ String packageName) {
+ PackageManager packageManager = context.getPackageManager();
+ PackageInfo packageInfo = null;
+ try {
+ packageInfo = packageManager.getPackageInfo(
+ packageName, /* flags= */ 0);
+ } catch (PackageManager.NameNotFoundException ex) {
+ Log.e(TAG, "package not found: " + packageName);
+ }
+ return packageInfo;
+ }
+
+ private static String createRegexFromGlob(String glob) {
+ Pattern reg = Pattern.compile(NOT_ASTERISK_OR_CAPTURED_ASTERISK);
+ Matcher m = reg.matcher(glob);
+ StringBuffer b = new StringBuffer();
+ while (m.find()) {
+ if (m.group(1) != null) {
+ m.appendReplacement(b, ".*");
+ } else {
+ m.appendReplacement(b, Matcher.quoteReplacement(m.group(0)));
+ }
+ }
+ m.appendTail(b);
+ return b.toString();
+ }
+
+ private int getNavBarHeight() {
+ Resources resources = mContext.getResources();
+ int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
+ if (resourceId > 0) {
+ return resources.getDimensionPixelSize(resourceId);
+ }
+ return 0;
+ }
+
+ /**
+ * To support wide screen mode, IME should always call
+ * {@link InputMethodService#setExtractViewShown} with false and pass the flag to this method.
+ *
+ * For example, within the IMS service call
+ * <pre>
+ * @Override
+ * public void setExtractViewShown(boolean shown) {
+ * if (!carUiImeWideScreenController.isWideScreenMode()) {
+ * super.setExtractViewShown(shown);
+ * return;
+ * }
+ * super.setExtractViewShown(false);
+ * mImeWideScreenController.setExtractViewShown(shown);
+ * }
+ * </pre>
+ *
+ * This is required as IMS checks for ExtractViewIsShown and if that is true then set the
+ * touchable insets to the entire screen rather than a region. If an app hides the content area
+ * in that case we want the user to be able to interact with the application.
+ */
+ public void setExtractViewShown(boolean shown) {
+ if (!isWideScreenMode()) {
+ return;
+ }
+ if (mExtractViewHidden == !shown) {
+ return;
+ }
+ mExtractViewHidden = !shown;
+ if (mExtractViewHidden) {
+ mFullscreenArea.setVisibility(View.INVISIBLE);
+ } else {
+ mFullscreenArea.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void intiExtractAction(CharSequence textForImeAction) {
+ if (mExtractActionAutomotive == null) {
+ return;
+ }
+ if (mInputEditorInfo.actionLabel != null) {
+ ((TextView) mExtractActionAutomotive).setText(mInputEditorInfo.actionLabel);
+ } else {
+ ((TextView) mExtractActionAutomotive).setText(textForImeAction);
+ }
+
+ // click listener for the action button shown in the content area.
+ mExtractActionAutomotive.setOnClickListener(v -> {
+ final EditorInfo editorInfo = mInputEditorInfo;
+ final InputConnection inputConnection = mInputConnection;
+ if (editorInfo == null || inputConnection == null) {
+ return;
+ }
+ if (editorInfo.actionId != 0) {
+ inputConnection.performEditorAction(editorInfo.actionId);
+ } else if ((editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION)
+ != EditorInfo.IME_ACTION_NONE) {
+ inputConnection.performEditorAction(
+ editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION);
+ }
+ });
+ }
+
+ private void setExtractedEditTextBackground(int drawableResId) {
+ mExtractEditText.setBackgroundTintList(mContext.getColorStateList(drawableResId));
+ }
+
+ @VisibleForTesting
+ void setExtractEditText(ExtractEditText editText) {
+ mExtractEditText = editText;
+ }
+
+ /**
+ * Sets the icon in the input area. If the icon resource Id is not provided by the application
+ * then application icon will be used instead.
+ *
+ * @param iconResId icon resource id for the image drawable to load.
+ */
+ private void setWideScreenExtractedIcon(@DrawableRes int iconResId) {
+ if (mInputEditorInfo == null || mWideScreenExtractedTextIcon == null) {
+ return;
+ }
+ try {
+ if (iconResId == 0) {
+ mWideScreenExtractedTextIcon.setImageDrawable(
+ mContext.getPackageManager().getApplicationIcon(
+ mInputEditorInfo.packageName));
+ } else {
+ mIsExtractIconProvidedByApp = true;
+ mWideScreenExtractedTextIcon.setImageDrawable(
+ mContext.createPackageContext(mInputEditorInfo.packageName, 0).getDrawable(
+ iconResId));
+ }
+ mWideScreenExtractedTextIcon.setVisibility(View.VISIBLE);
+ } catch (PackageManager.NameNotFoundException ex) {
+ Log.w(TAG, "setWideScreenExtractedIcon: package name not found ", ex);
+ mWideScreenExtractedTextIcon.setVisibility(View.GONE);
+ } catch (Resources.NotFoundException ex) {
+ Log.w(TAG, "setWideScreenExtractedIcon: resource not found with id " + iconResId, ex);
+ mWideScreenExtractedTextIcon.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Called when IME window closes. Reset all the views once that happens.
+ */
+ public void onFinishInputView() {
+ if (!isWideScreenMode()) {
+ return;
+ }
+ resetAutomotiveWideScreenViews();
+ }
+
+ private void resetAutomotiveWideScreenViews() {
+ mWideScreenDescriptionTitle.setVisibility(View.GONE);
+ mContentAreaSurfaceView.setVisibility(View.GONE);
+ mWideScreenErrorMessage.setVisibility(View.GONE);
+ mRecyclerView.setVisibility(View.GONE);
+ mWideScreenDescription.setVisibility(View.GONE);
+ mFullscreenArea.setVisibility(View.VISIBLE);
+ if (mWideScreenExtractedTextIcon != null) {
+ mWideScreenExtractedTextIcon.setVisibility(View.VISIBLE);
+ }
+ mWideScreenClearData.setVisibility(View.VISIBLE);
+ mWideScreenErrorImage.setVisibility(View.GONE);
+ if (mExtractActionAutomotive != null) {
+ mExtractActionAutomotive.setVisibility(View.GONE);
+ }
+ mContentAreaAutomotive.setVisibility(View.VISIBLE);
+ mContentAreaAutomotive.setBackground(
+ mContext.getDrawable(R.drawable.car_ui_ime_wide_screen_no_content_background));
+ setExtractedEditTextBackground(R.drawable.car_ui_ime_wide_screen_input_area_tint_color);
+ mImeRendersAllContent = true;
+ mIsExtractIconProvidedByApp = false;
+ mExtractViewHidden = false;
+ mAutomotiveSearchItems = null;
+ }
+
+ /**
+ * Returns whether or not system is running in a wide screen mode.
+ */
+ public boolean isWideScreenMode() {
+ return CarUiUtils.getBooleanSystemProperty(mContext.getResources(),
+ R.string.car_ui_ime_wide_screen_system_property_name, false);
+ }
+
+ private Drawable loadDrawableFromPackage(int resId) {
+ try {
+ if (mInputEditorInfo != null) {
+ return mContext.createPackageContext(mInputEditorInfo.packageName, 0)
+ .getDrawable(resId);
+ }
+ } catch (PackageManager.NameNotFoundException ex) {
+ Log.e(TAG, "loadDrawableFromPackage: package name not found: ", ex);
+ } catch (Resources.NotFoundException ex) {
+ Log.w(TAG, "loadDrawableFromPackage: resource not found with id " + resId, ex);
+ }
+ return null;
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/ListPreferenceFragment.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/ListPreferenceFragment.java
index 5a34dcd..b9ad789 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/ListPreferenceFragment.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/ListPreferenceFragment.java
@@ -30,6 +30,7 @@
import androidx.preference.ListPreference;
import androidx.preference.Preference;
+import com.android.car.ui.FocusArea;
import com.android.car.ui.R;
import com.android.car.ui.baselayout.Insets;
import com.android.car.ui.baselayout.InsetsChangedListener;
@@ -51,20 +52,23 @@
*/
public class ListPreferenceFragment extends Fragment implements InsetsChangedListener {
- private ToolbarController mToolbar;
+ private static final String ARG_FULLSCREEN = "fullscreen";
+
private ListPreference mPreference;
private CarUiContentListItem mSelectedItem;
private int mSelectedIndex = -1;
+ private boolean mFullScreen;
/**
* Returns a new instance of {@link ListPreferenceFragment} for the {@link ListPreference} with
* the given {@code key}.
*/
@NonNull
- static ListPreferenceFragment newInstance(String key) {
+ static ListPreferenceFragment newInstance(String key, boolean fullScreen) {
ListPreferenceFragment fragment = new ListPreferenceFragment();
Bundle b = new Bundle(/* capacity= */ 1);
b.putString(ARG_KEY, key);
+ b.putBoolean(ARG_FULLSCREEN, fullScreen);
fragment.setArguments(b);
return fragment;
}
@@ -85,29 +89,40 @@
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
final CarUiRecyclerView carUiRecyclerView = CarUiUtils.requireViewByRefId(view, R.id.list);
- mToolbar = CarUi.getToolbar(getActivity());
+ mFullScreen = requireArguments().getBoolean(ARG_FULLSCREEN, true);
+ ToolbarController toolbar = mFullScreen ? CarUi.getToolbar(getActivity()) : null;
// TODO(b/150230923) remove the code for the old toolbar height change when apps are ready
- if (mToolbar == null) {
- Toolbar toolbarView = CarUiUtils.requireViewByRefId(view, R.id.toolbar);
- mToolbar = toolbarView;
+ if (toolbar == null) {
+ Toolbar toolbarView = CarUiUtils.findViewByRefId(view, R.id.toolbar);
+ toolbar = toolbarView;
- carUiRecyclerView.setPadding(0, toolbarView.getHeight(), 0, 0);
- toolbarView.registerToolbarHeightChangeListener(newHeight -> {
- if (carUiRecyclerView.getPaddingTop() == newHeight) {
- return;
- }
+ if (toolbarView != null) {
+ carUiRecyclerView.setPadding(0, toolbarView.getHeight(), 0, 0);
+ toolbarView.registerToolbarHeightChangeListener(newHeight -> {
+ if (carUiRecyclerView.getPaddingTop() == newHeight) {
+ return;
+ }
- int oldHeight = carUiRecyclerView.getPaddingTop();
- carUiRecyclerView.setPadding(0, newHeight, 0, 0);
- carUiRecyclerView.scrollBy(0, oldHeight - newHeight);
- });
+ int oldHeight = carUiRecyclerView.getPaddingTop();
+ carUiRecyclerView.setPadding(0, newHeight, 0, 0);
+ carUiRecyclerView.scrollBy(0, oldHeight - newHeight);
+
+ FocusArea focusArea = view.findViewById(R.id.car_ui_focus_area);
+ if (focusArea != null) {
+ focusArea.setHighlightPadding(0, newHeight, 0, 0);
+ focusArea.setBoundsOffset(0, newHeight, 0, 0);
+ }
+ });
+ }
}
carUiRecyclerView.setClipToPadding(false);
mPreference = getListPreference();
- mToolbar.setTitle(mPreference.getTitle());
- mToolbar.setState(Toolbar.State.SUBPAGE);
+ if (toolbar != null) {
+ toolbar.setTitle(mPreference.getTitle());
+ toolbar.setState(Toolbar.State.SUBPAGE);
+ }
CharSequence[] entries = mPreference.getEntries();
CharSequence[] entryValues = mPreference.getEntryValues();
@@ -178,11 +193,7 @@
}
private ListPreference getListPreference() {
- if (getArguments() == null) {
- throw new IllegalStateException("Preference arguments cannot be null");
- }
-
- String key = getArguments().getString(ARG_KEY);
+ String key = requireArguments().getString(ARG_KEY);
DialogPreference.TargetFragment fragment =
(DialogPreference.TargetFragment) getTargetFragment();
@@ -210,9 +221,17 @@
@Override
public void onCarUiInsetsChanged(@NonNull Insets insets) {
+ if (!mFullScreen) {
+ return;
+ }
View view = requireView();
CarUiUtils.requireViewByRefId(view, R.id.list)
.setPadding(0, insets.getTop(), 0, insets.getBottom());
view.setPadding(insets.getLeft(), 0, insets.getRight(), 0);
+ FocusArea focusArea = view.findViewById(R.id.car_ui_focus_area);
+ if (focusArea != null) {
+ focusArea.setHighlightPadding(0, insets.getTop(), 0, insets.getBottom());
+ focusArea.setBoundsOffset(0, insets.getTop(), 0, insets.getBottom());
+ }
}
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/MultiSelectListPreferenceFragment.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/MultiSelectListPreferenceFragment.java
index 44c5f43..f9cf8a2 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/MultiSelectListPreferenceFragment.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/MultiSelectListPreferenceFragment.java
@@ -29,6 +29,7 @@
import androidx.preference.DialogPreference;
import androidx.preference.Preference;
+import com.android.car.ui.FocusArea;
import com.android.car.ui.R;
import com.android.car.ui.baselayout.Insets;
import com.android.car.ui.baselayout.InsetsChangedListener;
@@ -52,19 +53,23 @@
*/
public class MultiSelectListPreferenceFragment extends Fragment implements InsetsChangedListener {
+ private static final String ARG_FULLSCREEN = "fullscreen";
+
private CarUiMultiSelectListPreference mPreference;
private Set<String> mNewValues;
private ToolbarController mToolbar;
+ private boolean mFullScreen;
/**
* Returns a new instance of {@link MultiSelectListPreferenceFragment} for the {@link
* CarUiMultiSelectListPreference} with the given {@code key}.
*/
@NonNull
- static MultiSelectListPreferenceFragment newInstance(String key) {
+ static MultiSelectListPreferenceFragment newInstance(String key, boolean fullScreen) {
MultiSelectListPreferenceFragment fragment = new MultiSelectListPreferenceFragment();
Bundle b = new Bundle(/* capacity= */ 1);
b.putString(ARG_KEY, key);
+ b.putBoolean(ARG_FULLSCREEN, fullScreen);
fragment.setArguments(b);
return fragment;
}
@@ -85,30 +90,41 @@
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
final CarUiRecyclerView recyclerView = CarUiUtils.requireViewByRefId(view, R.id.list);
- mToolbar = CarUi.getToolbar(requireActivity());
+ mFullScreen = requireArguments().getBoolean(ARG_FULLSCREEN, true);
+ mToolbar = mFullScreen ? CarUi.getToolbar(requireActivity()) : null;
// TODO(b/150230923) remove the code for the old toolbar height change when apps are ready
if (mToolbar == null) {
- Toolbar toolbarView = CarUiUtils.requireViewByRefId(view, R.id.toolbar);
+ Toolbar toolbarView = CarUiUtils.findViewByRefId(view, R.id.toolbar);
mToolbar = toolbarView;
- recyclerView.setPadding(0, toolbarView.getHeight(), 0, 0);
- toolbarView.registerToolbarHeightChangeListener(newHeight -> {
- if (recyclerView.getPaddingTop() == newHeight) {
- return;
- }
+ if (toolbarView != null) {
+ recyclerView.setPadding(0, toolbarView.getHeight(), 0, 0);
+ toolbarView.registerToolbarHeightChangeListener(newHeight -> {
+ if (recyclerView.getPaddingTop() == newHeight) {
+ return;
+ }
- int oldHeight = recyclerView.getPaddingTop();
- recyclerView.setPadding(0, newHeight, 0, 0);
- recyclerView.scrollBy(0, oldHeight - newHeight);
- });
+ int oldHeight = recyclerView.getPaddingTop();
+ recyclerView.setPadding(0, newHeight, 0, 0);
+ recyclerView.scrollBy(0, oldHeight - newHeight);
+
+ FocusArea focusArea = view.findViewById(R.id.car_ui_focus_area);
+ if (focusArea != null) {
+ focusArea.setHighlightPadding(0, newHeight, 0, 0);
+ focusArea.setBoundsOffset(0, newHeight, 0, 0);
+ }
+ });
+ }
}
mPreference = getPreference();
recyclerView.setClipToPadding(false);
- mToolbar.setTitle(mPreference.getTitle());
- mToolbar.setState(Toolbar.State.SUBPAGE);
+ if (mToolbar != null) {
+ mToolbar.setTitle(mPreference.getTitle());
+ mToolbar.setState(Toolbar.State.SUBPAGE);
+ }
mNewValues = new HashSet<>(mPreference.getValues());
CharSequence[] entries = mPreference.getEntries();
@@ -205,9 +221,17 @@
@Override
public void onCarUiInsetsChanged(@NonNull Insets insets) {
+ if (!mFullScreen) {
+ return;
+ }
View view = requireView();
CarUiUtils.requireViewByRefId(view, R.id.list)
.setPadding(0, insets.getTop(), 0, insets.getBottom());
view.setPadding(insets.getLeft(), 0, insets.getRight(), 0);
+ FocusArea focusArea = view.findViewById(R.id.car_ui_focus_area);
+ if (focusArea != null) {
+ focusArea.setHighlightPadding(0, insets.getTop(), 0, insets.getBottom());
+ focusArea.setBoundsOffset(0, insets.getTop(), 0, insets.getBottom());
+ }
}
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/PreferenceFragment.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/PreferenceFragment.java
index fbd6856..3f5b5cb 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/PreferenceFragment.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/PreferenceFragment.java
@@ -72,15 +72,29 @@
private static final String DIALOG_FRAGMENT_TAG =
"com.android.car.ui.PreferenceFragment.DIALOG";
+ /**
+ * This method can be overridden to indicate whether or not this fragment covers the
+ * whole screen. When it returns false, the preference fragment will not attempt to change
+ * the CarUi base layout toolbar (but will still have its own toolbar and change it when using
+ * non-baselayout toolbars), and will also not take into account CarUi insets.
+ *
+ * @return Whether to PreferenceFragment takes up the whole app's space. Defaults to true.
+ */
+ protected boolean isFullScreenFragment() {
+ return true;
+ }
+
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
- ToolbarController baseLayoutToolbar = CarUi.getToolbar(getActivity());
- if (baseLayoutToolbar != null) {
- baseLayoutToolbar.setState(Toolbar.State.SUBPAGE);
- if (getPreferenceScreen() != null) {
- baseLayoutToolbar.setTitle(getPreferenceScreen().getTitle());
+ if (isFullScreenFragment()) {
+ ToolbarController baseLayoutToolbar = CarUi.getToolbar(getActivity());
+ if (baseLayoutToolbar != null) {
+ baseLayoutToolbar.setState(Toolbar.State.SUBPAGE);
+ if (getPreferenceScreen() != null) {
+ baseLayoutToolbar.setTitle(getPreferenceScreen().getTitle());
+ }
}
}
@@ -103,6 +117,7 @@
FocusArea focusArea = CarUiUtils.requireViewByRefId(view, R.id.car_ui_focus_area);
focusArea.setHighlightPadding(0, newHeight, 0, 0);
+ focusArea.setBoundsOffset(0, newHeight, 0, 0);
});
recyclerView.setClipToPadding(false);
@@ -122,9 +137,14 @@
@Override
public void onCarUiInsetsChanged(@NonNull Insets insets) {
+ if (!isFullScreenFragment()) {
+ return;
+ }
+
View view = requireView();
FocusArea focusArea = CarUiUtils.requireViewByRefId(view, R.id.car_ui_focus_area);
focusArea.setHighlightPadding(0, insets.getTop(), 0, insets.getBottom());
+ focusArea.setBoundsOffset(0, insets.getTop(), 0, insets.getBottom());
CarUiUtils.requireViewByRefId(view, R.id.recycler_view)
.setPadding(0, insets.getTop(), 0, insets.getBottom());
view.setPadding(insets.getLeft(), 0, insets.getRight(), 0);
@@ -158,9 +178,10 @@
if (preference instanceof EditTextPreference) {
f = EditTextPreferenceDialogFragment.newInstance(preference.getKey());
} else if (preference instanceof ListPreference) {
- f = ListPreferenceFragment.newInstance(preference.getKey());
+ f = ListPreferenceFragment.newInstance(preference.getKey(), isFullScreenFragment());
} else if (preference instanceof MultiSelectListPreference) {
- f = MultiSelectListPreferenceFragment.newInstance(preference.getKey());
+ f = MultiSelectListPreferenceFragment
+ .newInstance(preference.getKey(), isFullScreenFragment());
} else if (preference instanceof CarUiSeekBarDialogPreference) {
f = SeekbarPreferenceDialogFragment.newInstance(preference.getKey());
} else {
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiContentListItem.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiContentListItem.java
index 71f5d1a..e6a14d7 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiContentListItem.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiContentListItem.java
@@ -17,7 +17,6 @@
package com.android.car.ui.recyclerview;
import android.graphics.drawable.Drawable;
-import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -114,7 +113,7 @@
private boolean mIsActivated;
private OnClickListener mOnClickListener;
private OnCheckedChangeListener mOnCheckedChangeListener;
- private View.OnClickListener mSupplementalIconOnClickListener;
+ private OnClickListener mSupplementalIconOnClickListener;
public CarUiContentListItem(Action action) {
@@ -301,7 +300,7 @@
* @param listener the callback that is invoked when the icon is clicked.
*/
public void setSupplementalIcon(@Nullable Drawable icon,
- @Nullable View.OnClickListener listener) {
+ @Nullable OnClickListener listener) {
if (mAction != Action.ICON) {
throw new IllegalStateException(
"Cannot set supplemental icon on list item that does not have an action of "
@@ -313,7 +312,7 @@
}
@Nullable
- public View.OnClickListener getSupplementalIconOnClickListener() {
+ public OnClickListener getSupplementalIconOnClickListener() {
return mSupplementalIconOnClickListener;
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiListItemAdapter.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiListItemAdapter.java
index 8bc39eb..a45b1e8 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiListItemAdapter.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiListItemAdapter.java
@@ -283,6 +283,7 @@
case ICON:
mSupplementalIcon.setVisibility(View.VISIBLE);
mSupplementalIcon.setImageDrawable(item.getSupplementalIcon());
+
mActionContainer.setVisibility(View.VISIBLE);
// If the icon has a click listener, use a reduced touch interceptor to create
@@ -309,8 +310,7 @@
mActionContainerTouchInterceptor.setOnClickListener(
(container) -> {
if (item.getSupplementalIconOnClickListener() != null) {
- item.getSupplementalIconOnClickListener().onClick(
- mSupplementalIcon);
+ item.getSupplementalIconOnClickListener().onClick(item);
}
});
mTouchInterceptor.setVisibility(View.GONE);
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java
index 627ab95..d94fbf3 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java
@@ -16,6 +16,7 @@
package com.android.car.ui.recyclerview;
import static com.android.car.ui.utils.CarUiUtils.findViewByRefId;
+import static com.android.car.ui.utils.RotaryConstants.ROTARY_CONTAINER;
import static com.android.car.ui.utils.RotaryConstants.ROTARY_HORIZONTALLY_SCROLLABLE;
import static com.android.car.ui.utils.RotaryConstants.ROTARY_VERTICALLY_SCROLLABLE;
@@ -25,8 +26,6 @@
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Rect;
-import android.os.Handler;
-import android.os.Looper;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.AttributeSet;
@@ -36,7 +35,7 @@
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
+import android.view.ViewPropertyAnimator;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
@@ -53,15 +52,16 @@
import com.android.car.ui.recyclerview.decorations.linear.LinearDividerItemDecoration;
import com.android.car.ui.recyclerview.decorations.linear.LinearOffsetItemDecoration;
import com.android.car.ui.recyclerview.decorations.linear.LinearOffsetItemDecoration.OffsetPosition;
+import com.android.car.ui.utils.CarUiUtils;
import com.android.car.ui.utils.CarUxRestrictionsUtil;
import java.lang.annotation.Retention;
import java.util.Objects;
/**
- * View that extends a {@link RecyclerView} and wraps itself into a {@link LinearLayout} which
- * could potentially include a scrollbar that has page up and down arrows. Interaction with this
- * view is similar to a {@code RecyclerView} as it takes the same adapter and the layout manager.
+ * View that extends a {@link RecyclerView} and wraps itself into a {@link LinearLayout} which could
+ * potentially include a scrollbar that has page up and down arrows. Interaction with this view is
+ * similar to a {@code RecyclerView} as it takes the same adapter and the layout manager.
*/
public final class CarUiRecyclerView extends RecyclerView {
@@ -77,22 +77,21 @@
private String mScrollBarClass;
private int mScrollBarPaddingTop;
private int mScrollBarPaddingBottom;
- private boolean mHasScrolledToTop = false;
@Nullable
private ScrollBar mScrollBar;
- @NonNull
+ @Nullable
private GridOffsetItemDecoration mTopOffsetItemDecorationGrid;
- @NonNull
+ @Nullable
private GridOffsetItemDecoration mBottomOffsetItemDecorationGrid;
- @NonNull
+ @Nullable
private RecyclerView.ItemDecoration mTopOffsetItemDecorationLinear;
- @NonNull
+ @Nullable
private RecyclerView.ItemDecoration mBottomOffsetItemDecorationLinear;
- @NonNull
+ @Nullable
private GridDividerItemDecoration mDividerItemDecorationGrid;
- @NonNull
+ @Nullable
private RecyclerView.ItemDecoration mDividerItemDecorationLinear;
private int mNumOfColumns;
private boolean mInstallingExtScrollBar = false;
@@ -104,22 +103,24 @@
@Nullable
private LinearLayout mContainer;
+ // Set to true when when styled attributes are read and initialized.
+ private boolean mIsInitialized;
private boolean mEnableDividers;
private int mTopOffset;
private int mBottomOffset;
- private ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener = () -> {
- if (!mHasScrolledToTop && getLayoutManager() != null) {
- // Scroll to the top after the first global layout, so that
- // we can set padding for the insets and still have the
- // recyclerview start at the top.
- new Handler(Objects.requireNonNull(Looper.myLooper())).post(() ->
- getLayoutManager().scrollToPosition(0));
- mHasScrolledToTop = true;
+ private boolean mHasScrolled = false;
+
+ private OnScrollListener mOnScrollListener = new OnScrollListener() {
+ @Override
+ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+ if (dx > 0 || dy > 0) {
+ mHasScrolled = true;
+ removeOnScrollListener(this);
+ }
}
};
-
/**
* The possible values for setScrollBarPosition. The default value is actually {@link
* CarUiRecyclerViewLayout#LINEAR}.
@@ -131,13 +132,15 @@
@Retention(SOURCE)
public @interface CarUiRecyclerViewLayout {
/**
- * Arranges items either horizontally in a single row or vertically in a single column.
- * This is default.
+ * Arranges items either horizontally in a single row or vertically in a single column. This
+ * is default.
*/
int LINEAR = 0;
- /** Arranges items in a Grid. */
- int GRID = 2;
+ /**
+ * Arranges items in a Grid.
+ */
+ int GRID = 1;
}
/**
@@ -164,8 +167,7 @@
/**
* Sets the maximum number of items available in the adapter. A value less than '0' means
- * the
- * list should not be capped.
+ * the list should not be capped.
*/
void setMaxItems(int maxItems);
}
@@ -186,13 +188,13 @@
}
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
- initRotaryScroll(context, attrs, defStyleAttr);
setClipToPadding(false);
TypedArray a = context.obtainStyledAttributes(
attrs,
R.styleable.CarUiRecyclerView,
defStyleAttr,
R.style.Widget_CarUi_CarUiRecyclerView);
+ initRotaryScroll(a);
mScrollBarEnabled = context.getResources().getBoolean(R.bool.car_ui_scrollbar_enable);
@@ -229,19 +231,26 @@
mBottomOffsetItemDecorationGrid =
new GridOffsetItemDecoration(mBottomOffset, mNumOfColumns,
OffsetPosition.END);
- if (carUiRecyclerViewLayout == CarUiRecyclerViewLayout.LINEAR) {
+
+ mIsInitialized = true;
+
+ // Check if a layout manager has already been set via XML
+ boolean isLayoutMangerSet = getLayoutManager() != null;
+ if (!isLayoutMangerSet && carUiRecyclerViewLayout == CarUiRecyclerViewLayout.LINEAR) {
setLayoutManager(new LinearLayoutManager(getContext()));
- } else {
+ } else if (!isLayoutMangerSet && carUiRecyclerViewLayout == CarUiRecyclerViewLayout.GRID) {
setLayoutManager(new GridLayoutManager(getContext(), mNumOfColumns));
}
+ addOnScrollListener(mOnScrollListener);
a.recycle();
-
if (!mScrollBarEnabled) {
return;
}
+ mContainer = new LinearLayout(getContext());
+
setVerticalScrollBarEnabled(false);
setHorizontalScrollBarEnabled(false);
@@ -249,59 +258,65 @@
}
@Override
- public void setLayoutManager(@Nullable LayoutManager layout) {
- addItemDecorations(layout);
- super.setLayoutManager(layout);
+ public void setLayoutManager(@Nullable LayoutManager layoutManager) {
+ // Cannot setup item decorations before stylized attributes have been read.
+ if (mIsInitialized) {
+ addItemDecorations(layoutManager);
+ }
+ super.setLayoutManager(layoutManager);
}
- private void addItemDecorations(LayoutManager layout) {
- // remove existing Item decorations
- removeItemDecoration(mDividerItemDecorationGrid);
- removeItemDecoration(mTopOffsetItemDecorationGrid);
- removeItemDecoration(mBottomOffsetItemDecorationGrid);
- removeItemDecoration(mDividerItemDecorationLinear);
- removeItemDecoration(mTopOffsetItemDecorationLinear);
- removeItemDecoration(mBottomOffsetItemDecorationLinear);
+ // This method should not be invoked before item decorations are initialized by the #init()
+ // method.
+ private void addItemDecorations(LayoutManager layoutManager) {
+ // remove existing Item decorations.
+ removeItemDecoration(Objects.requireNonNull(mDividerItemDecorationGrid));
+ removeItemDecoration(Objects.requireNonNull(mTopOffsetItemDecorationGrid));
+ removeItemDecoration(Objects.requireNonNull(mBottomOffsetItemDecorationGrid));
+ removeItemDecoration(Objects.requireNonNull(mDividerItemDecorationLinear));
+ removeItemDecoration(Objects.requireNonNull(mTopOffsetItemDecorationLinear));
+ removeItemDecoration(Objects.requireNonNull(mBottomOffsetItemDecorationLinear));
- if (layout instanceof GridLayoutManager) {
+ if (layoutManager instanceof GridLayoutManager) {
if (mEnableDividers) {
- addItemDecoration(mDividerItemDecorationGrid);
+ addItemDecoration(Objects.requireNonNull(mDividerItemDecorationGrid));
}
- addItemDecoration(mTopOffsetItemDecorationGrid);
- addItemDecoration(mBottomOffsetItemDecorationGrid);
- setNumOfColumns(((GridLayoutManager) layout).getSpanCount());
+ addItemDecoration(Objects.requireNonNull(mTopOffsetItemDecorationGrid));
+ addItemDecoration(Objects.requireNonNull(mBottomOffsetItemDecorationGrid));
+ setNumOfColumns(((GridLayoutManager) layoutManager).getSpanCount());
} else {
if (mEnableDividers) {
- addItemDecoration(mDividerItemDecorationLinear);
+ addItemDecoration(Objects.requireNonNull(mDividerItemDecorationLinear));
}
- addItemDecoration(mTopOffsetItemDecorationLinear);
- addItemDecoration(mBottomOffsetItemDecorationLinear);
+ addItemDecoration(Objects.requireNonNull(mTopOffsetItemDecorationLinear));
+ addItemDecoration(Objects.requireNonNull(mBottomOffsetItemDecorationLinear));
}
}
/**
- * If this view's content description isn't set to opt out of scrolling via the rotary
- * controller, initialize it accordingly.
+ * If this view's {@code rotaryScrollEnabled} attribute is set to true, sets the content
+ * description so that the {@code RotaryService} will treat it as a scrollable container and
+ * initializes this view accordingly.
*/
- private void initRotaryScroll(Context context, AttributeSet attrs, int defStyleAttr) {
- CharSequence contentDescription = getContentDescription();
- if (contentDescription == null) {
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView,
- defStyleAttr, /* defStyleRes= */ 0);
- int orientation = a.getInt(R.styleable.RecyclerView_android_orientation,
+ private void initRotaryScroll(@Nullable TypedArray styledAttributes) {
+ boolean rotaryScrollEnabled = styledAttributes != null && styledAttributes.getBoolean(
+ R.styleable.CarUiRecyclerView_rotaryScrollEnabled, /* defValue=*/ false);
+ if (rotaryScrollEnabled) {
+ int orientation = styledAttributes.getInt(R.styleable.RecyclerView_android_orientation,
LinearLayout.VERTICAL);
- setContentDescription(
- orientation == LinearLayout.HORIZONTAL
- ? ROTARY_HORIZONTALLY_SCROLLABLE
- : ROTARY_VERTICALLY_SCROLLABLE);
- } else if (!ROTARY_HORIZONTALLY_SCROLLABLE.contentEquals(contentDescription)
- && !ROTARY_VERTICALLY_SCROLLABLE.contentEquals(contentDescription)) {
- return;
+ CarUiUtils.setRotaryScrollEnabled(
+ this, /* isVertical= */ orientation == LinearLayout.VERTICAL);
+ } else {
+ CharSequence contentDescription = getContentDescription();
+ rotaryScrollEnabled = contentDescription != null
+ && (ROTARY_HORIZONTALLY_SCROLLABLE.contentEquals(contentDescription)
+ || ROTARY_VERTICALLY_SCROLLABLE.contentEquals(contentDescription));
}
- // Convert SOURCE_ROTARY_ENCODER scroll events into SOURCE_MOUSE scroll events that
- // RecyclerView knows how to handle.
- setOnGenericMotionListener((v, event) -> {
+ // If rotary scrolling is enabled, set a generic motion event listener to convert
+ // SOURCE_ROTARY_ENCODER scroll events into SOURCE_MOUSE scroll events that RecyclerView
+ // knows how to handle.
+ setOnGenericMotionListener(rotaryScrollEnabled ? (v, event) -> {
if (event.getAction() == MotionEvent.ACTION_SCROLL) {
if (event.getSource() == InputDevice.SOURCE_ROTARY_ENCODER) {
MotionEvent mouseEvent = MotionEvent.obtain(event);
@@ -311,11 +326,11 @@
}
}
return false;
- });
+ } : null);
- // Mark this view as focusable. This view will be focused when no focusable elements are
- // visible.
- setFocusable(true);
+ // If rotary scrolling is enabled, mark this view as focusable. This view will be focused
+ // when no focusable elements are visible.
+ setFocusable(rotaryScrollEnabled);
// Focus this view before descendants so that the RotaryService can focus this view when it
// wants to.
@@ -324,15 +339,20 @@
// Disable the default focus highlight. No highlight should appear when this view is
// focused.
setDefaultFocusHighlightEnabled(false);
+
+ // This view is a rotary container if it's not a scrollable container.
+ if (!rotaryScrollEnabled) {
+ super.setContentDescription(ROTARY_CONTAINER);
+ }
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
super.onRestoreInstanceState(state);
- // If we're restoring an existing RecyclerView, we don't want
- // to do the initial scroll to top
- mHasScrolledToTop = true;
+ // If we're restoring an existing RecyclerView, consider
+ // it as having already scrolled some.
+ mHasScrolled = true;
}
@Override
@@ -369,7 +389,6 @@
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mCarUxRestrictionsUtil.register(mListener);
- this.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener);
if (mInstallingExtScrollBar || !mScrollBarEnabled) {
return;
}
@@ -385,11 +404,10 @@
/**
* This method will detach the current recycler view from its parent and attach it to the
- * container which is a LinearLayout. Later the entire container is attached to the
- * parent where the recycler view was set with the same layout params.
+ * container which is a LinearLayout. Later the entire container is attached to the parent where
+ * the recycler view was set with the same layout params.
*/
private void installExternalScrollBar() {
- mContainer = new LinearLayout(getContext());
LayoutInflater inflater = LayoutInflater.from(getContext());
inflater.inflate(R.layout.car_ui_recycler_view, mContainer, true);
mContainer.setVisibility(mContainerVisibility);
@@ -444,10 +462,23 @@
}
@Override
+ public void setAlpha(float value) {
+ if (mScrollBarEnabled) {
+ mContainer.setAlpha(value);
+ } else {
+ super.setAlpha(value);
+ }
+ }
+
+ @Override
+ public ViewPropertyAnimator animate() {
+ return mScrollBarEnabled ? mContainer.animate() : super.animate();
+ }
+
+ @Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mCarUxRestrictionsUtil.unregister(mListener);
- this.getViewTreeObserver().removeOnGlobalLayoutListener(mOnGlobalLayoutListener);
}
@Override
@@ -455,6 +486,13 @@
mContainerPaddingRelative = null;
if (mScrollBarEnabled) {
super.setPadding(0, top, 0, bottom);
+ if (!mHasScrolled) {
+ // If we haven't scrolled, and thus are still at the top of the screen,
+ // we should stay scrolled to the top after applying padding. Without this
+ // scroll, the padding will start scrolled offscreen. We need the padding
+ // to be onscreen to shift the content into a good visible range.
+ scrollToPosition(0);
+ }
mContainerPadding = new Rect(left, 0, right, 0);
if (mContainer != null) {
mContainer.setPadding(left, 0, right, 0);
@@ -470,6 +508,13 @@
mContainerPadding = null;
if (mScrollBarEnabled) {
super.setPaddingRelative(0, top, 0, bottom);
+ if (!mHasScrolled) {
+ // If we haven't scrolled, and thus are still at the top of the screen,
+ // we should stay scrolled to the top after applying padding. Without this
+ // scroll, the padding will start scrolled offscreen. We need the padding
+ // to be onscreen to shift the content into a good visible range.
+ scrollToPosition(0);
+ }
mContainerPaddingRelative = new Rect(start, 0, end, 0);
if (mContainer != null) {
mContainer.setPaddingRelative(start, 0, end, 0);
@@ -481,8 +526,8 @@
}
/**
- * Sets the scrollbar's padding top and bottom.
- * This padding is applied in addition to the padding of the RecyclerView.
+ * Sets the scrollbar's padding top and bottom. This padding is applied in addition to the
+ * padding of the RecyclerView.
*/
public void setScrollBarPadding(int paddingTop, int paddingBottom) {
if (mScrollBarEnabled) {
@@ -518,6 +563,22 @@
removeItemDecoration(mDividerItemDecorationGrid);
}
+ @Override
+ public void setContentDescription(CharSequence contentDescription) {
+ super.setContentDescription(contentDescription);
+ initRotaryScroll(/* styledAttributes= */ null);
+ }
+
+ @Override
+ public void setAdapter(@Nullable Adapter adapter) {
+ if (mScrollBar != null) {
+ // Make sure this is called before super so that scrollbar can get a reference to
+ // the adapter using RecyclerView#getAdapter()
+ mScrollBar.adapterChanged(adapter);
+ }
+ super.setAdapter(adapter);
+ }
+
private static RuntimeException andLog(String msg, Throwable t) {
Log.e(TAG, msg, t);
throw new RuntimeException(msg, t);
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiSnapHelper.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiSnapHelper.java
index 136dc6e..fd532b5 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiSnapHelper.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiSnapHelper.java
@@ -246,7 +246,7 @@
* @param helper An {@link OrientationHelper} to aid with calculation.
* @return A float indicating the percentage of the given view that is visible.
*/
- private static float getPercentageVisible(View view, OrientationHelper helper) {
+ static float getPercentageVisible(View view, OrientationHelper helper) {
int start = helper.getStartAfterPadding();
int end = helper.getEndAfterPadding();
@@ -340,6 +340,80 @@
}
/**
+ * Estimates a position to which CarUiSnapHelper will try to snap to for a requested scroll
+ * distance.
+ *
+ * @param helper The {@link OrientationHelper} that is created from the LayoutManager.
+ * @param scrollDistance The intended scroll distance.
+ *
+ * @return The diff between the target snap position and the current position.
+ */
+ public int estimateNextPositionDiffForScrollDistance(OrientationHelper helper,
+ int scrollDistance) {
+ float distancePerChild = computeDistancePerChild(helper.getLayoutManager(), helper);
+ if (distancePerChild <= 0) {
+ return 0;
+ }
+ return (int) Math.round(scrollDistance / distancePerChild);
+ }
+
+ /**
+ * This method is taken verbatim from the [androidx] {@link LinearSnapHelper} private method
+ * implementation.
+ *
+ * Computes an average pixel value to pass a single child.
+ * <p>
+ * Returns a negative value if it cannot be calculated.
+ *
+ * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
+ * {@link RecyclerView}.
+ * @param helper The relevant {@link OrientationHelper} for the attached
+ * {@link RecyclerView.LayoutManager}.
+ *
+ * @return A float value that is the average number of pixels needed to scroll by one view in
+ * the relevant direction.
+ */
+ float computeDistancePerChild(RecyclerView.LayoutManager layoutManager,
+ OrientationHelper helper) {
+ View minPosView = null;
+ View maxPosView = null;
+ int minPos = Integer.MAX_VALUE;
+ int maxPos = Integer.MIN_VALUE;
+ int childCount = layoutManager.getChildCount();
+ if (childCount == 0) {
+ return 1;
+ }
+
+ for (int i = 0; i < childCount; i++) {
+ View child = layoutManager.getChildAt(i);
+ final int pos = layoutManager.getPosition(child);
+ if (pos == RecyclerView.NO_POSITION) {
+ continue;
+ }
+ if (pos < minPos) {
+ minPos = pos;
+ minPosView = child;
+ }
+ if (pos > maxPos) {
+ maxPos = pos;
+ maxPosView = child;
+ }
+ }
+ if (minPosView == null || maxPosView == null) {
+ return 1;
+ }
+ int start = Math.min(helper.getDecoratedStart(minPosView),
+ helper.getDecoratedStart(maxPosView));
+ int end = Math.max(helper.getDecoratedEnd(minPosView),
+ helper.getDecoratedEnd(maxPosView));
+ int distance = end - start;
+ if (distance == 0) {
+ return 0;
+ }
+ return 1f * distance / ((maxPos - minPos) + 1);
+ }
+
+ /**
* Returns {@code true} if the RecyclerView is completely displaying the first item.
*/
public boolean isAtStart(@Nullable LayoutManager layoutManager) {
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/DefaultScrollBar.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/DefaultScrollBar.java
index b27aab0..42554bf 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/DefaultScrollBar.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/DefaultScrollBar.java
@@ -19,6 +19,7 @@
import android.content.res.Resources;
import android.os.Handler;
+import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
@@ -26,6 +27,7 @@
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.recyclerview.widget.OrientationHelper;
import androidx.recyclerview.widget.RecyclerView;
@@ -35,8 +37,8 @@
/**
* The default scroll bar widget for the {@link CarUiRecyclerView}.
*
- * <p>Inspired by {@link androidx.car.widget.PagedListView}. Most pagination and scrolling logic has
- * been ported from the PLV with minor updates.
+ * <p>Inspired by {@link androidx.car.widget.PagedListView}. Most pagination and scrolling logic
+ * has been ported from the PLV with minor updates.
*/
class DefaultScrollBar implements ScrollBar {
@@ -59,6 +61,9 @@
private OrientationHelper mOrientationHelper;
+ private OnContinuousScrollListener mPageUpOnContinuousScrollListener;
+ private OnContinuousScrollListener mPageDownOnContinuousScrollListener;
+
@Override
public void initialize(RecyclerView rv, View scrollView) {
mRecyclerView = rv;
@@ -77,14 +82,17 @@
mUpButton = requireViewByRefId(mScrollView, R.id.car_ui_scrollbar_page_up);
View.OnClickListener paginateUpButtonOnClickListener = v -> pageUp();
mUpButton.setOnClickListener(paginateUpButtonOnClickListener);
- mUpButton.setOnTouchListener(
- new OnContinuousScrollListener(rv.getContext(), paginateUpButtonOnClickListener));
+ mPageUpOnContinuousScrollListener = new OnContinuousScrollListener(rv.getContext(),
+ paginateUpButtonOnClickListener);
+ mUpButton.setOnTouchListener(mPageUpOnContinuousScrollListener);
+
mDownButton = requireViewByRefId(mScrollView, R.id.car_ui_scrollbar_page_down);
View.OnClickListener paginateDownButtonOnClickListener = v -> pageDown();
mDownButton.setOnClickListener(paginateDownButtonOnClickListener);
- mDownButton.setOnTouchListener(
- new OnContinuousScrollListener(rv.getContext(), paginateDownButtonOnClickListener));
+ mPageDownOnContinuousScrollListener = new OnContinuousScrollListener(rv.getContext(),
+ paginateDownButtonOnClickListener);
+ mDownButton.setOnTouchListener(mPageDownOnContinuousScrollListener);
mScrollTrack = requireViewByRefId(mScrollView, R.id.car_ui_scrollbar_track);
mScrollThumb = requireViewByRefId(mScrollView, R.id.car_ui_scrollbar_thumb);
@@ -125,12 +133,35 @@
mScrollView.getPaddingRight(), paddingEnd);
}
+ @Override
+ public void adapterChanged(@Nullable RecyclerView.Adapter adapter) {
+ try {
+ if (mRecyclerView.getAdapter() != null) {
+ mRecyclerView.getAdapter().unregisterAdapterDataObserver(mAdapterChangeObserver);
+ }
+ if (adapter != null) {
+ adapter.registerAdapterDataObserver(mAdapterChangeObserver);
+ }
+ } catch (IllegalStateException e) {
+ // adapter is already registered. and we're trying to register again.
+ // or adapter was not registered and we're trying to unregister again.
+ // ignore.
+ }
+ }
+
/**
* Sets whether or not the up button on the scroll bar is clickable.
*
* @param enabled {@code true} if the up button is enabled.
*/
private void setUpEnabled(boolean enabled) {
+ // If the button is held down the button is disabled, the MotionEvent.ACTION_UP event on
+ // button release will not be sent to cancel pending scrolls. Manually cancel any pending
+ // scroll.
+ if (!enabled) {
+ mPageUpOnContinuousScrollListener.cancelPendingScroll();
+ }
+
mUpButton.setEnabled(enabled);
mUpButton.setAlpha(enabled ? 1f : mButtonDisabledAlpha);
}
@@ -141,6 +172,13 @@
* @param enabled {@code true} if the down button is enabled.
*/
private void setDownEnabled(boolean enabled) {
+ // If the button is held down the button is disabled, the MotionEvent.ACTION_UP event on
+ // button release will not be sent to cancel pending scrolls. Manually cancel any pending
+ // scroll.
+ if (!enabled) {
+ mPageDownOnContinuousScrollListener.cancelPendingScroll();
+ }
+
mDownButton.setEnabled(enabled);
mDownButton.setAlpha(enabled ? 1f : mButtonDisabledAlpha);
}
@@ -160,10 +198,9 @@
* where the thumb should be; and finally, extent is the size of the thumb.
*
* <p>These values can be expressed in arbitrary units, so long as they share the same units.
- * The
- * values should also be positive.
+ * The values should also be positive.
*
- * @param range The range of the scrollbar's thumb
+ * @param range The range of the scrollbar's thumb
* @param offset The offset of the scrollbar's thumb
* @param extent The extent of the scrollbar's thumb
*/
@@ -199,7 +236,7 @@
* Calculates and returns how big the scroll bar thumb should be based on the given range and
* extent.
*
- * @param range The total amount of space the scroll bar is allowed to roam over.
+ * @param range The total amount of space the scroll bar is allowed to roam over.
* @param extent The amount of space that the scroll bar takes up relative to the range.
* @return The height of the scroll bar thumb in pixels.
*/
@@ -213,9 +250,9 @@
* Calculates and returns how much the scroll thumb should be offset from the top of where it
* has been laid out.
*
- * @param range The total amount of space the scroll bar is allowed to roam over.
- * @param offset The amount the scroll bar should be offset, expressed in the same units as
- * the given range.
+ * @param range The total amount of space the scroll bar is allowed to roam over.
+ * @param offset The amount the scroll bar should be offset, expressed in the same units as
+ * the given range.
* @param thumbLength The current length of the thumb in pixels.
* @return The amount the thumb should be offset in pixels.
*/
@@ -230,7 +267,9 @@
: mScrollTrack.getHeight() - thumbLength);
}
- /** Moves the given view to the specified 'y' position. */
+ /**
+ * Moves the given view to the specified 'y' position.
+ */
private void moveY(final View view, float newPosition) {
view.animate()
.y(newPosition)
@@ -244,8 +283,76 @@
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
updatePaginationButtons();
+ cacheChildrenHeight(recyclerView.getLayoutManager());
}
};
+ private final SparseArray<Integer> mChildHeightByAdapterPosition = new SparseArray();
+
+ private final RecyclerView.AdapterDataObserver mAdapterChangeObserver =
+ new RecyclerView.AdapterDataObserver() {
+ @Override
+ public void onChanged() {
+ clearCachedHeights();
+ }
+ @Override
+ public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
+ clearCachedHeights();
+ }
+ @Override
+ public void onItemRangeChanged(int positionStart, int itemCount) {
+ clearCachedHeights();
+ }
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ clearCachedHeights();
+ }
+ @Override
+ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ clearCachedHeights();
+ }
+ @Override
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ clearCachedHeights();
+ }
+ };
+
+ private void clearCachedHeights() {
+ mChildHeightByAdapterPosition.clear();
+ cacheChildrenHeight(mRecyclerView.getLayoutManager());
+ }
+
+ private void cacheChildrenHeight(@Nullable RecyclerView.LayoutManager layoutManager) {
+ if (layoutManager == null) {
+ return;
+ }
+ for (int i = 0; i < layoutManager.getChildCount(); i++) {
+ View child = layoutManager.getChildAt(i);
+ int childPosition = layoutManager.getPosition(child);
+ if (mChildHeightByAdapterPosition.indexOfKey(childPosition) < 0) {
+ mChildHeightByAdapterPosition.put(childPosition, child.getHeight());
+ }
+ }
+ }
+
+ private int estimateNextPositionScrollUp(int currentPos, int scrollDistance,
+ OrientationHelper orientationHelper) {
+ int nextPos = 0;
+ int distance = 0;
+ for (int i = currentPos - 1; i >= 0; i--) {
+ if (mChildHeightByAdapterPosition.indexOfKey(i) < 0) {
+ // Use the average height estimate when there is not enough data
+ nextPos = mSnapHelper.estimateNextPositionDiffForScrollDistance(orientationHelper,
+ -scrollDistance);
+ break;
+ }
+ if ((distance + mChildHeightByAdapterPosition.get(i)) > Math.abs(scrollDistance)) {
+ nextPos = i - currentPos + 1;
+ break;
+ }
+ distance += mChildHeightByAdapterPosition.get(i);
+ }
+ return nextPos;
+ }
private OrientationHelper getOrientationHelper(RecyclerView.LayoutManager layoutManager) {
if (mOrientationHelper == null || mOrientationHelper.getLayoutManager() != layoutManager) {
@@ -260,49 +367,52 @@
* {@code CarUiRecyclerView}.
*
* <p>The resulting first item in the list will be snapped to so that it is completely visible.
- * If
- * this is not possible due to the first item being taller than the containing {@code
+ * If this is not possible due to the first item being taller than the containing {@code
* CarUiRecyclerView}, then the snapping will not occur.
*/
void pageUp() {
int currentOffset = getRecyclerView().computeVerticalScrollOffset();
- if (getRecyclerView().getLayoutManager() == null
- || getRecyclerView().getChildCount() == 0
- || currentOffset == 0) {
+ RecyclerView.LayoutManager layoutManager = getRecyclerView().getLayoutManager();
+ if (layoutManager == null || layoutManager.getChildCount() == 0 || currentOffset == 0) {
return;
}
// Use OrientationHelper to calculate scroll distance in order to match snapping behavior.
- OrientationHelper orientationHelper =
- getOrientationHelper(getRecyclerView().getLayoutManager());
+ OrientationHelper orientationHelper = getOrientationHelper(layoutManager);
int screenSize = orientationHelper.getTotalSpace();
int scrollDistance = screenSize;
- // The iteration order matters. In case where there are 2 items longer than screen size, we
- // want to focus on upcoming view.
- for (int i = 0; i < getRecyclerView().getChildCount(); i++) {
- /*
- * We treat child View longer than screen size differently:
- * 1) When it enters screen, next pageUp will align its bottom with parent bottom;
- * 2) When it leaves screen, next pageUp will align its top with parent top.
- */
- View child = getRecyclerView().getChildAt(i);
- if (child.getHeight() > screenSize) {
- if (orientationHelper.getDecoratedEnd(child) < screenSize) {
- // Child view bottom is entering screen. Align its bottom with parent bottom.
- scrollDistance = screenSize - orientationHelper.getDecoratedEnd(child);
- } else if (-screenSize < orientationHelper.getDecoratedStart(child)
- && orientationHelper.getDecoratedStart(child) < 0) {
- // Child view top is about to enter screen - its distance to parent top
- // is less than a full scroll. Align child top with parent top.
- scrollDistance = Math.abs(orientationHelper.getDecoratedStart(child));
- }
- // There can be two items that are longer than the screen. We stop at the first one.
- // This is affected by the iteration order.
+
+ View currentPosView = getFirstMostVisibleChild(orientationHelper);
+ int currentPos = currentPosView != null ? mRecyclerView.getLayoutManager().getPosition(
+ currentPosView) : 0;
+ int nextPos = estimateNextPositionScrollUp(currentPos,
+ scrollDistance - Math.max(0, orientationHelper.getStartAfterPadding()
+ - orientationHelper.getDecoratedStart(currentPosView)), orientationHelper);
+ if (nextPos == 0) {
+ // Distance should always be positive. Negate its value to scroll up.
+ mRecyclerView.smoothScrollBy(0, -scrollDistance);
+ } else {
+ mRecyclerView.smoothScrollToPosition(Math.max(0, currentPos + nextPos));
+ }
+ }
+
+ private View getFirstMostVisibleChild(OrientationHelper helper) {
+ float mostVisiblePercent = 0;
+ View mostVisibleView = null;
+
+ for (int i = 0; i < getRecyclerView().getLayoutManager().getChildCount(); i++) {
+ View child = getRecyclerView().getLayoutManager().getChildAt(i);
+ float visiblePercentage = CarUiSnapHelper.getPercentageVisible(child, helper);
+ if (visiblePercentage == 1f) {
+ mostVisibleView = child;
break;
+ } else if (visiblePercentage > mostVisiblePercent) {
+ mostVisiblePercent = visiblePercentage;
+ mostVisibleView = child;
}
}
- // Distance should always be positive. Negate its value to scroll up.
- mRecyclerView.smoothScrollBy(0, -scrollDistance);
+
+ return mostVisibleView;
}
/**
@@ -314,53 +424,55 @@
* scrolled the length of a page, but not snapped to.
*/
void pageDown() {
- if (getRecyclerView().getLayoutManager() == null
- || getRecyclerView().getChildCount() == 0) {
+ RecyclerView.LayoutManager layoutManager = getRecyclerView().getLayoutManager();
+ if (layoutManager == null || layoutManager.getChildCount() == 0) {
return;
}
- OrientationHelper orientationHelper =
- getOrientationHelper(getRecyclerView().getLayoutManager());
+ OrientationHelper orientationHelper = getOrientationHelper(layoutManager);
int screenSize = orientationHelper.getTotalSpace();
int scrollDistance = screenSize;
- // If the last item is partially visible, page down should bring it to the top.
- View lastChild = getRecyclerView().getChildAt(getRecyclerView().getChildCount() - 1);
- if (getRecyclerView().getLayoutManager().isViewPartiallyVisible(lastChild,
- /* completelyVisible= */ false, /* acceptEndPointInclusion= */ false)) {
- scrollDistance = orientationHelper.getDecoratedStart(lastChild)
- - orientationHelper.getStartAfterPadding();
- if (scrollDistance <= 0) {
- // - Scroll value is zero if the top of last item is aligned with top of the screen;
- // - Scroll value can be negative if the child is longer than the screen size and
- // the visible area of the screen does not show the start of the child.
- // Scroll to the next screen in both cases.
- scrollDistance = screenSize;
- }
+ View currentPosView = getFirstMostVisibleChild(orientationHelper);
+
+ // If current view is partially visible and bottom of the view is below visible area of
+ // the recyclerview either scroll down one page (screenSize) or enough to align the bottom
+ // of the view with the bottom of the recyclerview. Note that this will not cause a snap,
+ // because the current view is already snapped to the top or it wouldn't be the most
+ // visible view.
+ if (layoutManager.isViewPartiallyVisible(currentPosView,
+ /* completelyVisible= */ false, /* acceptEndPointInclusion= */ false)
+ && orientationHelper.getDecoratedEnd(currentPosView)
+ > orientationHelper.getEndAfterPadding()) {
+ scrollDistance = Math.min(screenSize,
+ orientationHelper.getDecoratedEnd(currentPosView)
+ - orientationHelper.getEndAfterPadding());
}
- // The iteration order matters. In case where there are 2 items longer than screen size, we
- // want to focus on upcoming view (the one at the bottom of screen).
- for (int i = getRecyclerView().getChildCount() - 1; i >= 0; i--) {
- /* We treat child View longer than screen size differently:
- * 1) When it enters screen, next pageDown will align its top with parent top;
- * 2) When it leaves screen, next pageDown will align its bottom with parent bottom.
- */
- View child = getRecyclerView().getChildAt(i);
- if (child.getHeight() > screenSize) {
- if (orientationHelper.getDecoratedStart(child)
- - orientationHelper.getStartAfterPadding() > 0) {
- // Child view top is entering screen. Align its top with parent top.
- scrollDistance = orientationHelper.getDecoratedStart(lastChild)
+ // Iterate over the childview (bottom to top) and stop when we find the first
+ // view that we can snap to and the scroll size is less than max scroll size (screenSize)
+ for (int i = layoutManager.getChildCount() - 1; i >= 0; i--) {
+ View child = layoutManager.getChildAt(i);
+
+ // Ignore the child if it's above the currentview, as scrolldown will only move down.
+ // Note that in case of gridview, child will not be the same as the currentview.
+ if (orientationHelper.getDecoratedStart(child)
+ <= orientationHelper.getDecoratedStart(currentPosView)) {
+ break;
+ }
+
+ // Ignore the child if the scroll distance is bigger than the max scroll size
+ if (orientationHelper.getDecoratedStart(child)
+ - orientationHelper.getStartAfterPadding() <= screenSize) {
+ // If the child is already fully visible we can scroll even further.
+ if (orientationHelper.getDecoratedEnd(child)
+ <= orientationHelper.getEndAfterPadding()) {
+ scrollDistance = orientationHelper.getDecoratedEnd(child)
- orientationHelper.getStartAfterPadding();
- } else if (screenSize < orientationHelper.getDecoratedEnd(child)
- && orientationHelper.getDecoratedEnd(child) < 2 * screenSize) {
- // Child view bottom is about to enter screen - its distance to parent bottom
- // is less than a full scroll. Align child bottom with parent bottom.
- scrollDistance = orientationHelper.getDecoratedEnd(child) - screenSize;
+ } else {
+ scrollDistance = orientationHelper.getDecoratedStart(child)
+ - orientationHelper.getStartAfterPadding();
}
- // There can be two items that are longer than the screen. We stop at the first one.
- // This is affected by the iteration order.
break;
}
}
@@ -371,12 +483,9 @@
/**
* Determines if scrollbar should be visible or not and shows/hides it accordingly. If this is
* being called as a result of adapter changes, it should be called after the new layout has
- * been
- * calculated because the method of determining scrollbar visibility uses the current layout.
- * If
- * this is called after an adapter change but before the new layout, the visibility
- * determination
- * may not be correct.
+ * been calculated because the method of determining scrollbar visibility uses the current
+ * layout. If this is called after an adapter change but before the new layout, the visibility
+ * determination may not be correct.
*/
private void updatePaginationButtons() {
@@ -387,6 +496,7 @@
// enable/disable the button before the view is shown. So there is no flicker.
setUpEnabled(!isAtStart);
setDownEnabled(!isAtEnd);
+
if ((isAtStart && isAtEnd) || layoutManager == null || layoutManager.getItemCount() == 0) {
mScrollView.setVisibility(View.INVISIBLE);
} else {
@@ -412,12 +522,16 @@
mScrollView.invalidate();
}
- /** Returns {@code true} if the RecyclerView is completely displaying the first item. */
+ /**
+ * Returns {@code true} if the RecyclerView is completely displaying the first item.
+ */
boolean isAtStart() {
return mSnapHelper.isAtStart(getRecyclerView().getLayoutManager());
}
- /** Returns {@code true} if the RecyclerView is completely displaying the last item. */
+ /**
+ * Returns {@code true} if the RecyclerView is completely displaying the last item.
+ */
boolean isAtEnd() {
return mSnapHelper.isAtEnd(getRecyclerView().getLayoutManager());
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/OnContinuousScrollListener.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/OnContinuousScrollListener.java
index 4fafc8d..48570c6 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/OnContinuousScrollListener.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/OnContinuousScrollListener.java
@@ -28,16 +28,15 @@
import com.android.car.ui.R;
/**
- * A class, that can be used as a TouchListener on any view (e.g. a Button).
- * It periodically calls the provided clickListener. The first callback is fired after the
- * initial Delay, and subsequent ones after the defined interval.
+ * A class, that can be used as a TouchListener on any view (e.g. a Button). It periodically calls
+ * the provided clickListener. The first callback is fired after the initial Delay, and subsequent
+ * ones after the defined interval.
*/
public class OnContinuousScrollListener implements OnTouchListener {
- private Handler mHandler = new Handler();
-
- private int mInitialDelay;
- private int mRepeatInterval;
+ private final Handler mHandler = new Handler();
+ private final int mInitialDelay;
+ private final int mRepeatInterval;
private final OnClickListener mOnClickListener;
private View mTouchedView;
private boolean mIsLongPressed;
@@ -45,7 +44,7 @@
/**
* Notifies listener and self schedules to be re-run at next callback interval.
*/
- private Runnable mPeriodicRunnable = new Runnable() {
+ private final Runnable mPeriodicRunnable = new Runnable() {
@Override
public void run() {
if (mTouchedView.isEnabled()) {
@@ -59,14 +58,12 @@
};
/**
- * @param clickListener The OnClickListener, that will be called
- * periodically
+ * @param clickListener The OnClickListener, that will be called periodically
*/
public OnContinuousScrollListener(@NonNull Context context,
@NonNull OnClickListener clickListener) {
this.mInitialDelay = context.getResources().getInteger(
R.integer.car_ui_scrollbar_longpress_initial_delay);
-
this.mRepeatInterval = context.getResources().getInteger(
R.integer.car_ui_scrollbar_longpress_repeat_interval);
@@ -76,6 +73,15 @@
this.mOnClickListener = clickListener;
}
+ /**
+ * Cancel pending scroll operations. Any scroll operations that were scheduled to possibly be
+ * performed, as part of a continuous scroll, will be cancelled.
+ */
+ public void cancelPendingScroll() {
+ mHandler.removeCallbacks(mPeriodicRunnable);
+ mIsLongPressed = false;
+ }
+
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
mTouchedView = view;
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/ScrollBar.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/ScrollBar.java
index 8e127f0..9f4ee97 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/ScrollBar.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/ScrollBar.java
@@ -39,4 +39,9 @@
/** Sets the padding of the scrollbar, relative to the padding of the RecyclerView. */
void setPadding(int paddingStart, int paddingEnd);
+
+ /**
+ * Called when recyclerview's setAdapter is called.
+ */
+ void adapterChanged(RecyclerView.Adapter adapter);
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/decorations/grid/GridDividerItemDecoration.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/decorations/grid/GridDividerItemDecoration.java
index abb90ca..5eb090a 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/decorations/grid/GridDividerItemDecoration.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/decorations/grid/GridDividerItemDecoration.java
@@ -25,6 +25,8 @@
import com.android.car.ui.R;
+import java.util.Objects;
+
/** Adds interior dividers to a RecyclerView with a GridLayoutManager. */
public class GridDividerItemDecoration extends RecyclerView.ItemDecoration {
@@ -89,7 +91,9 @@
* @param parent The RecyclerView onto which dividers are being added
*/
private void drawHorizontalDividers(Canvas canvas, RecyclerView parent) {
- int childCount = parent.getChildCount();
+ RecyclerView.LayoutManager layoutManager = Objects.requireNonNull(
+ parent.getLayoutManager());
+ int childCount = layoutManager.getChildCount();
int rowCount = childCount / mNumColumns;
int lastRowChildCount = childCount % mNumColumns;
int lastColumn = Math.min(childCount, mNumColumns);
@@ -102,8 +106,9 @@
lastRowChildIndex = i + ((rowCount - 1) * mNumColumns);
}
- View firstRowChild = parent.getChildAt(i);
- View lastRowChild = parent.getChildAt(lastRowChildIndex);
+
+ View firstRowChild = layoutManager.getChildAt(i);
+ View lastRowChild = layoutManager.getChildAt(lastRowChildIndex);
int dividerTop =
firstRowChild.getTop() + (int) parent.getContext().getResources().getDimension(
@@ -130,7 +135,9 @@
* @param parent The RecyclerView onto which dividers are being added
*/
private void drawVerticalDividers(Canvas canvas, RecyclerView parent) {
- double childCount = parent.getChildCount();
+ RecyclerView.LayoutManager layoutManager = Objects.requireNonNull(
+ parent.getLayoutManager());
+ double childCount = layoutManager.getChildCount();
double rowCount = Math.ceil(childCount / mNumColumns);
int rightmostChildIndex;
for (int i = 1; i <= rowCount; i++) {
@@ -144,8 +151,8 @@
rightmostChildIndex = (i * mNumColumns) - 1;
}
- View leftmostChild = parent.getChildAt(mNumColumns * (i - 1));
- View rightmostChild = parent.getChildAt(rightmostChildIndex);
+ View leftmostChild = layoutManager.getChildAt(mNumColumns * (i - 1));
+ View rightmostChild = layoutManager.getChildAt(rightmostChildIndex);
// draws on top of each row.
int dividerLeft =
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/decorations/linear/LinearDividerItemDecoration.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/decorations/linear/LinearDividerItemDecoration.java
index 4d5e6bd..3ab24aa 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/decorations/linear/LinearDividerItemDecoration.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/decorations/linear/LinearDividerItemDecoration.java
@@ -104,9 +104,10 @@
- (int) parent.getContext().getResources().getDimension(
R.dimen.car_ui_recyclerview_divider_bottom_margin);
- int childCount = parent.getChildCount();
+ RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
+ int childCount = layoutManager.getChildCount();
for (int i = 0; i < childCount - 1; i++) {
- View child = parent.getChildAt(i);
+ View child = layoutManager.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
@@ -133,9 +134,10 @@
- (int) parent.getContext().getResources().getDimension(
R.dimen.car_ui_recyclerview_divider_end_margin);
- int childCount = parent.getChildCount();
+ RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
+ int childCount = layoutManager.getChildCount();
for (int i = 0; i < childCount - 1; i++) {
- View child = parent.getChildAt(i);
+ View child = layoutManager.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/decorations/linear/LinearOffsetItemDecoration.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/decorations/linear/LinearOffsetItemDecoration.java
index 33e14db..62d361e 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/decorations/linear/LinearOffsetItemDecoration.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/decorations/linear/LinearOffsetItemDecoration.java
@@ -169,7 +169,8 @@
if (mOffsetPosition == OffsetPosition.START) {
parentLeft = parent.getPaddingLeft();
} else {
- View lastChild = parent.getChildAt(parent.getChildCount() - 1);
+ RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
+ View lastChild = layoutManager.getChildAt(layoutManager.getChildCount() - 1);
RecyclerView.LayoutParams lastChildLayoutParams =
(RecyclerView.LayoutParams) lastChild.getLayoutParams();
parentLeft = lastChild.getRight() + lastChildLayoutParams.rightMargin;
@@ -190,7 +191,8 @@
if (mOffsetPosition == OffsetPosition.START) {
parentTop = parent.getPaddingTop();
} else {
- View lastChild = parent.getChildAt(parent.getChildCount() - 1);
+ RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
+ View lastChild = layoutManager.getChildAt(layoutManager.getChildCount() - 1);
RecyclerView.LayoutParams lastChildLayoutParams =
(RecyclerView.LayoutParams) lastChild.getLayoutParams();
parentTop = lastChild.getBottom() + lastChildLayoutParams.bottomMargin;
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/CarUiEditText.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/CarUiEditText.java
new file mode 100644
index 0000000..1f1a07c
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/CarUiEditText.java
@@ -0,0 +1,119 @@
+/*
+ * 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.ui.toolbar;
+
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.CONTENT_AREA_SURFACE_DISPLAY_ID;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.CONTENT_AREA_SURFACE_HEIGHT;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.CONTENT_AREA_SURFACE_HOST_TOKEN;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.CONTENT_AREA_SURFACE_WIDTH;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.SEARCH_RESULT_ITEM_ID_LIST;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.SEARCH_RESULT_SUPPLEMENTAL_ICON_ID_LIST;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.WIDE_SCREEN_CLEAR_DATA_ACTION;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.AttributeSet;
+import android.widget.EditText;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Edit text supporting the callbacks from the IMS. This will be useful in widescreen IME mode to
+ * allow car-ui-lib to receive responses (like onClick events) from the IMS
+ */
+class CarUiEditText extends EditText {
+
+ private final Set<PrivateImeCommandCallback> mPrivateImeCommandCallback = new HashSet<>();
+
+ // These need to be public for the layout inflater to inflate them, but
+ // checkstyle complains about a public constructor on a package-private class
+ //CHECKSTYLE:OFF Generated code
+ public CarUiEditText(Context context) {
+ super(context);
+ }
+
+ public CarUiEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CarUiEditText(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public CarUiEditText(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+ //CHECKSTYLE:ON Generated code
+
+ @Override
+ public boolean onPrivateIMECommand(String action, Bundle data) {
+
+ if (WIDE_SCREEN_CLEAR_DATA_ACTION.equals(action)) {
+ // clear the text.
+ setText("");
+ }
+
+ if (data == null || mPrivateImeCommandCallback == null) {
+ return false;
+ }
+
+ if (data.getString(SEARCH_RESULT_ITEM_ID_LIST) != null) {
+ for (PrivateImeCommandCallback listener : mPrivateImeCommandCallback) {
+ listener.onItemClicked(data.getString(SEARCH_RESULT_ITEM_ID_LIST));
+ }
+ }
+
+ if (data.getString(SEARCH_RESULT_SUPPLEMENTAL_ICON_ID_LIST) != null) {
+ for (PrivateImeCommandCallback listener : mPrivateImeCommandCallback) {
+ listener.onSecondaryImageClicked(
+ data.getString(SEARCH_RESULT_SUPPLEMENTAL_ICON_ID_LIST));
+ }
+ }
+
+ int displayId = data.getInt(CONTENT_AREA_SURFACE_DISPLAY_ID);
+ int height = data.getInt(CONTENT_AREA_SURFACE_HEIGHT);
+ int width = data.getInt(CONTENT_AREA_SURFACE_WIDTH);
+ IBinder binder = data.getBinder(CONTENT_AREA_SURFACE_HOST_TOKEN);
+
+ if (binder != null) {
+ for (PrivateImeCommandCallback listener : mPrivateImeCommandCallback) {
+ listener.onSurfaceInfo(displayId, binder, height, width);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Registers a new {@link PrivateImeCommandCallback} to the list of
+ * listeners.
+ */
+ public void registerOnPrivateImeCommandListener(PrivateImeCommandCallback listener) {
+ mPrivateImeCommandCallback.add(listener);
+ }
+
+ /**
+ * Unregisters an existing {@link PrivateImeCommandCallback} from the list
+ * of listeners.
+ */
+ public boolean unregisterOnPrivateImeCommandListener(PrivateImeCommandCallback listener) {
+ return mPrivateImeCommandCallback.remove(listener);
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItem.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItem.java
index 6ee35d3..27f8140 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItem.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItem.java
@@ -54,6 +54,7 @@
private final boolean mIsSearch;
private final boolean mShowIconAndTitle;
private final boolean mIsTinted;
+ private final boolean mIsPrimary;
@CarUxRestrictions.CarUxRestrictionsInfo
private int mId;
@@ -88,6 +89,7 @@
mIsSearch = builder.mIsSearch;
mShowIconAndTitle = builder.mShowIconAndTitle;
mIsTinted = builder.mIsTinted;
+ mIsPrimary = builder.mIsPrimary;
mUxRestrictions = builder.mUxRestrictions;
mCurrentRestrictions = CarUxRestrictionsUtil.getInstance(mContext).getCurrentRestrictions();
@@ -300,6 +302,14 @@
: mContext.getDrawable(resId));
}
+ /**
+ * Returns if this MenuItem is a primary MenuItem, which means it should be visually
+ * distinct to indicate that.
+ */
+ public boolean isPrimary() {
+ return mIsPrimary;
+ }
+
/** Returns if this is the search MenuItem, which has special behavior when searching */
boolean isSearch() {
return mIsSearch;
@@ -329,6 +339,7 @@
private boolean mIsActivated = false;
private boolean mIsSearch = false;
private boolean mIsSettings = false;
+ private boolean mIsPrimary = false;
@CarUxRestrictions.CarUxRestrictionsInfo
private int mUxRestrictions = CarUxRestrictions.UX_RESTRICTIONS_BASELINE;
@@ -508,6 +519,14 @@
}
/**
+ * Sets whether the MenuItem is primary. This is just a visual change.
+ */
+ public Builder setPrimary(boolean primary) {
+ mIsPrimary = primary;
+ return this;
+ }
+
+ /**
* Sets under what {@link android.car.drivingstate.CarUxRestrictions.CarUxRestrictionsInfo}
* the MenuItem should be restricted.
*/
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItemRenderer.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItemRenderer.java
index 85141f1..ca3364b 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItemRenderer.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItemRenderer.java
@@ -32,6 +32,7 @@
import android.widget.Switch;
import android.widget.TextView;
+import androidx.annotation.LayoutRes;
import androidx.annotation.XmlRes;
import androidx.asynclayoutinflater.view.AsyncLayoutInflater;
import androidx.core.util.Consumer;
@@ -63,12 +64,9 @@
private View mIconContainer;
private ImageView mIconView;
private Switch mSwitch;
- private View mTextContainer;
private TextView mTextView;
private TextView mTextWithIconView;
-
- /** Whether the layout file supports rotary mode. */
- private boolean mIsRotaryEnabledLayout;
+ private boolean mIndividualClickListeners;
MenuItemRenderer(MenuItem item, ViewGroup parentView) {
mMenuItem = item;
@@ -98,7 +96,10 @@
void createView(Consumer<View> callback) {
AsyncLayoutInflater inflater = new AsyncLayoutInflater(mParentView.getContext());
- inflater.inflate(R.layout.car_ui_toolbar_menu_item, mParentView, (View view, int resid,
+ @LayoutRes int layout = mMenuItem.isPrimary()
+ ? R.layout.car_ui_toolbar_menu_item_primary
+ : R.layout.car_ui_toolbar_menu_item;
+ inflater.inflate(layout, mParentView, (View view, int resid,
ViewGroup parent) -> {
mView = view;
@@ -106,13 +107,11 @@
requireViewByRefId(mView, R.id.car_ui_toolbar_menu_item_icon_container);
mIconView = requireViewByRefId(mView, R.id.car_ui_toolbar_menu_item_icon);
mSwitch = requireViewByRefId(mView, R.id.car_ui_toolbar_menu_item_switch);
- // mTextContainer is only available in rotary enabled layout.
- mTextContainer =
- CarUiUtils.findViewByRefId(mView, R.id.car_ui_toolbar_menu_item_text_container);
- mIsRotaryEnabledLayout = mTextContainer != null;
mTextView = requireViewByRefId(mView, R.id.car_ui_toolbar_menu_item_text);
mTextWithIconView =
requireViewByRefId(mView, R.id.car_ui_toolbar_menu_item_text_with_icon);
+ mIndividualClickListeners = mView.getContext().getResources()
+ .getBoolean(R.bool.car_ui_toolbar_menuitem_individual_click_listeners);
updateView();
callback.accept(mView);
@@ -140,36 +139,31 @@
mView.setVisibility(View.VISIBLE);
mView.setContentDescription(mMenuItem.getTitle());
- int iconContainerVisibility = View.GONE;
- int textContainerVisibility = View.GONE;
- mTextView.setVisibility(View.GONE);
- mTextWithIconView.setVisibility(View.GONE);
- mSwitch.setVisibility(View.GONE);
+ View clickTarget;
if (checkable) {
mSwitch.setChecked(mMenuItem.isChecked());
- mSwitch.setVisibility(View.VISIBLE);
- if (mIsRotaryEnabledLayout) {
- iconContainerVisibility = View.VISIBLE;
- }
+ clickTarget = mSwitch;
} else if (hasText && hasIcon && textAndIcon) {
mMenuItem.getIcon().setBounds(0, 0, mMenuItemIconSize, mMenuItemIconSize);
mTextWithIconView.setCompoundDrawables(mMenuItem.getIcon(), null, null, null);
mTextWithIconView.setText(mMenuItem.getTitle());
- mTextWithIconView.setVisibility(View.VISIBLE);
- textContainerVisibility = View.VISIBLE;
+ clickTarget = mTextWithIconView;
} else if (hasIcon) {
mIconView.setImageDrawable(mMenuItem.getIcon());
- iconContainerVisibility = View.VISIBLE;
+ clickTarget = mIconContainer;
} else { // hasText will be true
mTextView.setText(mMenuItem.getTitle());
- mTextView.setVisibility(View.VISIBLE);
- textContainerVisibility = View.VISIBLE;
+ clickTarget = mTextView;
}
- // Unlike other views, we should only update the visibility of mIconContainer and
- // mTextContainer once, otherwise rotary focus might break.
- mIconContainer.setVisibility(iconContainerVisibility);
- if (mTextContainer != null) {
- mTextContainer.setVisibility(textContainerVisibility);
+
+ mIconContainer.setVisibility(clickTarget == mIconContainer ? View.VISIBLE : View.GONE);
+ mTextView.setVisibility(clickTarget == mTextView ? View.VISIBLE : View.GONE);
+ mTextWithIconView.setVisibility(clickTarget == mTextWithIconView
+ ? View.VISIBLE : View.GONE);
+ mSwitch.setVisibility(clickTarget == mSwitch ? View.VISIBLE : View.GONE);
+
+ if (!mIndividualClickListeners) {
+ clickTarget = mView;
}
if (!mMenuItem.isTinted() && hasIcon) {
@@ -179,19 +173,13 @@
recursiveSetEnabledAndDrawableState(mView);
mView.setActivated(mMenuItem.isActivated());
- View clickTarget = null;
- if (mIsRotaryEnabledLayout) {
- clickTarget = iconContainerVisibility == View.VISIBLE ? mIconContainer : mTextContainer;
- } else {
- clickTarget = mView;
- }
if (mMenuItem.getOnClickListener() != null
|| mMenuItem.isCheckable()
|| mMenuItem.isActivatable()) {
clickTarget.setOnClickListener(v -> mMenuItem.performClick());
- } else {
- clickTarget.setOnClickListener(null);
- clickTarget.setClickable(false);
+ } else if (clickTarget == mView) {
+ mView.setOnClickListener(null);
+ mView.setClickable(false);
}
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/PrivateImeCommandCallback.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/PrivateImeCommandCallback.java
new file mode 100644
index 0000000..49e7fa4
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/PrivateImeCommandCallback.java
@@ -0,0 +1,52 @@
+/*
+ * 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.ui.toolbar;
+
+import android.os.IBinder;
+
+/**
+ * Interface for {@link CarUiEditText} to support different actions and callbacks from IME
+ * when running in wide screen mode.
+ */
+public interface PrivateImeCommandCallback {
+ /**
+ * Called when user clicks on an item in the search results.
+ *
+ * @param itemId the id of the item clicked. This will be the same id that was passed by the
+ * application to the IMS template to display search results.
+ */
+ void onItemClicked(String itemId);
+
+ /**
+ * Called when user clicks on a secondary image within item in the search results.
+ *
+ * @param secondaryImageId the id of the secondary image clicked. This will be the same id
+ * that was passed by the application to the IMS template to display
+ * search results.
+ */
+ void onSecondaryImageClicked(String secondaryImageId);
+
+ /**
+ * Called when the edit text is clicked and IME is about to launch. IME provides the surface
+ * view information through this call that applications can use to display a view on the
+ * IME surface.
+ *
+ * This method will NOT be called if an OEM has not allowed an application to hide the
+ * content area.
+ */
+ void onSurfaceInfo(int displayId, IBinder binder, int height, int width);
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/SearchView.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/SearchView.java
index 9506fe1..dd6f056 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/SearchView.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/SearchView.java
@@ -15,16 +15,38 @@
*/
package com.android.car.ui.toolbar;
+import static android.view.WindowInsets.Type.ime;
+
+import static com.android.car.ui.core.SearchResultsProvider.CONTENT;
+import static com.android.car.ui.core.SearchResultsProvider.SEARCH_RESULTS_PROVIDER;
+import static com.android.car.ui.core.SearchResultsProvider.SEARCH_RESULTS_TABLE_NAME;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.CONTENT_AREA_SURFACE_PACKAGE;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.WIDE_SCREEN_ACTION;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.WIDE_SCREEN_SEARCH_RESULTS;
import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+import android.content.ContentValues;
import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
+import android.hardware.display.DisplayManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Parcel;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
+import android.view.Display;
import android.view.KeyEvent;
import android.view.LayoutInflater;
+import android.view.SurfaceControlViewHost;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
@@ -32,11 +54,20 @@
import android.widget.ImageView;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.car.ui.R;
+import com.android.car.ui.core.SearchResultsProvider;
+import com.android.car.ui.imewidescreen.CarUiImeSearchListItem;
+import com.android.car.ui.recyclerview.CarUiContentListItem;
+import com.android.car.ui.recyclerview.CarUiListItem;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -50,6 +81,16 @@
private final int mStartPaddingWithoutIcon;
private final int mStartPadding;
private final int mEndPadding;
+ @Nullable
+ private View mWideScreenImeContentAreaView;
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+ private SurfaceControlViewHost mSurfaceControlViewHost;
+ private int mSurfaceHeight;
+ private int mSurfaceWidth;
+ private List<? extends CarUiImeSearchListItem> mWideScreenSearchItemList;
+ private final Map<String, CarUiImeSearchListItem> mIdToListItem = new HashMap<>();
+
private Set<Toolbar.OnSearchListener> mSearchListeners = Collections.emptySet();
private Set<Toolbar.OnSearchCompletedListener> mSearchCompletedListeners =
Collections.emptySet();
@@ -82,7 +123,7 @@
super(context, attrs, defStyleAttr);
mInputMethodManager = (InputMethodManager)
- getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
LayoutInflater inflater = LayoutInflater.from(context);
inflater.inflate(R.layout.car_ui_toolbar_search_view, this, true);
@@ -91,7 +132,13 @@
mIcon = requireViewByRefId(this, R.id.car_ui_toolbar_search_icon);
mCloseIcon = requireViewByRefId(this, R.id.car_ui_toolbar_search_close);
- mCloseIcon.setOnClickListener(view -> mSearchText.getText().clear());
+ mCloseIcon.setOnClickListener(view -> {
+ if (view.isFocused()) {
+ mSearchText.requestFocus();
+ mInputMethodManager.showSoftInput(mSearchText, 0);
+ }
+ mSearchText.getText().clear();
+ });
mCloseIcon.setVisibility(View.GONE);
mStartPaddingWithoutIcon = mSearchText.getPaddingStart();
@@ -103,11 +150,11 @@
mSearchText.setSaveEnabled(false);
mSearchText.setPaddingRelative(mStartPadding, 0, mEndPadding, 0);
+ mSearchText.setOnClickListener((view) -> mInputMethodManager.showSoftInput(view, 0));
+
mSearchText.setOnFocusChangeListener(
(view, hasFocus) -> {
- if (hasFocus) {
- mInputMethodManager.showSoftInput(view, 0);
- } else {
+ if (!hasFocus) {
mInputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
});
@@ -128,6 +175,46 @@
}
return false;
});
+
+ if (mSearchText instanceof CarUiEditText) {
+ ((CarUiEditText) mSearchText).registerOnPrivateImeCommandListener(
+ new SearchViewImeCallback());
+ }
+ }
+
+ /**
+ * Apply window inset listener to the search container.
+ */
+ void installWindowInsetsListener(View searchContainer) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ // WindowInsets.isVisible() is only available on R or above
+ return;
+ }
+
+ searchContainer.getRootView().setOnApplyWindowInsetsListener((v, insets) -> {
+ if (insets.isVisible(ime())) {
+ displaySearchWideScreen();
+ mHandler.post(() -> {
+ if (mSurfaceControlViewHost != null && mWideScreenImeContentAreaView != null
+ && mSurfaceControlViewHost.getView() == null) {
+ mSurfaceControlViewHost.setView(
+ mWideScreenImeContentAreaView, mSurfaceWidth, mSurfaceHeight);
+ }
+ });
+ }
+ return v.onApplyWindowInsets(insets);
+ });
+ }
+
+ void setViewToImeWideScreenSurface(View view) {
+ if (view == null && mSurfaceControlViewHost != null) {
+ mSurfaceControlViewHost.release();
+ }
+
+ if (view != null && view.getParent() != null) {
+ throw new IllegalStateException("view should not have a parent");
+ }
+ mWideScreenImeContentAreaView = view;
}
private boolean isEnter(KeyEvent event) {
@@ -159,33 +246,87 @@
boolean hasQuery = mSearchText.getText().length() > 0;
mCloseIcon.setVisibility(hasQuery ? View.VISIBLE : View.GONE);
mSearchText.requestFocus();
+ mInputMethodManager.showSoftInput(mSearchText, 0);
}
mWasShown = isShown;
}
/**
- * Adds a listener for the search text changing.
- * See also {@link #unregisterOnSearchListener(Toolbar.OnSearchListener)}
+ * Sets a listener for the search text changing.
*/
public void setSearchListeners(Set<Toolbar.OnSearchListener> listeners) {
mSearchListeners = listeners;
}
/**
- * Removes a search listener.
- * See also {@link #registerOnSearchListener(Toolbar.OnSearchListener)}
+ * Sets a listener for the user completing their search, for example by clicking the
+ * enter/search button on the keyboard.
*/
public void setSearchCompletedListeners(Set<Toolbar.OnSearchCompletedListener> listeners) {
mSearchCompletedListeners = listeners;
}
/**
- * Sets the search hint.
- *
- * @param resId A string resource id of the search hint.
+ * Sets list of search item {@link CarUiListItem} to be displayed in the IMS
+ * template.
*/
- public void setHint(int resId) {
- mSearchText.setHint(resId);
+ public void setSearchItemsForWideScreen(List<? extends CarUiImeSearchListItem> searchItems) {
+ mWideScreenSearchItemList = searchItems != null ? new ArrayList<>(searchItems) : null;
+ displaySearchWideScreen();
+ }
+
+ private void displaySearchWideScreen() {
+ String url = CONTENT + getContext().getPackageName() + SEARCH_RESULTS_PROVIDER + "/"
+ + SEARCH_RESULTS_TABLE_NAME;
+ Uri contentUri = Uri.parse(url);
+ mIdToListItem.clear();
+ // clear the table.
+ getContext().getContentResolver().delete(contentUri, null, null);
+
+ // mWideScreenImeContentAreaView will only be set when running in widescreen mode and
+ // apps allowed by OEMs are trying to set their own view. In that case we did not want to
+ // send the information to IME for templatized solution.
+ if (mWideScreenImeContentAreaView != null) {
+ return;
+ }
+
+ if (mWideScreenSearchItemList == null) {
+ mInputMethodManager.sendAppPrivateCommand(mSearchText, WIDE_SCREEN_ACTION, null);
+ return;
+ }
+
+ int id = 0;
+
+ for (CarUiImeSearchListItem item : mWideScreenSearchItemList) {
+ ContentValues values = new ContentValues();
+ String idString = String.valueOf(id);
+ values.put(SearchResultsProvider.ITEM_ID, id);
+ values.put(SearchResultsProvider.SECONDARY_IMAGE_ID, id);
+ BitmapDrawable icon = (BitmapDrawable) item.getIcon();
+ values.put(SearchResultsProvider.PRIMARY_IMAGE_BLOB,
+ icon != null ? bitmapToByteArray(icon.getBitmap()) : null);
+ BitmapDrawable supplementalIcon = (BitmapDrawable) item.getSupplementalIcon();
+ values.put(SearchResultsProvider.SECONDARY_IMAGE_BLOB,
+ supplementalIcon != null ? bitmapToByteArray(supplementalIcon.getBitmap())
+ : null);
+ values.put(SearchResultsProvider.TITLE,
+ item.getTitle() != null ? item.getTitle().toString() : null);
+ values.put(SearchResultsProvider.SUBTITLE,
+ item.getBody() != null ? item.getBody().toString() : null);
+ getContext().getContentResolver().insert(contentUri, values);
+ mIdToListItem.put(idString, item);
+ id++;
+ }
+ mInputMethodManager.sendAppPrivateCommand(mSearchText, WIDE_SCREEN_SEARCH_RESULTS,
+ new Bundle());
+ }
+
+ private byte[] bitmapToByteArray(Bitmap bitmap) {
+ Parcel parcel = Parcel.obtain();
+ bitmap.writeToParcel(parcel, 0);
+ byte[] bytes = parcel.marshall();
+ parcel.recycle();
+ return bytes;
}
/**
@@ -197,11 +338,6 @@
mSearchText.setHint(hint);
}
- /** Gets the search hint */
- public CharSequence getHint() {
- return mSearchText.getHint();
- }
-
/**
* Sets a custom icon to display in the search box.
*/
@@ -261,4 +397,62 @@
mSearchText.setText(query);
mSearchText.setSelection(mSearchText.getText().length());
}
+
+ private class SearchViewImeCallback implements PrivateImeCommandCallback {
+
+ @Override
+ public void onItemClicked(String itemId) {
+ CarUiImeSearchListItem item = mIdToListItem.get(itemId);
+ if (item != null) {
+ CarUiContentListItem.OnClickListener listener =
+ item.getOnClickListener();
+ if (listener != null) {
+ listener.onClick(item);
+ }
+ }
+ }
+
+ @Override
+ public void onSecondaryImageClicked(String secondaryImageId) {
+ CarUiImeSearchListItem item = mIdToListItem.get(secondaryImageId);
+ if (item != null) {
+ CarUiContentListItem.OnClickListener listener =
+ item.getSupplementalIconOnClickListener();
+ if (listener != null) {
+ listener.onClick(item);
+ }
+ }
+ }
+
+ @Override
+ public void onSurfaceInfo(int displayId, IBinder binder, int height,
+ int width) {
+ if (Build.VERSION.SDK_INT < VERSION_CODES.R
+ || mWideScreenImeContentAreaView == null) {
+ // SurfaceControlViewHost is only available on R and above
+ return;
+ }
+
+ DisplayManager dm = (DisplayManager) getContext().getSystemService(
+ Context.DISPLAY_SERVICE);
+
+ Display display = dm.getDisplay(displayId);
+
+ if (mSurfaceControlViewHost != null) {
+ mSurfaceControlViewHost.release();
+ }
+
+ mSurfaceControlViewHost = new SurfaceControlViewHost(getContext(),
+ display, binder);
+
+ mSurfaceHeight = height;
+ mSurfaceWidth = width;
+
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(CONTENT_AREA_SURFACE_PACKAGE,
+ mSurfaceControlViewHost.getSurfacePackage());
+ mInputMethodManager.sendAppPrivateCommand(mSearchText,
+ WIDE_SCREEN_ACTION, bundle);
+ }
+ }
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/Toolbar.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/Toolbar.java
index b069fb0..e527a52 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/Toolbar.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/Toolbar.java
@@ -22,6 +22,7 @@
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
+import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.DrawableRes;
@@ -31,6 +32,8 @@
import androidx.annotation.XmlRes;
import com.android.car.ui.R;
+import com.android.car.ui.imewidescreen.CarUiImeSearchListItem;
+import com.android.car.ui.recyclerview.CarUiListItem;
import java.util.List;
@@ -42,8 +45,12 @@
* {@link android.app.Activity#setActionBar(android.widget.Toolbar)} with it)
*
* <p>The toolbar supports a navigation button, title, tabs, search, and {@link MenuItem MenuItems}
+ *
+ * @deprecated Instead of creating this class, use Theme.CarUi.WithToolbar, and get access to it
+ * via {@link com.android.car.ui.core.CarUi#requireToolbar(android.app.Activity)}
*/
-public class Toolbar extends FrameLayout implements ToolbarController {
+@Deprecated
+public final class Toolbar extends FrameLayout implements ToolbarController {
/** Callback that will be issued whenever the height of toolbar is changed. */
public interface OnHeightChangedListener {
@@ -268,13 +275,33 @@
/**
* Gets the {@link TabLayout} for this toolbar.
+ *
+ * @deprecated Use other tab-related functions in the ToolbarController interface.
*/
+ @Deprecated
@Override
public TabLayout getTabLayout() {
return mController.getTabLayout();
}
/**
+ * Gets the number of tabs in the toolbar. The tabs can be retrieved using
+ * {@link #getTab(int)}.
+ */
+ @Override
+ public int getTabCount() {
+ return mController.getTabCount();
+ }
+
+ /**
+ * Gets the index of the tab.
+ */
+ @Override
+ public int getTabPosition(TabLayout.Tab tab) {
+ return mController.getTabPosition(tab);
+ }
+
+ /**
* Adds a tab to this toolbar. You can listen for when it is selected via
* {@link #registerOnTabSelectedListener(OnTabSelectedListener)}.
*/
@@ -628,6 +655,50 @@
return mController.unregisterOnSearchListener(listener);
}
+ /**
+ * Returns true if the toolbar can display search result items. One example of this is when the
+ * system is configured to display search items in the IME instead of in the app.
+ */
+ @Override
+ public boolean canShowSearchResultItems() {
+ return mController.canShowSearchResultItems();
+ }
+
+ /**
+ * Returns true if the app is allowed to set search results view.
+ */
+ @Override
+ public boolean canShowSearchResultsView() {
+ return mController.canShowSearchResultsView();
+ }
+
+ /**
+ * Add a view within a container that will animate with the wide screen IME to display search
+ * results.
+ *
+ * <p>Note: Apps can only call this method if the package name is allowed via OEM to render
+ * their view. To check if the application have the permission to do so or not first call
+ * {@link #canShowSearchResultsView()}. If the app is not allowed this method will throw an
+ * {@link IllegalStateException}
+ *
+ * @param view to be added in the container.
+ */
+ @Override
+ public void setSearchResultsView(View view) {
+ mController.setSearchResultsView(view);
+ }
+
+ /**
+ * Sets list of search item {@link CarUiListItem} to be displayed in the IMS
+ * template. This method should be called when system is running in a wide screen mode. Apps
+ * can check that by using {@link #canShowSearchResultItems()}
+ * Else, this method will throw an {@link IllegalStateException}
+ */
+ @Override
+ public void setSearchResultItems(List<? extends CarUiImeSearchListItem> searchItems) {
+ mController.setSearchResultItems(searchItems);
+ }
+
/** Registers a new {@link OnSearchCompletedListener} to the list of listeners. */
@Override
public void registerOnSearchCompletedListener(OnSearchCompletedListener listener) {
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarController.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarController.java
index 4eb009d..6b38718 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarController.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarController.java
@@ -17,6 +17,7 @@
package com.android.car.ui.toolbar;
import android.graphics.drawable.Drawable;
+import android.view.View;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
@@ -24,6 +25,9 @@
import androidx.annotation.StringRes;
import androidx.annotation.XmlRes;
+import com.android.car.ui.imewidescreen.CarUiImeSearchListItem;
+import com.android.car.ui.recyclerview.CarUiListItem;
+
import java.util.List;
/**
@@ -77,10 +81,24 @@
/**
* Gets the {@link TabLayout} for this toolbar.
+ *
+ * @deprecated Use other tab-related functions in this interface.
*/
+ @Deprecated
TabLayout getTabLayout();
/**
+ * Gets the number of tabs in the toolbar. The tabs can be retrieved using
+ * {@link #getTab(int)}.
+ */
+ int getTabCount();
+
+ /**
+ * Gets the index of the tab.
+ */
+ int getTabPosition(TabLayout.Tab tab);
+
+ /**
* Adds a tab to this toolbar. You can listen for when it is selected via
* {@link #registerOnTabSelectedListener(Toolbar.OnTabSelectedListener)}.
*/
@@ -242,8 +260,10 @@
*/
void registerToolbarHeightChangeListener(Toolbar.OnHeightChangedListener listener);
- /** Unregisters an existing {@link Toolbar.OnHeightChangedListener} from the list of
- * listeners. */
+ /**
+ * Unregisters an existing {@link Toolbar.OnHeightChangedListener} from the list of
+ * listeners.
+ */
boolean unregisterToolbarHeightChangeListener(Toolbar.OnHeightChangedListener listener);
/** Registers a new {@link Toolbar.OnTabSelectedListener} to the list of listeners. */
@@ -258,11 +278,45 @@
/** Unregisters an existing {@link Toolbar.OnSearchListener} from the list of listeners. */
boolean unregisterOnSearchListener(Toolbar.OnSearchListener listener);
+ /**
+ * Returns true if the toolbar can display search result items. One example of this is when the
+ * system is configured to display search items in the IME instead of in the app.
+ */
+ boolean canShowSearchResultItems();
+
+ /**
+ * Returns true if the app is allowed to set search results view.
+ */
+ boolean canShowSearchResultsView();
+
+ /**
+ * Add a view within a container that will animate with the wide screen IME to display search
+ * results.
+ *
+ * <p>Note: Apps can only call this method if the package name is allowed via OEM to render
+ * their view. To check if the application have the permission to do so or not first call
+ * {@link #canShowSearchResultsView()}. If the app is not allowed this method will throw an
+ * {@link IllegalStateException}
+ *
+ * @param view to be added in the container.
+ */
+ void setSearchResultsView(View view);
+
+ /**
+ * Sets list of search item {@link CarUiListItem} to be displayed in the IMS
+ * template. This method should be called when system is running in a wide screen mode. Apps
+ * can check that by using {@link #canShowSearchResultItems()}
+ * Else, this method will throw an {@link IllegalStateException}
+ */
+ void setSearchResultItems(List<? extends CarUiImeSearchListItem> searchItems);
+
/** Registers a new {@link Toolbar.OnSearchCompletedListener} to the list of listeners. */
void registerOnSearchCompletedListener(Toolbar.OnSearchCompletedListener listener);
- /** Unregisters an existing {@link Toolbar.OnSearchCompletedListener} from the list of
- * listeners. */
+ /**
+ * Unregisters an existing {@link Toolbar.OnSearchCompletedListener} from the list of
+ * listeners.
+ */
boolean unregisterOnSearchCompletedListener(Toolbar.OnSearchCompletedListener listener);
/** Registers a new {@link Toolbar.OnBackListener} to the list of listeners. */
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarControllerImpl.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarControllerImpl.java
index 413beaa..02b3a1a 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarControllerImpl.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarControllerImpl.java
@@ -21,6 +21,7 @@
import static android.view.View.VISIBLE;
import static com.android.car.ui.utils.CarUiUtils.findViewByRefId;
+import static com.android.car.ui.utils.CarUiUtils.getBooleanSystemProperty;
import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
import android.app.Activity;
@@ -43,6 +44,7 @@
import com.android.car.ui.AlertDialogBuilder;
import com.android.car.ui.R;
+import com.android.car.ui.imewidescreen.CarUiImeSearchListItem;
import com.android.car.ui.recyclerview.CarUiContentListItem;
import com.android.car.ui.recyclerview.CarUiListItem;
import com.android.car.ui.recyclerview.CarUiListItemAdapter;
@@ -50,6 +52,7 @@
import com.android.car.ui.utils.CarUxRestrictionsUtil;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -61,7 +64,7 @@
* The implementation of {@link ToolbarController}. This class takes a ViewGroup, and looks
* in the ViewGroup to find all the toolbar-related views to control.
*/
-public class ToolbarControllerImpl implements ToolbarController {
+public final class ToolbarControllerImpl implements ToolbarController {
private static final String TAG = "CarUiToolbarController";
@Nullable
@@ -111,7 +114,9 @@
private AlertDialog mOverflowDialog;
private boolean mNavIconSpaceReserved;
private boolean mLogoFillsNavIconSpace;
+ private View mViewForContentAreaInWideScreenMode;
private boolean mShowLogo;
+ private List<? extends CarUiImeSearchListItem> mSearchItems;
private final ProgressBarController mProgressBar;
private final MenuItem.Listener mOverflowItemListener = item -> {
updateOverflowDialog(item);
@@ -266,13 +271,33 @@
/**
* Gets the {@link TabLayout} for this toolbar.
+ *
+ * @deprecated Use other tab-related functions in the ToolbarController interface.
*/
+ @Deprecated
@Override
public TabLayout getTabLayout() {
return mTabLayout;
}
/**
+ * Gets the number of tabs in the toolbar. The tabs can be retrieved using
+ * {@link #getTab(int)}.
+ */
+ @Override
+ public int getTabCount() {
+ return mTabLayout.getTabCount();
+ }
+
+ /**
+ * Gets the index of the tab.
+ */
+ @Override
+ public int getTabPosition(TabLayout.Tab tab) {
+ return mTabLayout.getTabPosition(tab);
+ }
+
+ /**
* Adds a tab to this toolbar. You can listen for when it is selected via
* {@link #registerOnTabSelectedListener(Toolbar.OnTabSelectedListener)}.
*/
@@ -693,6 +718,15 @@
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
mSearchViewContainer.addView(searchView, layoutParams);
+ if (canShowSearchResultsView()) {
+ searchView.setViewToImeWideScreenSurface(mViewForContentAreaInWideScreenMode);
+ }
+
+ searchView.installWindowInsetsListener(mSearchViewContainer);
+
+ if (mSearchItems != null) {
+ searchView.setSearchItemsForWideScreen(mSearchItems);
+ }
mSearchView = searchView;
}
@@ -790,13 +824,94 @@
mOverflowButton.setVisible(showButtons && countVisibleOverflowItems() > 0);
}
+ /**
+ * Return the list of package names allowed to hide the content area in wide screen IME.
+ */
+ private List<String> allowPackageList(Context context) {
+ String[] packages = context.getResources()
+ .getStringArray(R.array.car_ui_ime_wide_screen_allowed_package_list);
+ return Arrays.asList(packages);
+ }
+
+ /**
+ * Returns true if the toolbar can display search result items. One example of this is when the
+ * system is configured to display search items in the IME instead of in the app.
+ */
+ @Override
+ public boolean canShowSearchResultItems() {
+ return isWideScreenMode(mContext);
+ }
+
+ /**
+ * Returns whether or not system is running in a wide screen mode.
+ */
+ private static boolean isWideScreenMode(Context context) {
+ return getBooleanSystemProperty(context.getResources(),
+ R.string.car_ui_ime_wide_screen_system_property_name, false);
+ }
+
+ /**
+ * Returns true if the app is allowed to set search results view.
+ */
+ @Override
+ public boolean canShowSearchResultsView() {
+ boolean allowAppsToHideContentArea = mContext.getResources().getBoolean(
+ R.bool.car_ui_ime_wide_screen_allow_app_hide_content_area);
+ return isWideScreenMode(mContext) && (allowPackageList(mContext).contains(
+ mContext.getPackageName()) || allowAppsToHideContentArea);
+ }
+
+ /**
+ * Add a view within a container that will animate with the wide screen IME to display search
+ * results.
+ *
+ * <p>Note: Apps can only call this method if the package name is allowed via OEM to render
+ * their view. To check if the application have the permission to do so or not first call
+ * {@link #canShowSearchResultsView()}. If the app is not allowed this method will throw an
+ * {@link IllegalStateException}
+ *
+ * @param view to be added in the container.
+ */
+ @Override
+ public void setSearchResultsView(View view) {
+ if (!canShowSearchResultsView()) {
+ throw new IllegalStateException(
+ "not allowed to add view to wide screen IME, package name: "
+ + mContext.getPackageName());
+ }
+
+ if (mSearchView != null) {
+ mSearchView.setViewToImeWideScreenSurface(view);
+ }
+
+ mViewForContentAreaInWideScreenMode = view;
+ }
+
+ /**
+ * Sets list of search item {@link CarUiListItem} to be displayed in the IMS
+ * template. This method should be called when system is running in a wide screen mode. Apps
+ * can check that by using {@link #canShowSearchResultItems()}
+ * Else, this method will throw an {@link IllegalStateException}
+ */
+ @Override
+ public void setSearchResultItems(List<? extends CarUiImeSearchListItem> searchItems) {
+ if (!canShowSearchResultItems()) {
+ throw new IllegalStateException(
+ "system not in wide screen mode, not allowed to set search result items ");
+ }
+ mSearchItems = searchItems;
+ if (mSearchView != null) {
+ mSearchView.setSearchItemsForWideScreen(searchItems);
+ }
+ }
+
+
/** Gets the current {@link Toolbar.State} of the toolbar. */
@Override
public Toolbar.State getState() {
return mState;
}
-
/**
* Registers a new {@link Toolbar.OnHeightChangedListener} to the list of listeners. Register a
* {@link com.android.car.ui.recyclerview.CarUiRecyclerView} only if there is a toolbar at
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/CarUiUtils.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/CarUiUtils.java
index b27a81a..ffa191b 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/CarUiUtils.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/CarUiUtils.java
@@ -15,13 +15,22 @@
*/
package com.android.car.ui.utils;
+import static com.android.car.ui.utils.RotaryConstants.ROTARY_HORIZONTALLY_SCROLLABLE;
+import static com.android.car.ui.utils.RotaryConstants.ROTARY_VERTICALLY_SCROLLABLE;
+
import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.res.Resources;
import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
@@ -34,10 +43,18 @@
import androidx.annotation.UiThread;
import androidx.core.view.ViewCompat;
+import java.lang.reflect.Method;
+
/**
* Collection of utility methods
*/
public final class CarUiUtils {
+
+ private static final String TAG = "CarUiUtils";
+ private static final String READ_ONLY_SYSTEM_PROPERTY_PREFIX = "ro.";
+ /** A map to cache read-only system properties. */
+ private static final SparseArray<String> READ_ONLY_SYSTEM_PROPERTY_MAP = new SparseArray<>();
+
/** This is a utility class */
private CarUiUtils() {
}
@@ -46,7 +63,7 @@
* Reads a float value from a dimens resource. This is necessary as {@link Resources#getFloat}
* is not currently public.
*
- * @param res {@link Resources} to read values from
+ * @param res {@link Resources} to read values from
* @param resId Id of the dimens resource to read
*/
public static float getFloat(Resources res, @DimenRes int resId) {
@@ -134,11 +151,11 @@
/**
* Updates the ripple state on the given preference.
*
- * @param isEnabled whether the preference is enabled or not
+ * @param isEnabled whether the preference is enabled or not
* @param shouldShowRippleOnDisabledPreference should ripple be displayed when the preference is
- * clicked
- * @param background drawable that represents the ripple
- * @param preference preference on which drawable will be applied
+ * clicked
+ * @param background drawable that represents the ripple
+ * @param preference preference on which drawable will be applied
*/
public static void updateRippleStateOnDisabledPreference(boolean isEnabled,
boolean shouldShowRippleOnDisabledPreference, Drawable background, View preference) {
@@ -153,6 +170,19 @@
}
/**
+ * Enables rotary scrolling for {@code view}, either vertically (if {@code isVertical} is true)
+ * or horizontally (if {@code isVertical} is false). With rotary scrolling enabled, rotating the
+ * rotary controller will scroll rather than moving the focus when moving the focus would cause
+ * a lot of scrolling. Rotary scrolling should be enabled for scrolling views which contain
+ * content which the user may want to see but can't interact with, either alone or along with
+ * interactive (focusable) content.
+ */
+ public static void setRotaryScrollEnabled(@NonNull View view, boolean isVertical) {
+ view.setContentDescription(
+ isVertical ? ROTARY_VERTICALLY_SCROLLABLE : ROTARY_HORIZONTALLY_SCROLLABLE);
+ }
+
+ /**
* It behaves similarly to {@link View#findViewById(int)}, except that on Q and below,
* it will first resolve the id to whatever it references.
*
@@ -199,4 +229,105 @@
}
return view;
}
+
+ /**
+ * Returns the system property of type boolean. This method converts the boolean value in string
+ * returned by {@link #getSystemProperty(Resources, int)}
+ */
+ public static boolean getBooleanSystemProperty(
+ @NonNull Resources resources, int propertyResId, boolean defaultValue) {
+ String value = getSystemProperty(resources, propertyResId);
+
+ if (!TextUtils.isEmpty(value)) {
+ return Boolean.parseBoolean(value);
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Use reflection to interact with the hidden API <code>android.os.SystemProperties</code>.
+ *
+ * <p>This method caches read-only properties. CAVEAT: Please do not set read-only properties
+ * by 'adb setprop' after app started. Read-only properties CAN BE SET ONCE if it is unset.
+ * Thus, read-only properties MAY BE CHANGED from unset to set during application's lifetime if
+ * you use 'adb setprop' command to set read-only properties after app started. For the sake of
+ * performance, this method also caches the unset state. Otherwise, cache may not effective if
+ * the system property is unset (which is most-likely).
+ *
+ * @param resources resources object to fetch string
+ * @param propertyResId the property resource id.
+ * @return The value of the property if defined, else null. Does not return empty strings.
+ */
+ @Nullable
+ public static String getSystemProperty(@NonNull Resources resources, int propertyResId) {
+ String propertyName = resources.getString(propertyResId);
+ boolean isReadOnly = propertyName.startsWith(READ_ONLY_SYSTEM_PROPERTY_PREFIX);
+ if (!isReadOnly) {
+ return readSystemProperty(propertyName);
+ }
+ synchronized (READ_ONLY_SYSTEM_PROPERTY_MAP) {
+ // readOnlySystemPropertyMap may contain null values.
+ if (READ_ONLY_SYSTEM_PROPERTY_MAP.indexOfKey(propertyResId) >= 0) {
+ return READ_ONLY_SYSTEM_PROPERTY_MAP.get(propertyResId);
+ }
+ String value = readSystemProperty(propertyName);
+ READ_ONLY_SYSTEM_PROPERTY_MAP.put(propertyResId, value);
+ return value;
+ }
+ }
+
+ @Nullable
+ private static String readSystemProperty(String propertyName) {
+ Class<?> systemPropertiesClass;
+ try {
+ systemPropertiesClass = Class.forName("android.os.SystemProperties");
+ } catch (ClassNotFoundException e) {
+ Log.w(TAG, "Cannot find android.os.SystemProperties: ", e);
+ return null;
+ }
+
+ Method getMethod;
+ try {
+ getMethod = systemPropertiesClass.getMethod("get", String.class);
+ } catch (NoSuchMethodException e) {
+ Log.w(TAG, "Cannot find SystemProperties.get(): ", e);
+ return null;
+ }
+
+ try {
+ Object[] params = new Object[]{propertyName};
+ String value = (String) getMethod.invoke(systemPropertiesClass, params);
+ return TextUtils.isEmpty(value) ? null : value;
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to invoke SystemProperties.get(): ", e);
+ return null;
+ }
+ }
+
+ /**
+ * Converts a drawable to bitmap. This value should not be null.
+ */
+ public static Bitmap drawableToBitmap(@NonNull Drawable drawable) {
+ Bitmap bitmap;
+
+ if (drawable instanceof BitmapDrawable) {
+ BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
+ if (bitmapDrawable.getBitmap() != null) {
+ return bitmapDrawable.getBitmap();
+ }
+ }
+
+ if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
+ bitmap = Bitmap.createBitmap(1, 1,
+ Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel
+ } else {
+ bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
+ drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+ }
+
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+ return bitmap;
+ }
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/CarUxRestrictionsUtil.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/CarUxRestrictionsUtil.java
index e5c7fc9..b4baf31 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/CarUxRestrictionsUtil.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/CarUxRestrictionsUtil.java
@@ -22,6 +22,7 @@
import android.car.drivingstate.CarUxRestrictions.CarUxRestrictionsInfo;
import android.car.drivingstate.CarUxRestrictionsManager;
import android.content.Context;
+import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -34,10 +35,6 @@
import java.util.Set;
import java.util.WeakHashMap;
-// This can't be in the middle of the rest of the imports on gerrit or it will
-// fail our style checks
-// copybara:insert import android.car.CarNotConnectedException;
-
/**
* Utility class to access Car Restriction Manager.
*
@@ -48,10 +45,6 @@
public class CarUxRestrictionsUtil {
private static final String TAG = "CarUxRestrictionsUtil";
- /* copybara:insert
- private final Car mCarApi;
- private CarUxRestrictionsManager mCarUxRestrictionsManager;
- */
@NonNull
private CarUxRestrictions mCarUxRestrictions = getDefaultRestrictions();
@@ -73,36 +66,36 @@
}
};
- // copybara:strip_begin
- Car.createCar(context.getApplicationContext(), null, Car.CAR_WAIT_TIMEOUT_DO_NOT_WAIT,
- (Car car, boolean ready) -> {
- if (ready) {
- CarUxRestrictionsManager carUxRestrictionsManager =
- (CarUxRestrictionsManager) car.getCarManager(
- Car.CAR_UX_RESTRICTION_SERVICE);
- carUxRestrictionsManager.registerListener(listener);
- listener.onUxRestrictionsChanged(
- carUxRestrictionsManager.getCurrentCarUxRestrictions());
- } else {
- Log.w(TAG, "Car service disconnected, assuming fully restricted uxr");
- listener.onUxRestrictionsChanged(null);
- }
- });
- /* copybara:strip_end_and_replace
- mCarApi = Car.createCar(context.getApplicationContext());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ Car.createCar(context.getApplicationContext(), null, Car.CAR_WAIT_TIMEOUT_DO_NOT_WAIT,
+ (Car car, boolean ready) -> {
+ if (ready) {
+ CarUxRestrictionsManager carUxRestrictionsManager =
+ (CarUxRestrictionsManager) car.getCarManager(
+ Car.CAR_UX_RESTRICTION_SERVICE);
+ carUxRestrictionsManager.registerListener(listener);
+ listener.onUxRestrictionsChanged(
+ carUxRestrictionsManager.getCurrentCarUxRestrictions());
+ } else {
+ Log.w(TAG, "Car service disconnected, assuming fully restricted uxr");
+ listener.onUxRestrictionsChanged(null);
+ }
+ });
+ } else {
+ Car carApi = Car.createCar(context.getApplicationContext());
- try {
- mCarUxRestrictionsManager =
- (CarUxRestrictionsManager) mCarApi.getCarManager(
- Car.CAR_UX_RESTRICTION_SERVICE);
- mCarUxRestrictionsManager.registerListener(listener);
- listener.onUxRestrictionsChanged(
- mCarUxRestrictionsManager.getCurrentCarUxRestrictions());
- } catch (CarNotConnectedException | NullPointerException e) {
- Log.e(TAG, "Car not connected", e);
- // mCarUxRestrictions will be the default
+ try {
+ CarUxRestrictionsManager carUxRestrictionsManager =
+ (CarUxRestrictionsManager) carApi.getCarManager(
+ Car.CAR_UX_RESTRICTION_SERVICE);
+ carUxRestrictionsManager.registerListener(listener);
+ listener.onUxRestrictionsChanged(
+ carUxRestrictionsManager.getCurrentCarUxRestrictions());
+ } catch (NullPointerException e) {
+ Log.e(TAG, "Car not connected", e);
+ // mCarUxRestrictions will be the default
+ }
}
- */
}
@NonNull
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/RotaryConstants.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/RotaryConstants.java
index 99154df..e6c62c9 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/RotaryConstants.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/RotaryConstants.java
@@ -23,19 +23,47 @@
*/
public final class RotaryConstants {
/**
- * Content description indicating that the rotary controller should scroll this view
- * horizontally.
+ * Content description indicating that the view is a rotary container.
+ * <p>
+ * A rotary container contains focusable elements. When initializing focus, the first element
+ * in the rotary container is prioritized to take focus. When searching for nudge target, the
+ * bounds of the rotary container is the minimum bounds containing its descendants.
+ * <p>
+ * A rotary container shouldn't be focusable unless it's a scrollable container. Though it
+ * can't be focused, it can be scrolled as a side-effect of moving the focus within it.
+ */
+ public static final String ROTARY_CONTAINER =
+ "com.android.car.ui.utils.ROTARY_CONTAINER";
+
+ /**
+ * Content description indicating that the view is a scrollable container and can be scrolled
+ * horizontally by the rotary controller.
+ * <p>
+ * A scrollable container is a focusable rotary container. When it's focused, it can be scrolled
+ * when the rotary controller rotates. A scrollable container is often used to show long text.
*/
public static final String ROTARY_HORIZONTALLY_SCROLLABLE =
"com.android.car.ui.utils.HORIZONTALLY_SCROLLABLE";
/**
- * Content description indicating that the rotary controller should scroll this view
- * vertically.
+ * Content description indicating that the view is a scrollable container and can be scrolled
+ * vertically by the rotary controller.
+ * <p>
+ * A scrollable container is a focusable rotary container. When it's focused, it can be scrolled
+ * when the rotary controller rotates. A scrollable container is often used to show long text.
*/
public static final String ROTARY_VERTICALLY_SCROLLABLE =
"com.android.car.ui.utils.VERTICALLY_SCROLLABLE";
+ /**
+ * Content description indicating that the view is a focus delegating container. When
+ * restoring focus, FocusParkingView and FocusArea will skip non-focusable views unless it's
+ * a focus delegating container. The focus delegating container can delegate focus to one of
+ * its descendants.
+ */
+ public static final String ROTARY_FOCUS_DELEGATING_CONTAINER =
+ "com.android.car.ui.utils.FOCUS_DELEGATING_CONTAINER";
+
/** The key to store the offset of the FocusArea's left bound in the node's extras. */
public static final String FOCUS_AREA_LEFT_BOUND_OFFSET =
"com.android.car.ui.utils.FOCUS_AREA_LEFT_BOUND_OFFSET";
@@ -70,7 +98,7 @@
/** Action performed on a FocusArea to move focus to another FocusArea. */
public static final int ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA = 0x02000000;
- /** Action performed on a FocusParkingView to restore the default focus. */
+ /** Action performed on a FocusParkingView to restore the focus in the window. */
public static final int ACTION_RESTORE_DEFAULT_FOCUS = 0x04000000;
/** Action performed on a FocusParkingView to hide the IME. */
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/ViewUtils.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/ViewUtils.java
index 6dd993b..62c189f 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/ViewUtils.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/ViewUtils.java
@@ -16,17 +16,28 @@
package com.android.car.ui.utils;
-import static android.view.View.VISIBLE;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS;
+import static com.android.car.ui.utils.RotaryConstants.ROTARY_CONTAINER;
+import static com.android.car.ui.utils.RotaryConstants.ROTARY_FOCUS_DELEGATING_CONTAINER;
import static com.android.car.ui.utils.RotaryConstants.ROTARY_HORIZONTALLY_SCROLLABLE;
import static com.android.car.ui.utils.RotaryConstants.ROTARY_VERTICALLY_SCROLLABLE;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
+import android.view.ViewParent;
+import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.car.ui.FocusArea;
+import com.android.car.ui.FocusParkingView;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
/**
* Utility class used by {@link com.android.car.ui.FocusArea} and {@link
@@ -36,50 +47,350 @@
*/
public final class ViewUtils {
- /** This is a utility class */
+ /**
+ * No view is focused, the focused view is not shown, or the focused view is a FocusParkingView.
+ */
+ public static final int NO_FOCUS = 1;
+
+ /** A scrollable container is focused. */
+ public static final int SCROLLABLE_CONTAINER_FOCUS = 2;
+
+ /**
+ * A regular view is focused. A regular View is a View that is neither a FocusParkingView nor a
+ * scrollable container.
+ */
+ public static final int REGULAR_FOCUS = 3;
+
+ /**
+ * An implicit default focus view (i.e., the first focusable item in a scrollable container) is
+ * focused.
+ */
+ public static final int IMPLICIT_DEFAULT_FOCUS = 4;
+
+ /** The {@code app:defaultFocus} view is focused. */
+ public static final int DEFAULT_FOCUS = 5;
+
+ /** The {@code android:focusedByDefault} view is focused. */
+ public static final int FOCUSED_BY_DEFAULT = 6;
+
+ /**
+ * Focus level of a view. When adjusting the focus, the view with the highest focus level will
+ * be focused.
+ */
+ @IntDef(flag = true, value = {NO_FOCUS, SCROLLABLE_CONTAINER_FOCUS, REGULAR_FOCUS,
+ IMPLICIT_DEFAULT_FOCUS, DEFAULT_FOCUS, FOCUSED_BY_DEFAULT})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FocusLevel {
+ }
+
+ /** This is a utility class. */
private ViewUtils() {
}
/**
- * Searches the {@code view} and its descendants in depth first order, and returns the first
- * view that is focused by default, can take focus, but has no invisible ancestors. Returns null
- * if not found.
+ * This is a functional interface and can therefore be used as the assignment target for a
+ * lambda expression or method reference.
+ *
+ * @param <T> the type of the input to the predicate
+ */
+ private interface Predicate<T> {
+ /** Evaluates this predicate on the given argument. */
+ boolean test(@NonNull T t);
+ }
+
+ /** Gets the ancestor FocusArea of the {@code view}, if any. Returns null if not found. */
+ @Nullable
+ public static FocusArea getAncestorFocusArea(@NonNull View view) {
+ ViewParent parent = view.getParent();
+ while (parent != null) {
+ if (parent instanceof FocusArea) {
+ return (FocusArea) parent;
+ }
+ parent = parent.getParent();
+ }
+ return null;
+ }
+
+ /**
+ * Gets the ancestor scrollable container of the {@code view}, if any. Returns null if not
+ * found.
*/
@Nullable
- public static View findFocusedByDefaultView(@NonNull View view) {
- return depthFirstSearch(view,
- /* targetPredicate= */ v -> v.isFocusedByDefault() && canTakeFocus(v),
- /* skipPredicate= */ v -> v.getVisibility() != VISIBLE);
+ public static ViewGroup getAncestorScrollableContainer(@Nullable View view) {
+ if (view == null) {
+ return null;
+ }
+ ViewParent parent = view.getParent();
+ // A scrollable container can't contain a FocusArea, so let's return earlier if we found
+ // a FocusArea.
+ while (parent != null && parent instanceof ViewGroup && !(parent instanceof FocusArea)) {
+ ViewGroup viewGroup = (ViewGroup) parent;
+ if (isScrollableContainer(viewGroup)) {
+ return viewGroup;
+ }
+ parent = parent.getParent();
+ }
+ return null;
+ }
+
+ /**
+ * Focuses on the {@code view} if it can be focused.
+ *
+ * @return whether it was successfully focused or already focused
+ */
+ public static boolean requestFocus(@Nullable View view) {
+ if (view == null || !canTakeFocus(view)) {
+ return false;
+ }
+ if (view.isFocused()) {
+ return true;
+ }
+ // Exit touch mode and focus the view. The view may not be focusable in touch mode, so we
+ // need to exit touch mode before focusing it.
+ return view.performAccessibilityAction(ACTION_FOCUS, /* arguments= */ null);
+ }
+
+ /**
+ * Searches the {@code root}'s descendants for a view with the highest {@link FocusLevel}. If
+ * the view's FocusLevel is higher than the {@code currentFocus}'s FocusLevel, focuses on the
+ * view.
+ *
+ * @return whether the view is focused
+ */
+ public static boolean adjustFocus(@NonNull View root, @Nullable View currentFocus) {
+ @FocusLevel int level = getFocusLevel(currentFocus);
+ return adjustFocus(root, level);
+ }
+
+ /**
+ * Searches the {@code root}'s descendants for a view with the highest {@link FocusLevel}. If
+ * the view's FocusLevel is higher than {@code currentLevel}, focuses on the view.
+ *
+ * @return whether the view is focused
+ */
+ public static boolean adjustFocus(@NonNull View root, @FocusLevel int currentLevel) {
+ if (currentLevel < FOCUSED_BY_DEFAULT && focusOnFocusedByDefaultView(root)) {
+ return true;
+ }
+ if (currentLevel < DEFAULT_FOCUS && focusOnDefaultFocusView(root)) {
+ return true;
+ }
+ if (currentLevel < IMPLICIT_DEFAULT_FOCUS && focusOnImplicitDefaultFocusView(root)) {
+ return true;
+ }
+ if (currentLevel < REGULAR_FOCUS && focusOnFirstRegularView(root)) {
+ return true;
+ }
+ if (currentLevel < SCROLLABLE_CONTAINER_FOCUS) {
+ return focusOnScrollableContainer(root);
+ }
+ return false;
+ }
+
+ @VisibleForTesting
+ @FocusLevel
+ static int getFocusLevel(@Nullable View view) {
+ if (view == null || view instanceof FocusParkingView || !view.isShown()) {
+ return NO_FOCUS;
+ }
+ if (view.isFocusedByDefault()) {
+ return FOCUSED_BY_DEFAULT;
+ }
+ if (isDefaultFocus(view)) {
+ return DEFAULT_FOCUS;
+ }
+ if (isImplicitDefaultFocusView(view)) {
+ return IMPLICIT_DEFAULT_FOCUS;
+ }
+ if (isScrollableContainer(view)) {
+ return SCROLLABLE_CONTAINER_FOCUS;
+ }
+ return REGULAR_FOCUS;
+ }
+
+ /** Returns whether the {@code view} is a {@code app:defaultFocus} view. */
+ private static boolean isDefaultFocus(@NonNull View view) {
+ FocusArea parent = getAncestorFocusArea(view);
+ return parent != null && view == parent.getDefaultFocusView();
+ }
+
+ /**
+ * Returns whether the {@code view} is an implicit default focus view, i.e., the first focusable
+ * item in a rotary container.
+ */
+ @VisibleForTesting
+ static boolean isImplicitDefaultFocusView(@NonNull View view) {
+ ViewGroup rotaryContainer = null;
+ ViewParent parent = view.getParent();
+ while (parent != null && parent instanceof ViewGroup) {
+ ViewGroup viewGroup = (ViewGroup) parent;
+ if (isRotaryContainer(viewGroup)) {
+ rotaryContainer = viewGroup;
+ break;
+ }
+ parent = parent.getParent();
+ }
+ if (rotaryContainer == null) {
+ return false;
+ }
+ return findFirstFocusableDescendant(rotaryContainer) == view;
+ }
+
+ private static boolean isRotaryContainer(@NonNull View view) {
+ CharSequence contentDescription = view.getContentDescription();
+ return TextUtils.equals(contentDescription, ROTARY_CONTAINER)
+ || TextUtils.equals(contentDescription, ROTARY_VERTICALLY_SCROLLABLE)
+ || TextUtils.equals(contentDescription, ROTARY_HORIZONTALLY_SCROLLABLE);
+ }
+
+ private static boolean isScrollableContainer(@NonNull View view) {
+ CharSequence contentDescription = view.getContentDescription();
+ return TextUtils.equals(contentDescription, ROTARY_VERTICALLY_SCROLLABLE)
+ || TextUtils.equals(contentDescription, ROTARY_HORIZONTALLY_SCROLLABLE);
+ }
+
+ private static boolean isFocusDelegatingContainer(@NonNull View view) {
+ CharSequence contentDescription = view.getContentDescription();
+ return TextUtils.equals(contentDescription, ROTARY_FOCUS_DELEGATING_CONTAINER);
+ }
+
+ /**
+ * Focuses on the first {@code app:defaultFocus} view in the view tree, if any.
+ *
+ * @param root the root of the view tree
+ * @return whether succeeded
+ */
+ private static boolean focusOnDefaultFocusView(@NonNull View root) {
+ View defaultFocus = findDefaultFocusView(root);
+ return requestFocus(defaultFocus);
+ }
+
+ /**
+ * Focuses on the first {@code android:focusedByDefault} view in the view tree, if any.
+ *
+ * @param root the root of the view tree
+ * @return whether succeeded
+ */
+ private static boolean focusOnFocusedByDefaultView(@NonNull View root) {
+ View focusedByDefault = findFocusedByDefaultView(root);
+ return requestFocus(focusedByDefault);
+ }
+
+ /**
+ * Focuses on the first implicit default focus view in the view tree, if any.
+ *
+ * @param root the root of the view tree
+ * @return whether succeeded
+ */
+ private static boolean focusOnImplicitDefaultFocusView(@NonNull View root) {
+ View implicitDefaultFocus = findImplicitDefaultFocusView(root);
+ return requestFocus(implicitDefaultFocus);
+ }
+
+ /**
+ * Tries to focus on the first focusable view in the view tree in depth first order, excluding
+ * the FocusParkingView and scrollable containers. If focusing on the first such view fails,
+ * keeps trying other views in depth first order until succeeds or there are no more such views.
+ *
+ * @param root the root of the view tree
+ * @return whether succeeded
+ */
+ private static boolean focusOnFirstRegularView(@NonNull View root) {
+ View focusedView = ViewUtils.depthFirstSearch(root,
+ /* targetPredicate= */
+ v -> !isScrollableContainer(v) && canTakeFocus(v) && requestFocus(v),
+ /* skipPredicate= */ v -> !v.isShown());
+ return focusedView != null;
+ }
+
+ /**
+ * Focuses on the first scrollable container in the view tree, if any.
+ *
+ * @param root the root of the view tree
+ * @return whether succeeded
+ */
+ private static boolean focusOnScrollableContainer(@NonNull View root) {
+ View focusedView = ViewUtils.depthFirstSearch(root,
+ /* targetPredicate= */ v -> isScrollableContainer(v) && canTakeFocus(v),
+ /* skipPredicate= */ v -> !v.isShown());
+ return requestFocus(focusedView);
+ }
+
+ /**
+ * Searches the {@code root}'s descendants in depth first order, and returns the first
+ * {@code app:defaultFocus} view that can take focus. Returns null if not found.
+ */
+ @Nullable
+ private static View findDefaultFocusView(@NonNull View view) {
+ if (!view.isShown()) {
+ return null;
+ }
+ if (view instanceof FocusArea) {
+ FocusArea focusArea = (FocusArea) view;
+ View defaultFocus = focusArea.getDefaultFocusView();
+ if (defaultFocus != null && canTakeFocus(defaultFocus)) {
+ return defaultFocus;
+ }
+ } else if (view instanceof ViewGroup) {
+ ViewGroup parent = (ViewGroup) view;
+ for (int i = 0; i < parent.getChildCount(); i++) {
+ View child = parent.getChildAt(i);
+ View defaultFocus = findDefaultFocusView(child);
+ if (defaultFocus != null) {
+ return defaultFocus;
+ }
+ }
+ }
+ return null;
}
/**
* Searches the {@code view} and its descendants in depth first order, and returns the first
- * primary focus view, i.e., the first focusable item in a scrollable container. Returns null
- * if not found.
+ * {@code android:focusedByDefault} view that can take focus. Returns null if not found.
*/
- public static View findPrimaryFocusView(@NonNull View view) {
- View scrollableContainer = findScrollableContainer(view);
- return scrollableContainer == null ? null : findFocusableDescendant(scrollableContainer);
+ @VisibleForTesting
+ @Nullable
+ static View findFocusedByDefaultView(@NonNull View view) {
+ return depthFirstSearch(view,
+ /* targetPredicate= */ v -> v.isFocusedByDefault() && canTakeFocus(v),
+ /* skipPredicate= */ v -> !v.isShown());
+ }
+
+ /**
+ * Searches the {@code view} and its descendants in depth first order, and returns the first
+ * implicit default focus view, i.e., the first focusable item in the first rotary container.
+ * Returns null if not found.
+ */
+ @VisibleForTesting
+ @Nullable
+ static View findImplicitDefaultFocusView(@NonNull View view) {
+ View rotaryContainer = findRotaryContainer(view);
+ return rotaryContainer == null
+ ? null
+ : findFirstFocusableDescendant(rotaryContainer);
}
/**
* Searches the {@code view}'s descendants in depth first order, and returns the first view
- * that can take focus but has no invisible ancestors, or null if not found.
+ * that can take focus, or null if not found.
*/
+ @VisibleForTesting
@Nullable
- public static View findFocusableDescendant(@NonNull View view) {
+ static View findFirstFocusableDescendant(@NonNull View view) {
return depthFirstSearch(view,
/* targetPredicate= */ v -> v != view && canTakeFocus(v),
- /* skipPredicate= */ v -> v.getVisibility() != VISIBLE);
+ /* skipPredicate= */ v -> !v.isShown());
}
/**
* Searches the {@code view} and its descendants in depth first order, and returns the first
- * view that meets the given condition. Returns null if not found.
+ * rotary container shown on the screen. Returns null if not found.
*/
@Nullable
- public static View depthFirstSearch(@NonNull View view, @NonNull Predicate<View> predicate) {
- return depthFirstSearch(view, predicate, /* skipPredicate= */ null);
+ private static View findRotaryContainer(@NonNull View view) {
+ return depthFirstSearch(view,
+ /* targetPredicate= */ v -> isRotaryContainer(v),
+ /* skipPredicate= */ v -> !v.isShown());
}
/**
@@ -90,7 +401,7 @@
@Nullable
private static View depthFirstSearch(@NonNull View view,
@NonNull Predicate<View> targetPredicate,
- @NonNull Predicate<View> skipPredicate) {
+ @Nullable Predicate<View> skipPredicate) {
if (skipPredicate != null && skipPredicate.test(view)) {
return null;
}
@@ -110,35 +421,14 @@
return null;
}
- /**
- * This is a functional interface and can therefore be used as the assignment target for a
- * lambda expression or method reference.
- *
- * @param <T> the type of the input to the predicate
- */
- public interface Predicate<T> {
- /** Evaluates this predicate on the given argument. */
- boolean test(@NonNull T t);
- }
-
- /**
- * Searches the {@code view} and its descendants in depth first order, and returns the first
- * scrollable container that has no invisible ancestors. Returns null if not found.
- */
- @Nullable
- private static View findScrollableContainer(@NonNull View view) {
- return depthFirstSearch(view,
- /* targetPredicate= */ v -> {
- CharSequence contentDescription = v.getContentDescription();
- return TextUtils.equals(contentDescription, ROTARY_VERTICALLY_SCROLLABLE)
- || TextUtils.equals(contentDescription, ROTARY_HORIZONTALLY_SCROLLABLE);
- },
- /* skipPredicate= */ v -> v.getVisibility() != VISIBLE);
- }
-
/** Returns whether {@code view} can be focused. */
private static boolean canTakeFocus(@NonNull View view) {
- return view.isFocusable() && view.isEnabled() && view.getVisibility() == VISIBLE
- && view.getWidth() > 0 && view.getHeight() > 0;
+ boolean focusable = view.isFocusable() || isFocusDelegatingContainer(view);
+ return focusable && view.isEnabled() && view.isShown()
+ && view.getWidth() > 0 && view.getHeight() > 0 && view.isAttachedToWindow()
+ && !(view instanceof FocusParkingView)
+ // If it's a scrollable container, it can be focused only when it has no focusable
+ // descendants. We focus on it so that the rotary controller can scroll it.
+ && (!isScrollableContainer(view) || findFirstFocusableDescendant(view) == null);
}
}
diff --git a/car-ui-lib/car-ui-lib/src/main/res-overlayable/values/overlayable.xml b/car-ui-lib/car-ui-lib/src/main/res-overlayable/values/overlayable.xml
index e9b706d..b6f00f0 100644
--- a/car-ui-lib/car-ui-lib/src/main/res-overlayable/values/overlayable.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res-overlayable/values/overlayable.xml
@@ -16,6 +16,7 @@
<resources>
<overlayable name="car-ui-lib">
<policy type="public">
+ <item type="array" name="car_ui_ime_wide_screen_allowed_package_list"/>
<item type="attr" name="CarUiToolbarStyle"/>
<item type="attr" name="barrierDirection"/>
<item type="attr" name="carUiPreferenceStyle"/>
@@ -70,16 +71,20 @@
<item type="attr" name="layout_optimizationLevel"/>
<item type="attr" name="state_ux_restricted"/>
<item type="attr" name="title"/>
+ <item type="bool" name="car_ui_alert_dialog_force_dismiss_button"/>
<item type="bool" name="car_ui_clear_focus_area_history_when_rotating"/>
<item type="bool" name="car_ui_enable_focus_area_background_highlight"/>
<item type="bool" name="car_ui_enable_focus_area_foreground_highlight"/>
<item type="bool" name="car_ui_escrow_check_components_automatically"/>
<item type="bool" name="car_ui_focus_area_default_focus_overrides_history"/>
+ <item type="bool" name="car_ui_ime_wide_screen_aligned_left"/>
+ <item type="bool" name="car_ui_ime_wide_screen_allow_app_hide_content_area"/>
<item type="bool" name="car_ui_list_item_single_line_title"/>
<item type="bool" name="car_ui_preference_list_show_full_screen"/>
<item type="bool" name="car_ui_preference_show_chevron"/>
<item type="bool" name="car_ui_scrollbar_enable"/>
<item type="bool" name="car_ui_toolbar_logo_fills_nav_icon_space"/>
+ <item type="bool" name="car_ui_toolbar_menuitem_individual_click_listeners"/>
<item type="bool" name="car_ui_toolbar_nav_icon_reserve_space"/>
<item type="bool" name="car_ui_toolbar_show_logo"/>
<item type="bool" name="car_ui_toolbar_tab_flexible_layout"/>
@@ -87,6 +92,12 @@
<item type="color" name="car_ui_activity_background_color"/>
<item type="color" name="car_ui_color_accent"/>
<item type="color" name="car_ui_dialog_icon_color"/>
+ <item type="color" name="car_ui_ime_wide_screen_description_color"/>
+ <item type="color" name="car_ui_ime_wide_screen_description_title_color"/>
+ <item type="color" name="car_ui_ime_wide_screen_divider_color"/>
+ <item type="color" name="car_ui_ime_wide_screen_error_text_color"/>
+ <item type="color" name="car_ui_ime_wide_screen_search_item_sub_title_color"/>
+ <item type="color" name="car_ui_ime_wide_screen_search_item_title_color"/>
<item type="color" name="car_ui_list_item_divider"/>
<item type="color" name="car_ui_preference_icon_color"/>
<item type="color" name="car_ui_preference_two_action_divider_color"/>
@@ -94,6 +105,8 @@
<item type="color" name="car_ui_ripple_color"/>
<item type="color" name="car_ui_rotary_focus_fill_color"/>
<item type="color" name="car_ui_rotary_focus_fill_secondary_color"/>
+ <item type="color" name="car_ui_rotary_focus_pressed_fill_color"/>
+ <item type="color" name="car_ui_rotary_focus_pressed_stroke_color"/>
<item type="color" name="car_ui_rotary_focus_stroke_color"/>
<item type="color" name="car_ui_rotary_focus_stroke_secondary_color"/>
<item type="color" name="car_ui_scrollbar_thumb"/>
@@ -120,6 +133,37 @@
<item type="dimen" name="car_ui_dialog_title_margin"/>
<item type="dimen" name="car_ui_divider_width"/>
<item type="dimen" name="car_ui_header_list_item_text_start_margin"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_action_button_height"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_action_button_margin_bottom"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_action_button_margin_left"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_action_button_text_size"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_description_padding_top"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_description_text_size"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_description_title_margin_top"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_description_title_padding_left"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_description_title_text_size"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_divider_width"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_error_text_padding_start"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_error_text_size"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_input_area_height"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_input_area_margin_top"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_input_edit_text_padding_left"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_input_edit_text_padding_right"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_input_edit_text_size"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_input_padding_start"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_keyboard_area_padding_bottom"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_keyboard_area_padding_end"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_keyboard_area_padding_start"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_keyboard_width"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_recycler_view_padding_top"/>
+ <item type="dimen" name="car_ui_ime_wide_search_item_icon_size"/>
+ <item type="dimen" name="car_ui_ime_wide_search_item_secondary_image_padding_left"/>
+ <item type="dimen" name="car_ui_ime_wide_search_item_sub_title_padding_left"/>
+ <item type="dimen" name="car_ui_ime_wide_search_item_sub_title_padding_top"/>
+ <item type="dimen" name="car_ui_ime_wide_search_item_sub_title_text_size"/>
+ <item type="dimen" name="car_ui_ime_wide_search_item_title_padding_left"/>
+ <item type="dimen" name="car_ui_ime_wide_search_item_title_padding_top"/>
+ <item type="dimen" name="car_ui_ime_wide_search_item_title_text_size"/>
<item type="dimen" name="car_ui_list_item_action_divider_height"/>
<item type="dimen" name="car_ui_list_item_action_divider_width"/>
<item type="dimen" name="car_ui_list_item_avatar_icon_height"/>
@@ -173,6 +217,7 @@
<item type="dimen" name="car_ui_recyclerview_divider_height"/>
<item type="dimen" name="car_ui_recyclerview_divider_start_margin"/>
<item type="dimen" name="car_ui_recyclerview_divider_top_margin"/>
+ <item type="dimen" name="car_ui_rotary_focus_pressed_stroke_width"/>
<item type="dimen" name="car_ui_rotary_focus_stroke_width"/>
<item type="dimen" name="car_ui_scrollbar_button_size"/>
<item type="dimen" name="car_ui_scrollbar_container_width"/>
@@ -231,12 +276,19 @@
<item type="drawable" name="car_ui_icon_delete"/>
<item type="drawable" name="car_ui_icon_down"/>
<item type="drawable" name="car_ui_icon_edit"/>
+ <item type="drawable" name="car_ui_icon_error"/>
<item type="drawable" name="car_ui_icon_lock"/>
<item type="drawable" name="car_ui_icon_overflow_menu"/>
<item type="drawable" name="car_ui_icon_save"/>
<item type="drawable" name="car_ui_icon_search"/>
<item type="drawable" name="car_ui_icon_search_nav_icon"/>
<item type="drawable" name="car_ui_icon_settings"/>
+ <item type="drawable" name="car_ui_ime_wide_screen_background"/>
+ <item type="drawable" name="car_ui_ime_wide_screen_content_area_background"/>
+ <item type="drawable" name="car_ui_ime_wide_screen_input_area_background"/>
+ <item type="drawable" name="car_ui_ime_wide_screen_input_area_tint_color"/>
+ <item type="drawable" name="car_ui_ime_wide_screen_input_area_tint_error_color"/>
+ <item type="drawable" name="car_ui_ime_wide_screen_no_content_background"/>
<item type="drawable" name="car_ui_list_header_background"/>
<item type="drawable" name="car_ui_list_item_avatar_icon_outline"/>
<item type="drawable" name="car_ui_list_item_background"/>
@@ -245,6 +297,7 @@
<item type="drawable" name="car_ui_preference_icon_chevron"/>
<item type="drawable" name="car_ui_preference_icon_chevron_disabled"/>
<item type="drawable" name="car_ui_preference_icon_chevron_enabled"/>
+ <item type="drawable" name="car_ui_recycler_view_ime_wide_screen_thumb"/>
<item type="drawable" name="car_ui_recyclerview_button_ripple_background"/>
<item type="drawable" name="car_ui_recyclerview_divider"/>
<item type="drawable" name="car_ui_recyclerview_ic_down"/>
@@ -266,7 +319,14 @@
<item type="id" name="car_ui_alert_subtitle"/>
<item type="id" name="car_ui_alert_title"/>
<item type="id" name="car_ui_base_layout_content_container"/>
+ <item type="id" name="car_ui_closeKeyboard"/>
+ <item type="id" name="car_ui_contentAreaAutomotive"/>
<item type="id" name="car_ui_focus_area"/>
+ <item type="id" name="car_ui_fullscreenArea"/>
+ <item type="id" name="car_ui_imeWideScreenInputArea"/>
+ <item type="id" name="car_ui_ime_surface"/>
+ <item type="id" name="car_ui_inputExtractActionAutomotive"/>
+ <item type="id" name="car_ui_inputExtractEditTextContainer"/>
<item type="id" name="car_ui_list_item_end_guideline"/>
<item type="id" name="car_ui_list_item_start_guideline"/>
<item type="id" name="car_ui_list_limiting_message"/>
@@ -288,7 +348,6 @@
<item type="id" name="car_ui_toolbar_menu_item_icon_container"/>
<item type="id" name="car_ui_toolbar_menu_item_switch"/>
<item type="id" name="car_ui_toolbar_menu_item_text"/>
- <item type="id" name="car_ui_toolbar_menu_item_text_container"/>
<item type="id" name="car_ui_toolbar_menu_item_text_with_icon"/>
<item type="id" name="car_ui_toolbar_menu_items_container"/>
<item type="id" name="car_ui_toolbar_nav_icon"/>
@@ -310,6 +369,14 @@
<item type="id" name="car_ui_toolbar_title_logo"/>
<item type="id" name="car_ui_toolbar_title_logo_container"/>
<item type="id" name="car_ui_toolbar_top_guideline"/>
+ <item type="id" name="car_ui_wideScreenClearData"/>
+ <item type="id" name="car_ui_wideScreenDescription"/>
+ <item type="id" name="car_ui_wideScreenDescriptionTitle"/>
+ <item type="id" name="car_ui_wideScreenError"/>
+ <item type="id" name="car_ui_wideScreenErrorMessage"/>
+ <item type="id" name="car_ui_wideScreenExtractedTextIcon"/>
+ <item type="id" name="car_ui_wideScreenInputArea"/>
+ <item type="id" name="car_ui_wideScreenSearchResultList"/>
<item type="id" name="checkbox_widget"/>
<item type="id" name="container"/>
<item type="id" name="content_icon"/>
@@ -351,6 +418,7 @@
<item type="layout" name="car_ui_base_layout_toolbar"/>
<item type="layout" name="car_ui_base_layout_toolbar_legacy"/>
<item type="layout" name="car_ui_header_list_item"/>
+ <item type="layout" name="car_ui_ims_wide_screen_input_view"/>
<item type="layout" name="car_ui_list_item"/>
<item type="layout" name="car_ui_list_limiting_message"/>
<item type="layout" name="car_ui_list_preference"/>
@@ -372,6 +440,7 @@
<item type="layout" name="car_ui_seekbar_dialog"/>
<item type="layout" name="car_ui_toolbar"/>
<item type="layout" name="car_ui_toolbar_menu_item"/>
+ <item type="layout" name="car_ui_toolbar_menu_item_primary"/>
<item type="layout" name="car_ui_toolbar_search_view"/>
<item type="layout" name="car_ui_toolbar_tab_item"/>
<item type="layout" name="car_ui_toolbar_tab_item_flexible"/>
@@ -383,6 +452,7 @@
<item type="string" name="car_ui_dialog_preference_negative"/>
<item type="string" name="car_ui_dialog_preference_positive"/>
<item type="string" name="car_ui_ellipsis"/>
+ <item type="string" name="car_ui_ime_wide_screen_system_property_name"/>
<item type="string" name="car_ui_installer_process_name"/>
<item type="string" name="car_ui_preference_switch_off"/>
<item type="string" name="car_ui_preference_switch_on"/>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/drawable/car_ui_icon_error.xml b/car-ui-lib/car-ui-lib/src/main/res/drawable/car_ui_icon_error.xml
new file mode 100644
index 0000000..b0f4a34
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/res/drawable/car_ui_icon_error.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:pathData="M15.73,3L8.27,3L3,8.27v7.46L8.27,21h7.46L21,15.73L21,8.27L15.73,3zM12,17.3c-0.72,0 -1.3,-0.58 -1.3,-1.3 0,-0.72 0.58,-1.3 1.3,-1.3 0.72,0 1.3,0.58 1.3,1.3 0,0.72 -0.58,1.3 -1.3,1.3zM13,13h-2L11,7h2v6z"
+ android:fillColor="#F00"/>
+</vector>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/drawable/car_ui_list_item_background.xml b/car-ui-lib/car-ui-lib/src/main/res/drawable/car_ui_list_item_background.xml
index f10416e..4d33475 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/drawable/car_ui_list_item_background.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/drawable/car_ui_list_item_background.xml
@@ -13,22 +13,20 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_focused="true" android:state_pressed="true">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/car_ui_rotary_focus_pressed_fill_color"/>
+ <stroke android:width="@dimen/car_ui_rotary_focus_pressed_stroke_width"
+ android:color="@color/car_ui_rotary_focus_pressed_stroke_color"/>
+ </shape>
+ </item>
<item android:state_focused="true">
- <layer-list>
- <item>
- <shape android:shape="rectangle">
- <solid android:color="@color/car_ui_rotary_focus_fill_color"/>
- </shape>
- </item>
- <item>
- <shape android:shape="rectangle">
- <stroke android:width="@dimen/car_ui_rotary_focus_stroke_width"
- android:color="@color/car_ui_rotary_focus_stroke_color"/>
- </shape>
- </item>
- </layer-list>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/car_ui_rotary_focus_fill_color"/>
+ <stroke android:width="@dimen/car_ui_rotary_focus_stroke_width"
+ android:color="@color/car_ui_rotary_focus_stroke_color"/>
+ </shape>
</item>
<item>
<ripple android:color="?android:attr/colorControlHighlight">
diff --git a/car-ui-lib/car-ui-lib/src/main/res/drawable/car_ui_recycler_view_ime_wide_screen_thumb.xml b/car-ui-lib/car-ui-lib/src/main/res/drawable/car_ui_recycler_view_ime_wide_screen_thumb.xml
new file mode 100644
index 0000000..5cb78e4
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/res/drawable/car_ui_recycler_view_ime_wide_screen_thumb.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <gradient android:startColor="#60A8F0" android:endColor="#60A8F0"
+ android:angle="45"/>
+ <corners android:radius="6dp" />
+</shape>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_alert_dialog_edit_text.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_alert_dialog_edit_text.xml
index d654b2b..985b7d8 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_alert_dialog_edit_text.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_alert_dialog_edit_text.xml
@@ -16,7 +16,7 @@
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android">
- <EditText
+ <com.android.car.ui.toolbar.CarUiEditText
android:id="@+id/textbox"
android:layout_width="match_parent"
android:layout_height="@dimen/car_ui_dialog_edittext_height"
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_header_list_item.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_header_list_item.xml
index 51204de..7ac4732 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_header_list_item.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_header_list_item.xml
@@ -46,6 +46,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/car_ui_header_list_item_text_start_margin"
+ android:textDirection="locale"
android:textAppearance="@style/TextAppearance.CarUi.ListItem.Header" />
<TextView
@@ -53,6 +54,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/car_ui_list_item_text_no_icon_start_margin"
+ android:textDirection="locale"
android:textAppearance="@style/TextAppearance.CarUi.ListItem.Body" />
</LinearLayout>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_ims_wide_screen_input_view.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_ims_wide_screen_input_view.xml
new file mode 100644
index 0000000..fd7539b
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_ims_wide_screen_input_view.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+
+ <RelativeLayout
+ android:layout_width="@dimen/car_ui_ime_wide_screen_keyboard_width"
+ android:layout_height="match_parent"
+ android:gravity="bottom"
+ android:paddingStart="@dimen/car_ui_ime_wide_screen_keyboard_area_padding_start"
+ android:paddingEnd="@dimen/car_ui_ime_wide_screen_keyboard_area_padding_end"
+ android:paddingBottom="@dimen/car_ui_ime_wide_screen_keyboard_area_padding_bottom"
+ android:layout_gravity="bottom"
+ android:background="@drawable/car_ui_ime_wide_screen_background">
+
+ <RelativeLayout
+ android:id="@id/car_ui_imeWideScreenInputArea"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/car_ui_ime_wide_screen_input_area_height"
+ android:layout_marginTop="@dimen/car_ui_ime_wide_screen_input_area_margin_top"
+ android:layout_alignParentTop="true">
+ <ImageView
+ android:id="@id/car_ui_closeKeyboard"
+ android:layout_width="@dimen/car_ui_primary_icon_size"
+ android:layout_height="@dimen/car_ui_primary_icon_size"
+ android:layout_centerVertical="true"
+ style="@style/Widget.CarUi.Toolbar.NavIcon"
+ android:layout_alignParentLeft="true"/>
+
+ <FrameLayout
+ android:id="@id/car_ui_fullscreenArea"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_centerVertical="true"
+ android:layout_toRightOf="@id/car_ui_closeKeyboard"
+ android:paddingLeft="@dimen/car_ui_ime_wide_screen_input_padding_start"
+ android:background="@drawable/car_ui_ime_wide_screen_input_area_background"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:id="@id/car_ui_inputExtractEditTextContainer"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:scrollbars="vertical"
+ android:gravity="left|center"
+ android:backgroundTint="@drawable/car_ui_ime_wide_screen_input_area_tint_color"
+ android:minLines="1"
+ android:inputType="text"/>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent">
+ <ImageView
+ android:id="@id/car_ui_wideScreenExtractedTextIcon"
+ android:layout_width="@dimen/car_ui_primary_icon_size"
+ android:layout_height="@dimen/car_ui_primary_icon_size"
+ android:gravity="center"
+ android:layout_gravity="center"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"/>
+
+ <ImageView
+ android:id="@id/car_ui_wideScreenClearData"
+ android:layout_width="@dimen/car_ui_primary_icon_size"
+ android:layout_height="@dimen/car_ui_primary_icon_size"
+ android:gravity="center"
+ android:layout_gravity="center"
+ android:background="@drawable/car_ui_icon_close"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"/>
+
+ <ImageView
+ android:id="@id/car_ui_wideScreenError"
+ android:layout_width="@dimen/car_ui_primary_icon_size"
+ android:layout_height="@dimen/car_ui_primary_icon_size"
+ android:gravity="center"
+ android:layout_gravity="center"
+ android:background="@drawable/car_ui_icon_error"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:visibility="gone"/>
+ </RelativeLayout>
+
+ </FrameLayout>
+ </RelativeLayout>
+
+ <TextView
+ android:id="@id/car_ui_wideScreenErrorMessage"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_below="@id/car_ui_imeWideScreenInputArea"
+ android:paddingLeft="@dimen/car_ui_ime_wide_screen_error_text_padding_start"
+ android:textColor="@color/car_ui_ime_wide_screen_error_text_color"
+ android:visibility="gone"
+ android:textSize="@dimen/car_ui_ime_wide_screen_error_text_size"/>
+
+ <FrameLayout
+ android:id="@id/car_ui_wideScreenInputArea"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true">
+ </FrameLayout>
+ </RelativeLayout>
+
+ <View
+ android:layout_width="@dimen/car_ui_ime_wide_screen_divider_width"
+ android:layout_height="match_parent"
+ android:background="@color/car_ui_ime_wide_screen_divider_color"/>
+
+ <SurfaceView
+ android:id="@id/car_ui_ime_surface"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone"
+ android:focusable="false"/>
+
+ <RelativeLayout
+ android:id="@id/car_ui_contentAreaAutomotive"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone"
+ android:background="@drawable/car_ui_ime_wide_screen_no_content_background">
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@id/car_ui_wideScreenSearchResultList"
+ android:scrollbarThumbVertical="@drawable/car_ui_recycler_view_ime_wide_screen_thumb"
+ android:scrollbars="vertical"
+ android:requiresFadingEdge="vertical"
+ android:paddingTop="@dimen/car_ui_ime_wide_screen_recycler_view_padding_top"
+ android:layout_width="match_parent"
+ android:visibility="gone"
+ android:layout_height="match_parent"/>
+ <TextView
+ android:id="@id/car_ui_wideScreenDescriptionTitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_marginTop="@dimen/car_ui_ime_wide_screen_description_title_margin_top"
+ android:paddingLeft="@dimen/car_ui_ime_wide_screen_description_title_padding_left"
+ android:textColor="@color/car_ui_ime_wide_screen_description_title_color"
+ android:textSize="@dimen/car_ui_ime_wide_screen_description_title_text_size"
+ android:visibility="gone"/>
+ <TextView
+ android:id="@id/car_ui_wideScreenDescription"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/car_ui_wideScreenDescriptionTitle"
+ android:layout_alignParentLeft="true"
+ android:paddingLeft="@dimen/car_ui_ime_wide_screen_description_title_padding_left"
+ android:paddingTop="@dimen/car_ui_ime_wide_screen_description_padding_top"
+ android:textColor="@color/car_ui_ime_wide_screen_description_color"
+ android:textSize="@dimen/car_ui_ime_wide_screen_description_text_size"
+ android:visibility="gone"/>
+
+ <Button
+ android:id="@id/car_ui_inputExtractActionAutomotive"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/car_ui_ime_wide_screen_action_button_height"
+ android:theme="@android:style/Theme.DeviceDefault"
+ android:textSize="@dimen/car_ui_ime_wide_screen_action_button_text_size"
+ android:layout_alignParentBottom="true"
+ android:visibility="gone"
+ android:layout_marginBottom="@dimen/car_ui_ime_wide_screen_action_button_margin_bottom"
+ android:layout_marginLeft="@dimen/car_ui_ime_wide_screen_action_button_margin_left"
+ android:layout_gravity="center"/>
+ </RelativeLayout>
+
+</LinearLayout>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_list_item.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_list_item.xml
index 8b5b281..d0f179e 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_list_item.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_list_item.xml
@@ -109,12 +109,14 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="@bool/car_ui_list_item_single_line_title"
+ android:textDirection="locale"
android:textAppearance="@style/TextAppearance.CarUi.ListItem" />
<TextView
android:id="@+id/body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:textDirection="locale"
android:textAppearance="@style/TextAppearance.CarUi.ListItem.Body" />
</LinearLayout>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_list_preference.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_list_preference.xml
index 70bf96d..d56fb38 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_list_preference.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_list_preference.xml
@@ -22,11 +22,16 @@
android:layout_height="match_parent"
android:background="@drawable/car_ui_activity_background">
- <com.android.car.ui.recyclerview.CarUiRecyclerView
- android:id="@+id/list"
+ <com.android.car.ui.FocusArea
+ android:id="@+id/car_ui_focus_area"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:tag="carUiPreferenceRecyclerView"
- app:enableDivider="true" />
+ android:layout_height="match_parent">
+ <com.android.car.ui.recyclerview.CarUiRecyclerView
+ android:id="@+id/list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:tag="carUiPreferenceRecyclerView"
+ app:enableDivider="true" />
+ </com.android.car.ui.FocusArea>
</FrameLayout>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_list_preference_with_toolbar.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_list_preference_with_toolbar.xml
index f42c9f1..c83528b 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_list_preference_with_toolbar.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_list_preference_with_toolbar.xml
@@ -16,23 +16,32 @@
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/container"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@drawable/car_ui_activity_background">
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/car_ui_activity_background">
- <com.android.car.ui.recyclerview.CarUiRecyclerView
- android:id="@+id/list"
+ <com.android.car.ui.FocusArea
+ android:id="@+id/car_ui_focus_area"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:tag="carUiPreferenceRecyclerView"
- app:enableDivider="true" />
+ android:layout_height="match_parent">
+ <com.android.car.ui.recyclerview.CarUiRecyclerView
+ android:id="@+id/list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:tag="carUiPreferenceRecyclerView"
+ app:enableDivider="true"/>
+ </com.android.car.ui.FocusArea>
- <com.android.car.ui.toolbar.Toolbar
- android:id="@+id/toolbar"
+ <com.android.car.ui.FocusArea
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:car_ui_state="subpage" />
+ android:layout_height="match_parent">
+ <com.android.car.ui.toolbar.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:car_ui_state="subpage"/>
+ </com.android.car.ui.FocusArea>
</FrameLayout>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_dialog_edittext.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_dialog_edittext.xml
index 04c1c37..86474a9 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_dialog_edittext.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_dialog_edittext.xml
@@ -33,7 +33,7 @@
android:layout_marginEnd="@dimen/car_ui_preference_edit_text_dialog_message_margin_end"
android:visibility="gone"/>
- <EditText
+ <com.android.car.ui.toolbar.CarUiEditText
android:id="@android:id/edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_recycler_view.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_recycler_view.xml
index 1aa70fa..a0c955f 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_recycler_view.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_recycler_view.xml
@@ -20,7 +20,7 @@
<com.android.car.ui.recyclerview.CarUiRecyclerViewContainer
android:id="@+id/car_ui_recycler_view"
- android:layout_width="0dp"
+ android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="@dimen/car_ui_scrollbar_margin"
android:tag="carUiRecyclerView"
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_toolbar_search_view.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_toolbar_search_view.xml
index c3ad68d..6a3d9ec 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_toolbar_search_view.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_toolbar_search_view.xml
@@ -19,7 +19,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
- <EditText
+ <com.android.car.ui.toolbar.CarUiEditText
android:id="@+id/car_ui_toolbar_search_bar"
android:layout_height="match_parent"
android:layout_width="match_parent"
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values-uk/strings.xml b/car-ui-lib/car-ui-lib/src/main/res/values-uk/strings.xml
index 46279f0..0ecec7e 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values-uk/strings.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values-uk/strings.xml
@@ -16,7 +16,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="car_ui_toolbar_default_search_hint" msgid="8339474149462104372">"Шукайте…"</string>
+ <string name="car_ui_toolbar_default_search_hint" msgid="8339474149462104372">"Пошук…"</string>
<string name="car_ui_scrollbar_page_down_button" msgid="746830252244551947">"Прокрутити вниз"</string>
<string name="car_ui_scrollbar_page_up_button" msgid="965431866383176249">"Прокрутити вгору"</string>
<string name="car_ui_toolbar_nav_icon_content_description" msgid="6116610935599234725">"Назад"</string>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/attrs.xml b/car-ui-lib/car-ui-lib/src/main/res/values/attrs.xml
index e823bdf..0ca5abe 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/attrs.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/attrs.xml
@@ -113,6 +113,13 @@
<!-- Bottom offset for car ui recycler view for linear layout. -->
<attr name="bottomOffset" format="integer" />
+ <!-- Whether to enable rotary scrolling. Disabled by default. With rotary scrolling enabled,
+ rotating the rotary controller will scroll rather than moving the focus when moving the
+ focus would cause a lot of scrolling. Rotary scrolling should be enabled when the recycler
+ view contains content which the user may want to see but can't interact with, either alone
+ or along with interactive (focusable) content. -->
+ <attr name="rotaryScrollEnabled" format="boolean" />
+
<!-- Number of columns in a grid layout. -->
<attr name="numOfColumns" format="integer" />
@@ -147,9 +154,30 @@
<!-- Attributes for FocusArea. -->
<declare-styleable name="FocusArea">
- <!-- The ID of a focusable descendant view which should be focused when the user nudges to
- this FocusArea, if there was no view focused in the FocusArea or
- car_ui_focus_area_default_focus_overrides_history is true. -->
+ <!-- The ID of the default focus view. The view will be prioritized when searching for a
+ focus target.
+ (1) When the user nudges the rotary controller, it will search for a target FocusArea,
+ then search for a target view within the target FocusArea, and focus on the target
+ view. The target view is chosen in the following order:
+ 1. the "android:focusedByDefault" view, if any
+ 2. the "app:defaultFocus" view, if any
+ 3. the first focusable item in a scrollable container, if any
+ 4. previously focused view, if any and the cache is not stale
+ 5. the first focusable view, if any
+ Note that 4 will be prioritized over 1&2&3 when
+ car_ui_focus_area_default_focus_overrides_history is false.
+ (2) When it needs to initialize the focus (such as when a window is opened), it will
+ search for a view in the window and focus on it. The view is chosen in the
+ following order:
+ 1. the first "android:focusedByDefault" view, if any
+ 2. the first "app:defaultFocus" view, if any
+ 3. the first focusable item in a scrollable container, if any
+ 4. the first focusable view that is not a FocusParkingView, if any
+ If there is only one FocusArea that needs to set default focus, you can use either
+ "app:defaultFocus" or "android:focusedByDefault". If there are more than one, you
+ should use "android:focusedByDefault" in the primary FocusArea, and use
+ "app:defaultFocus" in other FocusAreas. -->
+
<attr name="defaultFocus" format="reference"/>
<!-- The paddings of FocusArea highlight. It does't impact the paddings on its child views,
@@ -215,4 +243,12 @@
<!-- The ID of the target FocusArea when nudging down. -->
<attr name="nudgeDown" format="reference"/>
</declare-styleable>
+
+ <!-- Attributes for FocusParkingView. -->
+ <declare-styleable name="FocusParkingView">
+ <!-- Whether to restore focus when the frameworks wants to focus the FocusParkingView. When
+ false, the FocusParkingView allows itself to be focused instead. This should be false
+ for the FocusParkingView in an ActivityView. The default value is true. -->
+ <attr name="shouldRestoreFocus" format="boolean"/>
+ </declare-styleable>
</resources>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/bools.xml b/car-ui-lib/car-ui-lib/src/main/res/values/bools.xml
index 67efdca..ccf9c4d 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/bools.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/bools.xml
@@ -30,6 +30,8 @@
row, replacing the title -->
<bool name="car_ui_toolbar_tabs_on_second_row">false</bool>
+ <bool name="car_ui_toolbar_menuitem_individual_click_listeners">false</bool>
+
<!-- CarUiRecyclerView -->
<!-- Whether to display the Scroll Bar or not. Defaults to true. If this is set to false,
@@ -63,4 +65,13 @@
<!-- Whether to log the escrow components check automatically for all the activities or not. -->
<bool name="car_ui_escrow_check_components_automatically">false</bool>
+
+ <!-- If there is no positive/negative/neutral button, should we add one that says "dismiss"? -->
+ <bool name="car_ui_alert_dialog_force_dismiss_button">true</bool>
+ <!-- Whether or not to allow application to hide the content area in IME wide screen. -->
+ <bool name="car_ui_ime_wide_screen_allow_app_hide_content_area">true</bool>
+
+ <!-- Whether IME is aligned left or not(which means its on right). This value will be used by
+ the applications not using car-ui-lib components and are hiding the content area. -->
+ <bool name="car_ui_ime_wide_screen_aligned_left">true</bool>
</resources>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/colors.xml b/car-ui-lib/car-ui-lib/src/main/res/values/colors.xml
index d943920..b407cb6 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/colors.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/colors.xml
@@ -48,6 +48,17 @@
<color name="car_ui_rotary_focus_stroke_color">#94CBFF</color>
<color name="car_ui_rotary_focus_fill_color">#3D94CBFF</color>
+ <color name="car_ui_rotary_focus_pressed_stroke_color">#94CBFF</color>
+ <color name="car_ui_rotary_focus_pressed_fill_color">#8A94CBFF</color>
<color name="car_ui_rotary_focus_stroke_secondary_color">#0059B3</color>
<color name="car_ui_rotary_focus_fill_secondary_color">#3D0059B3</color>
+
+ <!-- IME wide screen -->
+
+ <color name="car_ui_ime_wide_screen_error_text_color">#F00</color>
+ <color name="car_ui_ime_wide_screen_divider_color">#2E3134</color>
+ <color name="car_ui_ime_wide_screen_description_title_color">#FFF</color>
+ <color name="car_ui_ime_wide_screen_description_color">#FFF</color>
+ <color name="car_ui_ime_wide_screen_search_item_title_color">#FFF</color>
+ <color name="car_ui_ime_wide_screen_search_item_sub_title_color">#FFF</color>
</resources>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/dimens.xml b/car-ui-lib/car-ui-lib/src/main/res/values/dimens.xml
index f0c2a8b..d355f99 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/dimens.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/dimens.xml
@@ -210,5 +210,41 @@
<!-- Rotary focus highlight -->
<dimen name="car_ui_rotary_focus_stroke_width">8dp</dimen>
+ <dimen name="car_ui_rotary_focus_pressed_stroke_width">4dp</dimen>
+ <!-- IME wide screen -->
+ <dimen name="car_ui_ime_wide_screen_keyboard_width">1250dp</dimen>
+ <dimen name="car_ui_ime_wide_screen_keyboard_area_padding_start">36dp</dimen>
+ <dimen name="car_ui_ime_wide_screen_keyboard_area_padding_end">36dp</dimen>
+ <dimen name="car_ui_ime_wide_screen_keyboard_area_padding_bottom">40dp</dimen>
+ <dimen name="car_ui_ime_wide_screen_input_area_height">96dp</dimen>
+ <dimen name="car_ui_ime_wide_screen_input_area_margin_top">56dp</dimen>
+ <dimen name="car_ui_ime_wide_screen_input_padding_start">36dp</dimen>
+ <dimen name="car_ui_ime_wide_screen_input_edit_text_padding_left">68dp</dimen>
+ <dimen name="car_ui_ime_wide_screen_input_edit_text_padding_right">0dp</dimen>
+ <dimen name="car_ui_ime_wide_screen_input_edit_text_size">68dp</dimen>
+ <dimen name="car_ui_ime_wide_screen_error_text_padding_start">68dp</dimen>
+ <dimen name="car_ui_ime_wide_screen_error_text_size">24dp</dimen>
+
+ <dimen name="car_ui_ime_wide_screen_divider_width">5dp</dimen>
+
+ <dimen name="car_ui_ime_wide_screen_recycler_view_padding_top">100dp</dimen>
+ <dimen name="car_ui_ime_wide_screen_description_title_margin_top">190dp</dimen>
+ <dimen name="car_ui_ime_wide_screen_description_title_padding_left">36dp</dimen>
+ <dimen name="car_ui_ime_wide_screen_description_title_text_size">44dp</dimen>
+ <dimen name="car_ui_ime_wide_screen_description_text_size">32dp</dimen>
+ <dimen name="car_ui_ime_wide_screen_description_padding_top">16dp</dimen>
+ <dimen name="car_ui_ime_wide_screen_action_button_text_size">32dp</dimen>
+ <dimen name="car_ui_ime_wide_screen_action_button_margin_bottom">40dp</dimen>
+ <dimen name="car_ui_ime_wide_screen_action_button_margin_left">36dp</dimen>
+ <dimen name="car_ui_ime_wide_screen_action_button_height">88dp</dimen>
+
+ <dimen name="car_ui_ime_wide_search_item_icon_size">116dp</dimen>
+ <dimen name="car_ui_ime_wide_search_item_title_text_size">32dp</dimen>
+ <dimen name="car_ui_ime_wide_search_item_title_padding_left">24dp</dimen>
+ <dimen name="car_ui_ime_wide_search_item_title_padding_top">22dp</dimen>
+ <dimen name="car_ui_ime_wide_search_item_sub_title_padding_left">24dp</dimen>
+ <dimen name="car_ui_ime_wide_search_item_sub_title_padding_top">22dp</dimen>
+ <dimen name="car_ui_ime_wide_search_item_sub_title_text_size">24dp</dimen>
+ <dimen name="car_ui_ime_wide_search_item_secondary_image_padding_left">36dp</dimen>
</resources>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/drawables.xml b/car-ui-lib/car-ui-lib/src/main/res/values/drawables.xml
index 181846c..34ee538 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/drawables.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/drawables.xml
@@ -36,4 +36,25 @@
<item name="car_ui_preference_icon_chevron_enabled" type="drawable">@null</item>
<!-- Overlayable drawable to use for the preference chevron when preference is disabled -->
<item name="car_ui_preference_icon_chevron_disabled" type="drawable">@null</item>
+
+ <!-- IME wide screen -->
+
+ <!-- Background of the entire area other than content area which is
+ input text box and keyboard. -->
+ <drawable name="car_ui_ime_wide_screen_background">#000</drawable>
+
+ <!-- Background of the content area. -->
+ <drawable name="car_ui_ime_wide_screen_content_area_background">#000</drawable>
+
+ <!-- Background of the content area when there is no content. -->
+ <drawable name="car_ui_ime_wide_screen_no_content_background">#cc000000</drawable>
+
+ <!-- Background of the input area. -->
+ <drawable name="car_ui_ime_wide_screen_input_area_background">#000</drawable>
+
+ <!-- Tint color of input area in wide screen with error string. -->
+ <drawable name="car_ui_ime_wide_screen_input_area_tint_error_color">#f00</drawable>
+
+ <!-- Tint color of input area in wide screen. -->
+ <drawable name="car_ui_ime_wide_screen_input_area_tint_color">#f5f5f5</drawable>
</resources>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/ids.xml b/car-ui-lib/car-ui-lib/src/main/res/values/ids.xml
index 3f0c9b1..5b6f651 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/ids.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/ids.xml
@@ -17,6 +17,20 @@
<!-- Id used for the search button when using Toolbar.createSearch() method -->
<item name="search" type="id"/>
- <!-- Id used for in car_ui_toolbar_menu_item.xml -->
- <item name="car_ui_toolbar_menu_item_text_container" type="id"/>
+ <!-- WideScreen Keyboard IDs-->
+ <item type="id" name="car_ui_wideScreenInputArea"/>
+ <item type="id" name="car_ui_imeWideScreenInputArea"/>
+ <item type="id" name="car_ui_closeKeyboard"/>
+ <item type="id" name="car_ui_ime_surface"/>
+ <item type="id" name="car_ui_fullscreenArea"/>
+ <item type="id" name="car_ui_wideScreenErrorMessage"/>
+ <item type="id" name="car_ui_contentAreaAutomotive"/>
+ <item type="id" name="car_ui_wideScreenSearchResultList"/>
+ <item type="id" name="car_ui_wideScreenDescriptionTitle"/>
+ <item type="id" name="car_ui_wideScreenDescription"/>
+ <item type="id" name="car_ui_inputExtractActionAutomotive"/>
+ <item type="id" name="car_ui_wideScreenExtractedTextIcon"/>
+ <item type="id" name="car_ui_wideScreenClearData"/>
+ <item type="id" name="car_ui_wideScreenError"/>
+ <item type="id" name="car_ui_inputExtractEditTextContainer"/>
</resources>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/integers.xml b/car-ui-lib/car-ui-lib/src/main/res/values/integers.xml
index b2c737b..976a634 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/integers.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/integers.xml
@@ -25,7 +25,7 @@
<integer name="car_ui_focus_history_cache_type">2</integer>
<!-- How many milliseconds before the entry in FocusHistoryCache expires. Must be positive value
when car_ui_focus_history_cache_type is 2. -->
- <integer name="car_ui_focus_history_expiration_period_ms">10000</integer>
+ <integer name="car_ui_focus_history_expiration_period_ms">300000</integer>
<!-- Type of FocusAreaHistoryCache. The values are defined in RotaryCache. 1 means the
cache is disabled, 2 means entries in the cache will expire after a period of time, and 3 means
@@ -33,5 +33,5 @@
<integer name="car_ui_focus_area_history_cache_type">2</integer>
<!-- How many milliseconds before an entry in FocusAreaHistoryCache expires. Must be positive
value when car_ui_focus_area_history_cache_type is 2. -->
- <integer name="car_ui_focus_area_history_expiration_period_ms">10000</integer>
+ <integer name="car_ui_focus_area_history_expiration_period_ms">3000</integer>
</resources>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/strings.xml b/car-ui-lib/car-ui-lib/src/main/res/values/strings.xml
index 46a29fe..a3f0539 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/strings.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/strings.xml
@@ -39,9 +39,11 @@
<string name="car_ui_toolbar_menu_item_overflow_title">Overflow</string>
<!-- Positive option for a preference dialog. [CHAR_LIMIT=30] -->
- <string name="car_ui_dialog_preference_positive" translatable="false">@android:string/ok</string>
+ <string name="car_ui_dialog_preference_positive" translatable="false">@android:string/ok
+ </string>
<!-- Negative option for a preference dialog. [CHAR_LIMIT=30] -->
- <string name="car_ui_dialog_preference_negative" translatable="false">@android:string/cancel</string>
+ <string name="car_ui_dialog_preference_negative" translatable="false">@android:string/cancel
+ </string>
<!-- Text to show when a preference switch is on. [CHAR_LIMIT=30] -->
<string name="car_ui_preference_switch_on">On</string>
<!-- Text to show when a preference switch is off. [CHAR_LIMIT=30] -->
@@ -61,4 +63,18 @@
<!-- Clients should override this value instead of changing the process name -->
<!-- from manifest file. -->
<string name="car_ui_installer_process_name" translatable="false"></string>
+
+ <!--
+ List of packages allowed to hide the content area in wide screen mode when
+ bool/car_ui_ime_wide_screen_allow_app_hide_content_area is set to false. Each package name
+ should be separated by a ",". For example, "com.package1,com.package2,com.package3" will allow
+ packages "com.package1", "com.package2" and "com.package3" to hide the content area.
+ -->
+ <string-array name="car_ui_ime_wide_screen_allowed_package_list" translatable="false">
+ </string-array>
+
+ <!-- Name of system property used to determine when wide screen mode is used. -->
+ <string name="car_ui_ime_wide_screen_system_property_name" translatable="false">
+ ro.build.automotive.ime.wide_screen.enabled
+ </string>
</resources>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/themes.xml b/car-ui-lib/car-ui-lib/src/main/res/values/themes.xml
index 34bbf3c..a599fa0 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/themes.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/themes.xml
@@ -155,6 +155,7 @@
<item name="seekBarStyle">?android:attr/seekBarStyle</item>
<!-- Button styles -->
+ <item name="android:buttonStyle">@style/Widget.CarUi.Button</item>
<item name="buttonStyle">?android:attr/buttonStyle</item>
<item name="buttonStyleSmall">?android:attr/buttonStyleSmall</item>
@@ -204,6 +205,9 @@
<!-- Used by CarUiRecyclerView -->
<item name="carUiRecyclerViewStyle">@style/Widget.CarUi.CarUiRecyclerView</item>
+
+ <!-- textAppearance -->
+ <item name="android:textAppearance">@style/TextAppearance.CarUi</item>
</style>
<!-- TODO(b/150230923) remove this when other apps are ready -->
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/values.xml b/car-ui-lib/car-ui-lib/src/main/res/values/values.xml
index 82a4d65..35f033c 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/values.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/values.xml
@@ -19,6 +19,7 @@
<!-- Toolbar -->
<!-- Layout to be used for toolbar tabs -->
- <item name="car_ui_toolbar_tab_item_layout" type="layout">@layout/car_ui_toolbar_tab_item</item>
- <item name="car_ui_toolbar_tab_item_layout_flexible" type="layout">@layout/car_ui_toolbar_tab_item_flexible</item>
+ <layout name="car_ui_toolbar_tab_item_layout">@layout/car_ui_toolbar_tab_item</layout>
+ <layout name="car_ui_toolbar_tab_item_layout_flexible">@layout/car_ui_toolbar_tab_item_flexible</layout>
+ <layout name="car_ui_toolbar_menu_item_primary">@layout/car_ui_toolbar_menu_item</layout>
</resources>
diff --git a/car-ui-lib/paintbooth/AndroidManifest-gradle.xml b/car-ui-lib/paintbooth/AndroidManifest-gradle.xml
index 3184bd7..36823ec 100644
--- a/car-ui-lib/paintbooth/AndroidManifest-gradle.xml
+++ b/car-ui-lib/paintbooth/AndroidManifest-gradle.xml
@@ -49,18 +49,27 @@
android:exported="false"
android:parentActivityName=".MainActivity"/>
<activity
+ android:name=".widescreenime.WideScreenImeActivity"
+ android:windowSoftInputMode="stateHidden|adjustNothing"
+ android:exported="false"
+ android:parentActivityName=".MainActivity">
+ </activity>
+ <activity
android:name=".toolbar.ToolbarActivity"
android:exported="false"
android:parentActivityName=".MainActivity">
<meta-data android:name="distractionOptimized" android:value="true"/>
</activity>
<activity
+ android:name=".toolbar.NoCarUiToolbarActivity"
+ android:exported="false"
+ android:parentActivityName=".MainActivity"
+ android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
+ <activity
android:name=".toolbar.OldToolbarActivity"
android:exported="false"
android:parentActivityName=".MainActivity"
- android:theme="@style/Theme.CarUi">
- <meta-data android:name="distractionOptimized" android:value="true"/>
- </activity>
+ android:theme="@style/Theme.CarUi"/>
<activity
android:name=".overlays.OverlayActivity"
android:exported="false"
diff --git a/car-ui-lib/paintbooth/AndroidManifest.xml b/car-ui-lib/paintbooth/AndroidManifest.xml
index c277ff5..9763b47 100644
--- a/car-ui-lib/paintbooth/AndroidManifest.xml
+++ b/car-ui-lib/paintbooth/AndroidManifest.xml
@@ -72,6 +72,12 @@
android:exported="false"
android:parentActivityName=".MainActivity"/>
<activity
+ android:name=".widescreenime.WideScreenImeActivity"
+ android:windowSoftInputMode="stateHidden|adjustNothing"
+ android:exported="false"
+ android:parentActivityName=".MainActivity">
+ </activity>
+ <activity
android:name=".toolbar.ToolbarActivity"
android:exported="false"
android:parentActivityName=".MainActivity">
diff --git a/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/MainActivity.java b/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/MainActivity.java
index 02b8088..bf50162 100644
--- a/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/MainActivity.java
+++ b/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/MainActivity.java
@@ -46,6 +46,7 @@
import com.android.car.ui.paintbooth.toolbar.NoCarUiToolbarActivity;
import com.android.car.ui.paintbooth.toolbar.OldToolbarActivity;
import com.android.car.ui.paintbooth.toolbar.ToolbarActivity;
+import com.android.car.ui.paintbooth.widescreenime.WideScreenImeActivity;
import com.android.car.ui.paintbooth.widgets.WidgetActivity;
import com.android.car.ui.recyclerview.CarUiRecyclerView;
import com.android.car.ui.toolbar.ToolbarController;
@@ -76,6 +77,7 @@
new ActivityElement("Old toolbar sample", OldToolbarActivity.class),
new ActivityElement("No CarUiToolbar sample", NoCarUiToolbarActivity.class),
new ActivityElement("Widget sample", WidgetActivity.class),
+ new ActivityElement("Wide Screen IME", WideScreenImeActivity.class),
new ActivityElement("ListItem sample", CarUiListItemActivity.class));
private abstract static class ViewHolder extends RecyclerView.ViewHolder {
diff --git a/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/VisibleBoundsSimulator.java b/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/VisibleBoundsSimulator.java
index 63a2aed..b3dfeb1 100644
--- a/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/VisibleBoundsSimulator.java
+++ b/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/VisibleBoundsSimulator.java
@@ -106,15 +106,12 @@
int screenHeight = displayMetrics.heightPixels;
int screenWidth = displayMetrics.widthPixels;
LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
- WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+ final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
- // WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY is a hidden api, so
- // use its value here so we can still compile on gradle / google3
- WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW + 26,
+ WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
- | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
PixelFormat.TRANSLUCENT);
params.packageName = this.getPackageName();
diff --git a/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/caruirecyclerview/CarUiListItemActivity.java b/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/caruirecyclerview/CarUiListItemActivity.java
index 1819427..68ec45b 100644
--- a/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/caruirecyclerview/CarUiListItemActivity.java
+++ b/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/caruirecyclerview/CarUiListItemActivity.java
@@ -191,6 +191,11 @@
item = new CarUiContentListItem(CarUiContentListItem.Action.ICON);
item.setTitle("Supplemental icon with listener");
+ item.setPrimaryIconType(CarUiContentListItem.IconType.CONTENT);
+ item.setIcon(getDrawable(R.drawable.ic_launcher));
+ item.setBody("body");
+ item.setOnItemClickedListener(v -> Toast.makeText(context, "Clicked item",
+ Toast.LENGTH_SHORT).show());
item.setSupplementalIcon(getDrawable(R.drawable.ic_launcher),
v -> Toast.makeText(context, "Clicked supplemental icon",
Toast.LENGTH_SHORT).show());
diff --git a/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/dialogs/DialogsActivity.java b/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/dialogs/DialogsActivity.java
index 7cf1cce..3d390fc 100644
--- a/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/dialogs/DialogsActivity.java
+++ b/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/dialogs/DialogsActivity.java
@@ -18,6 +18,7 @@
import android.Manifest;
import android.app.Activity;
+import android.app.AlertDialog;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Pair;
@@ -35,6 +36,8 @@
import com.android.car.ui.baselayout.InsetsChangedListener;
import com.android.car.ui.core.CarUi;
import com.android.car.ui.paintbooth.R;
+import com.android.car.ui.recyclerview.CarUiContentListItem;
+import com.android.car.ui.recyclerview.CarUiListItemAdapter;
import com.android.car.ui.recyclerview.CarUiRadioButtonListItem;
import com.android.car.ui.recyclerview.CarUiRadioButtonListItemAdapter;
import com.android.car.ui.recyclerview.CarUiRecyclerView;
@@ -83,6 +86,8 @@
v -> showDialogWithLongSubtitleAndIcon()));
mButtons.add(Pair.create(R.string.dialog_show_single_choice,
v -> showDialogWithSingleChoiceItems()));
+ mButtons.add(Pair.create(R.string.dialog_show_list_items_without_default_button,
+ v -> showDialogWithListItemsWithoutDefaultButton()));
mButtons.add(Pair.create(R.string.dialog_show_permission_dialog,
v -> showPermissionDialog()));
mButtons.add(Pair.create(R.string.dialog_show_multi_permission_dialog,
@@ -142,6 +147,7 @@
new AlertDialogBuilder(this)
.setTitle("Standard Alert Dialog")
.setEditBox("Edit me please", null, null)
+ .setEditTextTitleAndDescForWideScreen("title", "desc from app")
.setPositiveButton("OK", (dialogInterface, i) -> {
})
.show();
@@ -199,6 +205,35 @@
.show();
}
+
+ private void showDialogWithListItemsWithoutDefaultButton() {
+ ArrayList<CarUiContentListItem> data = new ArrayList<>();
+ AlertDialog[] dialog = new AlertDialog[1];
+
+ CarUiContentListItem item = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
+ item.setTitle("First item");
+ item.setOnItemClickedListener(i -> dialog[0].dismiss());
+ data.add(item);
+
+
+ item = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
+ item.setTitle("Second item");
+ item.setOnItemClickedListener(i -> dialog[0].dismiss());
+ data.add(item);
+
+ item = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
+ item.setTitle("Third item");
+ item.setOnItemClickedListener(i -> dialog[0].dismiss());
+ data.add(item);
+
+ dialog[0] = new AlertDialogBuilder(this)
+ .setTitle("Select one option.")
+ .setSubtitle("Ony one option may be selected at a time")
+ .setAdapter(new CarUiListItemAdapter(data))
+ .setAllowDismissButton(false)
+ .show();
+ }
+
private void showDialogWithSubtitleAndIcon() {
new AlertDialogBuilder(this)
.setTitle("My Title!")
diff --git a/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java b/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java
index f32818a..b9a9daa 100644
--- a/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java
+++ b/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java
@@ -159,6 +159,30 @@
toolbar.setMenuItems(mMenuItems);
}));
+ mButtons.add(Pair.create(getString(R.string.toolbar_add_bordered_text), v -> {
+ mMenuItems.add(MenuItem.builder(this)
+ .setTitle("Baz")
+ .setPrimary(true)
+ .setOnClickListener(
+ i -> Toast.makeText(this, "Clicked",
+ Toast.LENGTH_SHORT).show())
+ .build());
+ toolbar.setMenuItems(mMenuItems);
+ }));
+
+ mButtons.add(Pair.create(getString(R.string.toolbar_add_bordered_icon_text), v -> {
+ mMenuItems.add(MenuItem.builder(this)
+ .setIcon(R.drawable.ic_tracklist)
+ .setTitle("Bar")
+ .setPrimary(true)
+ .setShowIconAndTitle(true)
+ .setOnClickListener(
+ i -> Toast.makeText(this, "Clicked",
+ Toast.LENGTH_SHORT).show())
+ .build());
+ toolbar.setMenuItems(mMenuItems);
+ }));
+
mButtons.add(Pair.create(getString(R.string.toolbar_add_untinted_icon_and_text), v -> {
mMenuItems.add(MenuItem.builder(this)
.setIcon(R.drawable.ic_tracklist)
diff --git a/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/widescreenime/WideScreenImeActivity.java b/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/widescreenime/WideScreenImeActivity.java
new file mode 100644
index 0000000..71eb954
--- /dev/null
+++ b/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/widescreenime/WideScreenImeActivity.java
@@ -0,0 +1,378 @@
+/*
+ * 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.ui.paintbooth.widescreenime;
+
+import static android.view.inputmethod.EditorInfo.IME_FLAG_NO_EXTRACT_UI;
+
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.ADD_DESC_TITLE_TO_CONTENT_AREA;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.ADD_DESC_TO_CONTENT_AREA;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.ADD_ERROR_DESC_TO_INPUT_AREA;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.REQUEST_RENDER_CONTENT_AREA;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.WIDE_SCREEN_ACTION;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.WIDE_SCREEN_EXTRACTED_TEXT_ICON_RES_ID;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnFocusChangeListener;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.Switch;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.ui.baselayout.Insets;
+import com.android.car.ui.baselayout.InsetsChangedListener;
+import com.android.car.ui.core.CarUi;
+import com.android.car.ui.imewidescreen.CarUiImeSearchListItem;
+import com.android.car.ui.paintbooth.R;
+import com.android.car.ui.recyclerview.CarUiContentListItem;
+import com.android.car.ui.recyclerview.CarUiRecyclerView;
+import com.android.car.ui.toolbar.MenuItem;
+import com.android.car.ui.toolbar.Toolbar;
+import com.android.car.ui.toolbar.Toolbar.State;
+import com.android.car.ui.toolbar.ToolbarController;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Activity that shows different scenarios for wide screen ime.
+ */
+public class WideScreenImeActivity extends AppCompatActivity implements InsetsChangedListener {
+
+ private static final String TAG = "WideScreenImeActivity";
+
+ private final List<MenuItem> mMenuItems = new ArrayList<>();
+ private final List<ListElement> mWidescreenItems = new ArrayList<>();
+
+ private final ArrayList<String> mItemIdList = new ArrayList<>();
+ private final ArrayList<String> mTitleList = new ArrayList<>();
+ private final ArrayList<String> mSubTitleList = new ArrayList<>();
+ private final ArrayList<Integer> mPrimaryImageResId = new ArrayList<>();
+ private final ArrayList<String> mSecondaryItemId = new ArrayList<>();
+ private final ArrayList<Integer> mSecondaryImageResId = new ArrayList<>();
+ private final List<CarUiImeSearchListItem> mSearchItems = new ArrayList<>();
+
+ private InputMethodManager mInputMethodManager;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.car_ui_recycler_view_activity);
+
+ mInputMethodManager = (InputMethodManager)
+ getSystemService(Context.INPUT_METHOD_SERVICE);
+
+ ToolbarController toolbar = CarUi.getToolbar(this);
+ toolbar.setTitle(getTitle());
+ toolbar.setState(Toolbar.State.SUBPAGE);
+ toolbar.setLogo(R.drawable.ic_launcher);
+ toolbar.registerOnBackListener(
+ () -> {
+ if (toolbar.getState() == Toolbar.State.SEARCH
+ || toolbar.getState() == Toolbar.State.EDIT) {
+ toolbar.setState(Toolbar.State.SUBPAGE);
+ return true;
+ }
+ return false;
+ });
+
+ CarUiContentListItem.OnClickListener mainClickListener = i ->
+ Toast.makeText(this, "Item clicked!", Toast.LENGTH_SHORT).show();
+ CarUiContentListItem.OnClickListener secondaryClickListener = i ->
+ Toast.makeText(this, "Item's secondary action clicked!", Toast.LENGTH_SHORT).show();
+
+ Drawable icon = getDrawable(R.drawable.ic_launcher);
+
+ for (int i = 1; i <= 100; i++) {
+ CarUiImeSearchListItem item = new CarUiImeSearchListItem(
+ CarUiContentListItem.Action.ICON);
+ item.setTitle("Title " + i);
+ item.setBody("Sub title " + i);
+ item.setIcon(icon);
+ item.setSupplementalIcon(icon, secondaryClickListener);
+ item.setOnItemClickedListener(mainClickListener);
+
+ mSearchItems.add(item);
+ }
+
+ AtomicBoolean showResultsInView = new AtomicBoolean(false);
+
+ mMenuItems.add(MenuItem.builder(this)
+ .setToSearch()
+ .setOnClickListener(i -> {
+ toolbar.setState(State.SEARCH);
+ if (showResultsInView.get() && toolbar.canShowSearchResultsView()) {
+ LayoutInflater inflater = LayoutInflater.from(this);
+ View contentArea = inflater.inflate(R.layout.ime_wide_screen_dummy_view,
+ null, true);
+ contentArea.findViewById(R.id.button_1).setOnClickListener(v ->
+ Toast.makeText(this, "Button 1 clicked", Toast.LENGTH_SHORT).show()
+ );
+
+ contentArea.findViewById(R.id.button_2).setOnClickListener(v -> {
+ Toast.makeText(this, "Clearing the view...",
+ Toast.LENGTH_SHORT).show();
+ toolbar.setSearchResultsView(null);
+ }
+ );
+ toolbar.setSearchResultsView(contentArea);
+ } else if (toolbar.canShowSearchResultItems()) {
+ toolbar.setSearchResultsView(null);
+ toolbar.setSearchResultItems(mSearchItems);
+ }
+ })
+ .build());
+
+ toolbar.setMenuItems(mMenuItems);
+
+ mWidescreenItems.add(new ButtonElement("Show custom search view", v -> {
+ Switch swtch = (Switch) v;
+ showResultsInView.set(swtch.isChecked());
+ }));
+
+ mWidescreenItems.add(new EditTextElement("Default Input Edit Text field", null));
+ mWidescreenItems.add(
+ new EditTextElement("Add Desc to content area", this::addDescToContentArea));
+ mWidescreenItems.add(new EditTextElement("Hide the content area", this::hideContentArea));
+ mWidescreenItems.add(new EditTextElement("Hide extraction view", this::hideExtractionView));
+
+ for (int i = 0; i < 7; i++) {
+ mItemIdList.add("itemId" + i);
+ mTitleList.add("Title " + i);
+ mSubTitleList.add("subtitle " + i);
+ mPrimaryImageResId.add(R.drawable.ic_launcher);
+ mSecondaryItemId.add("imageId" + i);
+ mSecondaryImageResId.add(R.drawable.ic_launcher);
+ }
+
+ mWidescreenItems.add(
+ new EditTextElement("Add icon to extracted view", this::addIconToExtractedView));
+ mWidescreenItems.add(new EditTextElement("Add error message to content area",
+ this::addErrorDescToContentArea));
+
+ CarUiRecyclerView recyclerView = requireViewById(R.id.list);
+ recyclerView.setAdapter(mAdapter);
+ }
+
+ private void addIconToExtractedView(View view, boolean hasFocus) {
+ if (!hasFocus) {
+ return;
+ }
+
+ Bundle bundle = new Bundle();
+ bundle.putInt(WIDE_SCREEN_EXTRACTED_TEXT_ICON_RES_ID, R.drawable.car_ui_icon_edit);
+ mInputMethodManager.sendAppPrivateCommand(view, WIDE_SCREEN_ACTION, bundle);
+ }
+
+ private void addErrorDescToContentArea(View view, boolean hasFocus) {
+ if (!hasFocus) {
+ return;
+ }
+
+ Bundle bundle = new Bundle();
+ bundle.putString(ADD_ERROR_DESC_TO_INPUT_AREA, "Some error message");
+ bundle.putString(ADD_DESC_TITLE_TO_CONTENT_AREA, "Title");
+ bundle.putString(ADD_DESC_TO_CONTENT_AREA, "Description provided by the application");
+ mInputMethodManager.sendAppPrivateCommand(view, WIDE_SCREEN_ACTION, bundle);
+ }
+
+ private void hideExtractionView(View view, boolean hasFocus) {
+ if (!hasFocus) {
+ return;
+ }
+
+ EditText editText = (EditText) view;
+ editText.setImeOptions(IME_FLAG_NO_EXTRACT_UI);
+
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(REQUEST_RENDER_CONTENT_AREA, false);
+ mInputMethodManager.sendAppPrivateCommand(view, WIDE_SCREEN_ACTION, bundle);
+ }
+
+ private void addDescToContentArea(View view, boolean hasFocus) {
+ if (!hasFocus) {
+ return;
+ }
+
+ Bundle bundle = new Bundle();
+ bundle.putString(ADD_DESC_TITLE_TO_CONTENT_AREA, "Title");
+ bundle.putString(ADD_DESC_TO_CONTENT_AREA, "Description provided by the application");
+ mInputMethodManager.sendAppPrivateCommand(view, WIDE_SCREEN_ACTION, bundle);
+ }
+
+ private void hideContentArea(View view, boolean hasFocus) {
+ if (!hasFocus) {
+ return;
+ }
+
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(REQUEST_RENDER_CONTENT_AREA, false);
+ mInputMethodManager.sendAppPrivateCommand(view, WIDE_SCREEN_ACTION, bundle);
+ }
+
+
+ private abstract static class ViewHolder extends RecyclerView.ViewHolder {
+
+ ViewHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+
+ public abstract void bind(ListElement element);
+ }
+
+ private static class EditTextViewHolder extends ViewHolder {
+ private final EditText mEditText;
+
+ EditTextViewHolder(@NonNull View itemView) {
+ super(itemView);
+ mEditText = itemView.requireViewById(R.id.edit_text);
+ }
+
+ @Override
+ public void bind(ListElement e) {
+ if (!(e instanceof EditTextElement)) {
+ throw new IllegalArgumentException("Expected an EditTextElement");
+ }
+ EditTextElement element = (EditTextElement) e;
+ mEditText.setText(element.getText());
+ mEditText.setOnFocusChangeListener(element.getOnFocusChangeListener());
+ }
+ }
+
+ private static class ButtonViewHolder extends ViewHolder {
+ private final Switch mSwitch;
+
+ ButtonViewHolder(@NonNull View itemView) {
+ super(itemView);
+ mSwitch = itemView.requireViewById(R.id.button);
+ }
+
+ @Override
+ public void bind(ListElement e) {
+ if (!(e instanceof ButtonElement)) {
+ throw new IllegalArgumentException("Expected an ButtonElement");
+ }
+ ButtonElement element = (ButtonElement) e;
+ mSwitch.setText(element.getText());
+ mSwitch.setOnClickListener(element.getOnClickListener());
+ mSwitch.setChecked(false);
+ }
+ }
+
+ private final RecyclerView.Adapter<ViewHolder> mAdapter =
+ new RecyclerView.Adapter<ViewHolder>() {
+ @Override
+ public int getItemCount() {
+ return mWidescreenItems.size();
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ if (viewType == ListElement.TYPE_EDIT_TEXT) {
+ return new EditTextViewHolder(
+ inflater.inflate(R.layout.edit_text_list_item, parent, false));
+ } else if (viewType == ListElement.TYPE_BUTTON) {
+ return new ButtonViewHolder(
+ inflater.inflate(R.layout.list_item_switch, parent, false));
+ } else {
+ throw new IllegalArgumentException("Unknown viewType: " + viewType);
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ holder.bind(mWidescreenItems.get(position));
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return mWidescreenItems.get(position).getType();
+ }
+ };
+
+ @Override
+ public void onCarUiInsetsChanged(@NonNull Insets insets) {
+ requireViewById(R.id.list)
+ .setPadding(0, insets.getTop(), 0, insets.getBottom());
+ requireViewById(android.R.id.content)
+ .setPadding(insets.getLeft(), 0, insets.getRight(), 0);
+ }
+
+ private abstract static class ListElement {
+ static final int TYPE_EDIT_TEXT = 0;
+ static final int TYPE_BUTTON = 1;
+
+ private final String mText;
+
+ ListElement(String text) {
+ mText = text;
+ }
+
+ String getText() {
+ return mText;
+ }
+
+ abstract int getType();
+ }
+
+ private static class EditTextElement extends ListElement {
+ private OnFocusChangeListener mListener;
+
+ EditTextElement(String text, OnFocusChangeListener listener) {
+ super(text);
+ mListener = listener;
+ }
+
+ OnFocusChangeListener getOnFocusChangeListener() {
+ return mListener;
+ }
+
+ @Override
+ int getType() {
+ return TYPE_EDIT_TEXT;
+ }
+ }
+
+ private static class ButtonElement extends ListElement {
+ private OnClickListener mOnClickListener;
+
+ ButtonElement(String text, OnClickListener listener) {
+ super(text);
+ mOnClickListener = listener;
+ }
+
+ public OnClickListener getOnClickListener() {
+ return mOnClickListener;
+ }
+
+ @Override
+ int getType() {
+ return TYPE_BUTTON;
+ }
+ }
+}
diff --git a/car-ui-lib/paintbooth/src/main/res/layout/edit_text_list_item.xml b/car-ui-lib/paintbooth/src/main/res/layout/edit_text_list_item.xml
new file mode 100644
index 0000000..a04edd7
--- /dev/null
+++ b/car-ui-lib/paintbooth/src/main/res/layout/edit_text_list_item.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <EditText
+ android:id="@+id/edit_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:inputType="text"
+ android:layout_centerHorizontal="true"
+ android:layout_marginTop="20dp"
+ android:text="Edit Text Box"/>
+</RelativeLayout>
diff --git a/car-ui-lib/paintbooth/src/main/res/layout/ime_wide_screen_dummy_view.xml b/car-ui-lib/paintbooth/src/main/res/layout/ime_wide_screen_dummy_view.xml
new file mode 100644
index 0000000..1118ce9
--- /dev/null
+++ b/car-ui-lib/paintbooth/src/main/res/layout/ime_wide_screen_dummy_view.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/car_ui_activity_background">
+ <Button
+ android:id="@+id/button_1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Button 1"
+ android:theme="@android:style/Theme.DeviceDefault"
+ android:textSize="@dimen/car_ui_ime_wide_screen_action_button_text_size"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+ <Button
+ android:id="@+id/button_2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="20dp"
+ android:text="Clear View"
+ android:theme="@android:style/Theme.DeviceDefault"
+ android:textSize="@dimen/car_ui_ime_wide_screen_action_button_text_size"
+ app:layout_constraintTop_toBottomOf="@+id/button_1"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+</androidx.constraintlayout.widget.ConstraintLayout>
+
+
diff --git a/car-ui-lib/paintbooth/src/main/res/values/strings.xml b/car-ui-lib/paintbooth/src/main/res/values/strings.xml
index f347784..5965d68 100644
--- a/car-ui-lib/paintbooth/src/main/res/values/strings.xml
+++ b/car-ui-lib/paintbooth/src/main/res/values/strings.xml
@@ -180,6 +180,13 @@
<!-- Text for add icon text button [CHAR_LIMIT=45]-->
<string name="toolbar_add_icon_text">MenuItem: Add icon and text</string>
+
+ <!-- Text for add text button [CHAR_LIMIT=30]-->
+ <string name="toolbar_add_bordered_text">MenuItem: Add bordered text</string>
+
+ <!-- Text for add icon text button [CHAR_LIMIT=45]-->
+ <string name="toolbar_add_bordered_icon_text">MenuItem: Add bordered icon and text</string>
+
<!-- Text for add untined icon and text button [CHAR_LIMIT=60]-->
<string name="toolbar_add_untinted_icon_and_text">MenuItem: Add untinted icon and text</string>
@@ -270,6 +277,9 @@
<!-- Text to show Dialog with single choice items-->
<string name="dialog_show_single_choice">Show with single choice items</string>
+ <!-- Text to show a dialog with single choice items and no default button [CHAR_LIMIT=200] -->
+ <string name="dialog_show_list_items_without_default_button">Show with single choice items and no default button</string>
+
<!-- Text to show a permission Dialog [CHAR_LIMIT=50] -->
<string name="dialog_show_permission_dialog">Show permission dialog</string>
diff --git a/car-ui-lib/referencedesign/Android.mk b/car-ui-lib/referencedesign/Android.mk
index ac85df4..ef0ad97 100644
--- a/car-ui-lib/referencedesign/Android.mk
+++ b/car-ui-lib/referencedesign/Android.mk
@@ -20,12 +20,14 @@
com.android.car.settings \
com.android.car.voicecontrol \
com.android.car.faceenroll \
- com.android.permissioncontroller \
+ com.android.managedprovisioning \
com.android.settings.intelligence \
com.google.android.apps.automotive.inputmethod \
com.google.android.apps.automotive.inputmethod.dev \
+ com.google.android.apps.automotive.templates.host \
com.google.android.embedded.projection \
com.google.android.gms \
+ com.google.android.gsf \
com.google.android.packageinstaller \
com.google.android.carassistant \
com.google.android.tts \
@@ -39,9 +41,12 @@
CAR_UI_RRO_TARGETS := \
com.google.android.apps.automotive.inputmethod \
com.google.android.apps.automotive.inputmethod.dev \
+ com.google.android.apps.automotive.templates.host \
com.google.android.embedded.projection \
com.google.android.gms \
+ com.google.android.gsf \
com.google.android.packageinstaller \
+ com.google.android.permissioncontroller \
com.google.android.carassistant \
com.google.android.tts \
com.android.vending \
diff --git a/car-ui-lib/referencedesign/AndroidManifest-overlayable.xml b/car-ui-lib/referencedesign/AndroidManifest-overlayable.xml
index 214755c..1b0585a 100644
--- a/car-ui-lib/referencedesign/AndroidManifest-overlayable.xml
+++ b/car-ui-lib/referencedesign/AndroidManifest-overlayable.xml
@@ -6,6 +6,6 @@
android:targetName="car-ui-lib"
android:resourcesMap="@xml/overlays"
android:isStatic="true"
- android:requiredSystemPropertyName="ro.build.characteristics"
- android:requiredSystemPropertyValue="automotive"/>
+ android:requiredSystemPropertyName="ro.build.car_ui_rros_enabled"
+ android:requiredSystemPropertyValue="true"/>
</manifest>
diff --git a/car-ui-lib/referencedesign/AndroidManifest.xml b/car-ui-lib/referencedesign/AndroidManifest.xml
index a6dbae3..0994382 100644
--- a/car-ui-lib/referencedesign/AndroidManifest.xml
+++ b/car-ui-lib/referencedesign/AndroidManifest.xml
@@ -5,6 +5,6 @@
android:targetPackage="{{TARGET_PACKAGE_NAME}}"
android:resourcesMap="@xml/overlays"
android:isStatic="true"
- android:requiredSystemPropertyName="ro.build.characteristics"
- android:requiredSystemPropertyValue="automotive"/>
+ android:requiredSystemPropertyName="ro.build.car_ui_rros_enabled"
+ android:requiredSystemPropertyValue="true"/>
</manifest>
diff --git a/car-ui-lib/referencedesign/product.mk b/car-ui-lib/referencedesign/product.mk
index 92e0bea..e834714 100644
--- a/car-ui-lib/referencedesign/product.mk
+++ b/car-ui-lib/referencedesign/product.mk
@@ -17,15 +17,17 @@
googlecarui-com-android-car-settings \
googlecarui-com-android-car-voicecontrol \
googlecarui-com-android-car-faceenroll \
- googlecarui-com-android-permissioncontroller \
googlecarui-com-android-settings-intelligence \
googlecarui-com-google-android-apps-automotive-inputmethod \
googlecarui-com-google-android-apps-automotive-inputmethod-dev \
+ googlecarui-com-google-android-apps-automotive-templates-host \
googlecarui-com-google-android-embedded-projection \
googlecarui-com-google-android-gms \
+ googlecarui-com-google-android-gsf \
googlecarui-com-google-android-packageinstaller \
googlecarui-com-google-android-carassistant \
googlecarui-com-google-android-tts \
+ googlecarui-com-android-managedprovisioning \
googlecarui-com-android-vending \
@@ -33,9 +35,16 @@
PRODUCT_PACKAGES += \
googlecarui-overlayable-com-google-android-apps-automotive-inputmethod \
googlecarui-overlayable-com-google-android-apps-automotive-inputmethod-dev \
+ googlecarui-overlayable-com-google-android-apps-automotive-templates-host \
googlecarui-overlayable-com-google-android-embedded-projection \
googlecarui-overlayable-com-google-android-gms \
+ googlecarui-overlayable-com-google-android-gsf \
googlecarui-overlayable-com-google-android-packageinstaller \
+ googlecarui-overlayable-com-google-android-permissioncontroller \
googlecarui-overlayable-com-google-android-carassistant \
googlecarui-overlayable-com-google-android-tts \
googlecarui-overlayable-com-android-vending \
+
+# This system property is used to enable the RROs on startup via
+# the requiredSystemPropertyName/Value attributes in the manifest
+PRODUCT_PRODUCT_PROPERTIES += ro.build.car_ui_rros_enabled=true
diff --git a/car-ui-lib/referencedesign/res/drawable/car_ui_toolbar_menu_item_icon_ripple.xml b/car-ui-lib/referencedesign/res/drawable/car_ui_toolbar_menu_item_icon_ripple.xml
index 1f89ff2..14f6cd0 100644
--- a/car-ui-lib/referencedesign/res/drawable/car_ui_toolbar_menu_item_icon_ripple.xml
+++ b/car-ui-lib/referencedesign/res/drawable/car_ui_toolbar_menu_item_icon_ripple.xml
@@ -17,6 +17,15 @@
~
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_focused="true" android:state_pressed="true">
+ <shape android:shape="oval">
+ <solid android:color="#8A94CBFF"/>
+ <stroke android:width="4dp"
+ android:color="#94CBFF"/>
+ <size android:width="48dp"
+ android:height="48dp"/>
+ </shape>
+ </item>
<item android:state_focused="true">
<shape android:shape="oval">
<solid android:color="#3D94CBFF"/>
diff --git a/car-ui-lib/referencedesign/res/layout/car_ui_toolbar_menu_item.xml b/car-ui-lib/referencedesign/res/layout/car_ui_toolbar_menu_item.xml
index 18f2e16..0a6b24c 100644
--- a/car-ui-lib/referencedesign/res/layout/car_ui_toolbar_menu_item.xml
+++ b/car-ui-lib/referencedesign/res/layout/car_ui_toolbar_menu_item.xml
@@ -24,7 +24,7 @@
style="@style/Widget.CarUi.Toolbar.MenuItem.IndividualContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:background="@drawable/car_ui_toolbar_menu_item_icon_ripple">
+ android:layout_gravity="center">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -38,40 +38,27 @@
android:layout_gravity="center"
android:tint="@color/car_ui_toolbar_menu_item_icon_color"
android:tintMode="src_in"/>
- <com.android.car.ui.uxr.DrawableStateSwitch
- android:id="@+id/car_ui_toolbar_menu_item_switch"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:background="@null"
- android:focusable="false"
- android:clickable="false"/>
</FrameLayout>
-
- <FrameLayout
- android:id="@+id/car_ui_toolbar_menu_item_text_container"
- style="@style/Widget.CarUi.Toolbar.MenuItem.IndividualContainer"
+ <com.android.car.ui.uxr.DrawableStateSwitch
+ android:id="@+id/car_ui_toolbar_menu_item_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:background="?android:attr/selectableItemBackground">
- <!-- These buttons must have clickable="false" or they will steal the click events from the container -->
- <com.android.car.ui.uxr.DrawableStateButton
- android:id="@+id/car_ui_toolbar_menu_item_text"
- style="@style/Widget.CarUi.Toolbar.TextButton"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_gravity="center"
- android:background="@null"
- android:focusable="false"
- android:clickable="false"/>
- <com.android.car.ui.uxr.DrawableStateButton
- android:id="@+id/car_ui_toolbar_menu_item_text_with_icon"
- style="@style/Widget.CarUi.Toolbar.TextButton.WithIcon"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_gravity="center"
- android:background="@null"
- android:focusable="false"
- android:clickable="false"/>
- </FrameLayout>
+ android:layout_gravity="center"
+ android:clickable="false"/>
+
+ <!-- These buttons must have clickable="false" or they will steal the click events from the container -->
+ <com.android.car.ui.uxr.DrawableStateButton
+ android:id="@+id/car_ui_toolbar_menu_item_text"
+ style="@style/Widget.CarUi.Toolbar.TextButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:clickable="false"/>
+ <com.android.car.ui.uxr.DrawableStateButton
+ android:id="@+id/car_ui_toolbar_menu_item_text_with_icon"
+ style="@style/Widget.CarUi.Toolbar.TextButton.WithIcon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:clickable="false"/>
</FrameLayout>
diff --git a/car-ui-lib/referencedesign/res/layout/car_ui_toolbar_menu_item_primary.xml b/car-ui-lib/referencedesign/res/layout/car_ui_toolbar_menu_item_primary.xml
new file mode 100644
index 0000000..716c8ee
--- /dev/null
+++ b/car-ui-lib/referencedesign/res/layout/car_ui_toolbar_menu_item_primary.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2020, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:focusable="false">
+ <FrameLayout
+ android:id="@+id/car_ui_toolbar_menu_item_icon_container"
+ style="@style/Widget.CarUi.Toolbar.MenuItem.IndividualContainer"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center">
+ <ImageView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:src="@drawable/car_ui_toolbar_menu_item_icon_background"
+ android:scaleType="center"/>
+ <ImageView
+ android:id="@+id/car_ui_toolbar_menu_item_icon"
+ android:layout_width="44dp"
+ android:layout_height="44dp"
+ android:layout_gravity="center"
+ android:tint="@color/car_ui_toolbar_menu_item_icon_color"
+ android:tintMode="src_in"/>
+ </FrameLayout>
+ <com.android.car.ui.uxr.DrawableStateSwitch
+ android:id="@+id/car_ui_toolbar_menu_item_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:clickable="false"/>
+
+ <!-- These buttons must have clickable="false" or they will steal the click events from the container -->
+ <com.android.car.ui.uxr.DrawableStateButton
+ android:id="@+id/car_ui_toolbar_menu_item_text"
+ style="@style/Widget.CarUi.Toolbar.TextButton.Primary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:clickable="false"/>
+ <com.android.car.ui.uxr.DrawableStateButton
+ android:id="@+id/car_ui_toolbar_menu_item_text_with_icon"
+ style="@style/Widget.CarUi.Toolbar.TextButton.Primary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:clickable="false"/>
+</FrameLayout>
diff --git a/car-ui-lib/referencedesign/res/values/styles.xml b/car-ui-lib/referencedesign/res/values/styles.xml
index 433911c..b33d11a 100644
--- a/car-ui-lib/referencedesign/res/values/styles.xml
+++ b/car-ui-lib/referencedesign/res/values/styles.xml
@@ -34,13 +34,16 @@
<style name="Widget.CarUi.Toolbar.MenuItem.IndividualContainer">
<item name="android:minHeight">76dp</item>
<item name="android:minWidth">76dp</item>
- <item name="android:layout_gravity">center</item>
+ <item name="android:background">@drawable/car_ui_toolbar_menu_item_icon_ripple</item>
<item name="android:focusable">true</item>
</style>
<style name="Widget.CarUi.Button.Borderless.Colored"
parent="android:Widget.DeviceDefault.Button.Borderless.Colored"/>
+ <style name="Widget.CarUi.Button"
+ parent="android:Widget.DeviceDefault.Button"/>
+
<style name="Widget.CarUi.Toolbar.TextButton" parent="Widget.CarUi.Button.Borderless.Colored">
<item name="android:drawableTint">@color/car_ui_toolbar_menu_item_icon_color</item>
<item name="android:drawablePadding">10dp</item>
@@ -51,6 +54,12 @@
<item name="android:textColor">@color/car_ui_toolbar_menu_item_icon_color</item>
</style>
+ <style name="Widget.CarUi.Toolbar.TextButton.Primary" parent="Widget.CarUi.Button">
+ <item name="android:drawableTint">@color/car_ui_toolbar_menu_item_icon_color</item>
+ <item name="android:drawablePadding">10dp</item>
+ <item name="android:maxWidth">350dp</item>
+ </style>
+
<style name="Widget.CarUi.SeekbarPreference"/>
<!-- Style applied to the seekbar widget within the seekbar preference -->
diff --git a/car-ui-lib/referencedesign/res/values/values.xml b/car-ui-lib/referencedesign/res/values/values.xml
index 0cbb8ee..5dbf018 100644
--- a/car-ui-lib/referencedesign/res/values/values.xml
+++ b/car-ui-lib/referencedesign/res/values/values.xml
@@ -12,4 +12,6 @@
<dimen name="car_ui_toolbar_logo_size">44dp</dimen>
<dimen name="car_ui_toolbar_nav_icon_size">44dp</dimen>
+
+ <bool name="car_ui_toolbar_menuitem_individual_click_listeners">true</bool>
</resources>
diff --git a/car-ui-lib/referencedesign/res/xml/overlays.xml b/car-ui-lib/referencedesign/res/xml/overlays.xml
index cc9b092..6332b67 100644
--- a/car-ui-lib/referencedesign/res/xml/overlays.xml
+++ b/car-ui-lib/referencedesign/res/xml/overlays.xml
@@ -3,6 +3,7 @@
<item target="layout/car_ui_toolbar" value="@layout/car_ui_toolbar"/>
<item target="layout/car_ui_toolbar_two_row" value="@layout/car_ui_toolbar_two_row"/>
<item target="layout/car_ui_toolbar_menu_item" value="@layout/car_ui_toolbar_menu_item"/>
+ <item target="layout/car_ui_toolbar_menu_item_primary" value="@layout/car_ui_toolbar_menu_item_primary"/>
<item target="layout/car_ui_preference_widget_seekbar" value="@layout/car_ui_preference_widget_seekbar"/>
<item target="drawable/car_ui_icon_arrow_back" value="@drawable/car_ui_icon_arrow_back"/>
@@ -49,6 +50,7 @@
<item target="bool/car_ui_toolbar_logo_fills_nav_icon_space" value="@bool/car_ui_toolbar_logo_fills_nav_icon_space" />
<item target="bool/car_ui_toolbar_tab_flexible_layout" value="@bool/car_ui_toolbar_tab_flexible_layout" />
<item target="bool/car_ui_toolbar_tabs_on_second_row" value="@bool/car_ui_toolbar_tabs_on_second_row" />
+ <item target="bool/car_ui_toolbar_menuitem_individual_click_listeners" value="@bool/car_ui_toolbar_menuitem_individual_click_listeners" />
<item target="id/car_ui_toolbar_background" value="@id/car_ui_toolbar_background" />
<item target="id/car_ui_toolbar_nav_icon_container" value="@id/car_ui_toolbar_nav_icon_container" />
@@ -67,7 +69,6 @@
<item target="id/car_ui_toolbar_menu_item_icon_container" value="@id/car_ui_toolbar_menu_item_icon_container"/>
<item target="id/car_ui_toolbar_menu_item_icon" value="@id/car_ui_toolbar_menu_item_icon"/>
<item target="id/car_ui_toolbar_menu_item_switch" value="@id/car_ui_toolbar_menu_item_switch"/>
- <item target="id/car_ui_toolbar_menu_item_text_container" value="@id/car_ui_toolbar_menu_item_text_container"/>
<item target="id/car_ui_toolbar_menu_item_text" value="@id/car_ui_toolbar_menu_item_text"/>
<item target="id/car_ui_toolbar_menu_item_text_with_icon" value="@id/car_ui_toolbar_menu_item_text_with_icon"/>
<item target="id/seekbar" value="@id/seekbar"/>
diff --git a/car-ui-lib/tests/apitest/current.xml b/car-ui-lib/tests/apitest/current.xml
index 76eb5a2..769b040 100644
--- a/car-ui-lib/tests/apitest/current.xml
+++ b/car-ui-lib/tests/apitest/current.xml
@@ -1,20 +1,25 @@
<?xml version='1.0' encoding='UTF-8'?>
<!--This file is AUTO GENERATED, DO NOT EDIT MANUALLY.-->
<resources>
+ <public type="array" name="car_ui_ime_wide_screen_allowed_package_list"/>
<public type="attr" name="CarUiToolbarStyle"/>
<public type="attr" name="carUiPreferenceStyle"/>
<public type="attr" name="carUiRecyclerViewStyle"/>
<public type="attr" name="state_ux_restricted"/>
+ <public type="bool" name="car_ui_alert_dialog_force_dismiss_button"/>
<public type="bool" name="car_ui_clear_focus_area_history_when_rotating"/>
<public type="bool" name="car_ui_enable_focus_area_background_highlight"/>
<public type="bool" name="car_ui_enable_focus_area_foreground_highlight"/>
<public type="bool" name="car_ui_escrow_check_components_automatically"/>
<public type="bool" name="car_ui_focus_area_default_focus_overrides_history"/>
+ <public type="bool" name="car_ui_ime_wide_screen_aligned_left"/>
+ <public type="bool" name="car_ui_ime_wide_screen_allow_app_hide_content_area"/>
<public type="bool" name="car_ui_list_item_single_line_title"/>
<public type="bool" name="car_ui_preference_list_show_full_screen"/>
<public type="bool" name="car_ui_preference_show_chevron"/>
<public type="bool" name="car_ui_scrollbar_enable"/>
<public type="bool" name="car_ui_toolbar_logo_fills_nav_icon_space"/>
+ <public type="bool" name="car_ui_toolbar_menuitem_individual_click_listeners"/>
<public type="bool" name="car_ui_toolbar_nav_icon_reserve_space"/>
<public type="bool" name="car_ui_toolbar_show_logo"/>
<public type="bool" name="car_ui_toolbar_tab_flexible_layout"/>
@@ -22,6 +27,12 @@
<public type="color" name="car_ui_activity_background_color"/>
<public type="color" name="car_ui_color_accent"/>
<public type="color" name="car_ui_dialog_icon_color"/>
+ <public type="color" name="car_ui_ime_wide_screen_description_color"/>
+ <public type="color" name="car_ui_ime_wide_screen_description_title_color"/>
+ <public type="color" name="car_ui_ime_wide_screen_divider_color"/>
+ <public type="color" name="car_ui_ime_wide_screen_error_text_color"/>
+ <public type="color" name="car_ui_ime_wide_screen_search_item_sub_title_color"/>
+ <public type="color" name="car_ui_ime_wide_screen_search_item_title_color"/>
<public type="color" name="car_ui_list_item_divider"/>
<public type="color" name="car_ui_preference_icon_color"/>
<public type="color" name="car_ui_preference_two_action_divider_color"/>
@@ -29,6 +40,8 @@
<public type="color" name="car_ui_ripple_color"/>
<public type="color" name="car_ui_rotary_focus_fill_color"/>
<public type="color" name="car_ui_rotary_focus_fill_secondary_color"/>
+ <public type="color" name="car_ui_rotary_focus_pressed_fill_color"/>
+ <public type="color" name="car_ui_rotary_focus_pressed_stroke_color"/>
<public type="color" name="car_ui_rotary_focus_stroke_color"/>
<public type="color" name="car_ui_rotary_focus_stroke_secondary_color"/>
<public type="color" name="car_ui_scrollbar_thumb"/>
@@ -55,6 +68,37 @@
<public type="dimen" name="car_ui_dialog_title_margin"/>
<public type="dimen" name="car_ui_divider_width"/>
<public type="dimen" name="car_ui_header_list_item_text_start_margin"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_action_button_height"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_action_button_margin_bottom"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_action_button_margin_left"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_action_button_text_size"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_description_padding_top"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_description_text_size"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_description_title_margin_top"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_description_title_padding_left"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_description_title_text_size"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_divider_width"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_error_text_padding_start"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_error_text_size"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_input_area_height"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_input_area_margin_top"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_input_edit_text_padding_left"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_input_edit_text_padding_right"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_input_edit_text_size"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_input_padding_start"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_keyboard_area_padding_bottom"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_keyboard_area_padding_end"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_keyboard_area_padding_start"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_keyboard_width"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_recycler_view_padding_top"/>
+ <public type="dimen" name="car_ui_ime_wide_search_item_icon_size"/>
+ <public type="dimen" name="car_ui_ime_wide_search_item_secondary_image_padding_left"/>
+ <public type="dimen" name="car_ui_ime_wide_search_item_sub_title_padding_left"/>
+ <public type="dimen" name="car_ui_ime_wide_search_item_sub_title_padding_top"/>
+ <public type="dimen" name="car_ui_ime_wide_search_item_sub_title_text_size"/>
+ <public type="dimen" name="car_ui_ime_wide_search_item_title_padding_left"/>
+ <public type="dimen" name="car_ui_ime_wide_search_item_title_padding_top"/>
+ <public type="dimen" name="car_ui_ime_wide_search_item_title_text_size"/>
<public type="dimen" name="car_ui_list_item_action_divider_height"/>
<public type="dimen" name="car_ui_list_item_action_divider_width"/>
<public type="dimen" name="car_ui_list_item_avatar_icon_height"/>
@@ -108,6 +152,7 @@
<public type="dimen" name="car_ui_recyclerview_divider_height"/>
<public type="dimen" name="car_ui_recyclerview_divider_start_margin"/>
<public type="dimen" name="car_ui_recyclerview_divider_top_margin"/>
+ <public type="dimen" name="car_ui_rotary_focus_pressed_stroke_width"/>
<public type="dimen" name="car_ui_rotary_focus_stroke_width"/>
<public type="dimen" name="car_ui_scrollbar_button_size"/>
<public type="dimen" name="car_ui_scrollbar_container_width"/>
@@ -166,12 +211,19 @@
<public type="drawable" name="car_ui_icon_delete"/>
<public type="drawable" name="car_ui_icon_down"/>
<public type="drawable" name="car_ui_icon_edit"/>
+ <public type="drawable" name="car_ui_icon_error"/>
<public type="drawable" name="car_ui_icon_lock"/>
<public type="drawable" name="car_ui_icon_overflow_menu"/>
<public type="drawable" name="car_ui_icon_save"/>
<public type="drawable" name="car_ui_icon_search"/>
<public type="drawable" name="car_ui_icon_search_nav_icon"/>
<public type="drawable" name="car_ui_icon_settings"/>
+ <public type="drawable" name="car_ui_ime_wide_screen_background"/>
+ <public type="drawable" name="car_ui_ime_wide_screen_content_area_background"/>
+ <public type="drawable" name="car_ui_ime_wide_screen_input_area_background"/>
+ <public type="drawable" name="car_ui_ime_wide_screen_input_area_tint_color"/>
+ <public type="drawable" name="car_ui_ime_wide_screen_input_area_tint_error_color"/>
+ <public type="drawable" name="car_ui_ime_wide_screen_no_content_background"/>
<public type="drawable" name="car_ui_list_header_background"/>
<public type="drawable" name="car_ui_list_item_avatar_icon_outline"/>
<public type="drawable" name="car_ui_list_item_background"/>
@@ -180,6 +232,7 @@
<public type="drawable" name="car_ui_preference_icon_chevron"/>
<public type="drawable" name="car_ui_preference_icon_chevron_disabled"/>
<public type="drawable" name="car_ui_preference_icon_chevron_enabled"/>
+ <public type="drawable" name="car_ui_recycler_view_ime_wide_screen_thumb"/>
<public type="drawable" name="car_ui_recyclerview_button_ripple_background"/>
<public type="drawable" name="car_ui_recyclerview_divider"/>
<public type="drawable" name="car_ui_recyclerview_ic_down"/>
@@ -201,7 +254,14 @@
<public type="id" name="car_ui_alert_subtitle"/>
<public type="id" name="car_ui_alert_title"/>
<public type="id" name="car_ui_base_layout_content_container"/>
+ <public type="id" name="car_ui_closeKeyboard"/>
+ <public type="id" name="car_ui_contentAreaAutomotive"/>
<public type="id" name="car_ui_focus_area"/>
+ <public type="id" name="car_ui_fullscreenArea"/>
+ <public type="id" name="car_ui_imeWideScreenInputArea"/>
+ <public type="id" name="car_ui_ime_surface"/>
+ <public type="id" name="car_ui_inputExtractActionAutomotive"/>
+ <public type="id" name="car_ui_inputExtractEditTextContainer"/>
<public type="id" name="car_ui_list_item_end_guideline"/>
<public type="id" name="car_ui_list_item_start_guideline"/>
<public type="id" name="car_ui_list_limiting_message"/>
@@ -223,7 +283,6 @@
<public type="id" name="car_ui_toolbar_menu_item_icon_container"/>
<public type="id" name="car_ui_toolbar_menu_item_switch"/>
<public type="id" name="car_ui_toolbar_menu_item_text"/>
- <public type="id" name="car_ui_toolbar_menu_item_text_container"/>
<public type="id" name="car_ui_toolbar_menu_item_text_with_icon"/>
<public type="id" name="car_ui_toolbar_menu_items_container"/>
<public type="id" name="car_ui_toolbar_nav_icon"/>
@@ -245,6 +304,14 @@
<public type="id" name="car_ui_toolbar_title_logo"/>
<public type="id" name="car_ui_toolbar_title_logo_container"/>
<public type="id" name="car_ui_toolbar_top_guideline"/>
+ <public type="id" name="car_ui_wideScreenClearData"/>
+ <public type="id" name="car_ui_wideScreenDescription"/>
+ <public type="id" name="car_ui_wideScreenDescriptionTitle"/>
+ <public type="id" name="car_ui_wideScreenError"/>
+ <public type="id" name="car_ui_wideScreenErrorMessage"/>
+ <public type="id" name="car_ui_wideScreenExtractedTextIcon"/>
+ <public type="id" name="car_ui_wideScreenInputArea"/>
+ <public type="id" name="car_ui_wideScreenSearchResultList"/>
<public type="id" name="checkbox_widget"/>
<public type="id" name="container"/>
<public type="id" name="content_icon"/>
@@ -286,6 +353,7 @@
<public type="layout" name="car_ui_base_layout_toolbar"/>
<public type="layout" name="car_ui_base_layout_toolbar_legacy"/>
<public type="layout" name="car_ui_header_list_item"/>
+ <public type="layout" name="car_ui_ims_wide_screen_input_view"/>
<public type="layout" name="car_ui_list_item"/>
<public type="layout" name="car_ui_list_limiting_message"/>
<public type="layout" name="car_ui_list_preference"/>
@@ -307,6 +375,7 @@
<public type="layout" name="car_ui_seekbar_dialog"/>
<public type="layout" name="car_ui_toolbar"/>
<public type="layout" name="car_ui_toolbar_menu_item"/>
+ <public type="layout" name="car_ui_toolbar_menu_item_primary"/>
<public type="layout" name="car_ui_toolbar_search_view"/>
<public type="layout" name="car_ui_toolbar_tab_item"/>
<public type="layout" name="car_ui_toolbar_tab_item_flexible"/>
@@ -318,6 +387,7 @@
<public type="string" name="car_ui_dialog_preference_negative"/>
<public type="string" name="car_ui_dialog_preference_positive"/>
<public type="string" name="car_ui_ellipsis"/>
+ <public type="string" name="car_ui_ime_wide_screen_system_property_name"/>
<public type="string" name="car_ui_installer_process_name"/>
<public type="string" name="car_ui_preference_switch_off"/>
<public type="string" name="car_ui_preference_switch_on"/>
diff --git a/car-ui-lib/tests/apitest/resource_utils.py b/car-ui-lib/tests/apitest/resource_utils.py
index 763c5a0..7da5642 100755
--- a/car-ui-lib/tests/apitest/resource_utils.py
+++ b/car-ui-lib/tests/apitest/resource_utils.py
@@ -108,6 +108,8 @@
resName = resource.get('name')
resType = resource.tag
+ if resType == "string-array":
+ resType = "array"
if resource.tag == 'item' or resource.tag == 'public':
resType = resource.get('type')