Merge "Remove [email protected] from app/OWNERS" into main
diff --git a/OWNERS b/OWNERS
index d363985..b022f36 100644
--- a/OWNERS
+++ b/OWNERS
@@ -7,4 +7,3 @@
# Secondary
[email protected]
[email protected]
[email protected]
\ No newline at end of file
diff --git a/app/Android.bp b/app/Android.bp
index 159e5a0..8537aca 100644
--- a/app/Android.bp
+++ b/app/Android.bp
@@ -18,11 +18,11 @@
}
genrule {
- name: "statslog-carlauncher-java-gen",
- tools: ["stats-log-api-gen"],
- cmd: "$(location stats-log-api-gen) --java $(out) --module carlauncher"
- + " --javaPackage com.android.car.carlauncher --javaClass CarLauncherStatsLog",
- out: ["com/android/car/carlauncher/CarLauncherStatsLog.java"],
+ name: "statslog-carlauncher-java-gen",
+ tools: ["stats-log-api-gen"],
+ cmd: "$(location stats-log-api-gen) --java $(out) --module carlauncher" +
+ " --javaPackage com.android.car.carlauncher --javaClass CarLauncherStatsLog",
+ out: ["com/android/car/carlauncher/CarLauncherStatsLog.java"],
}
carlauncher_srcs = [
@@ -51,11 +51,14 @@
"CarAppGrid-lib",
"SystemUISharedLib",
"android.car.cluster.navigation",
+ "car-resource-common",
],
libs: ["android.car"],
manifest: "AndroidManifest.xml",
+ // TODO(b/319708040): re-enable use_resource_processor
+ use_resource_processor: false,
}
android_app {
@@ -90,11 +93,14 @@
dex_preopt: {
enabled: false,
},
+ // TODO(b/319708040): re-enable use_resource_processor
+ use_resource_processor: false,
}
aconfig_declarations {
name: "car_launcher_flags",
package: "com.android.car.carlauncher",
+ container: "system",
srcs: ["car_launcher_flags.aconfig"],
}
diff --git a/app/AndroidManifest.xml b/app/AndroidManifest.xml
index 0acb74b..50f4d47 100644
--- a/app/AndroidManifest.xml
+++ b/app/AndroidManifest.xml
@@ -75,6 +75,8 @@
<uses-permission android:name="android.car.permission.CONTROL_CAR_CLIMATE"/>
<!-- Permission to read navigation state -->
<uses-permission android:name="android.car.permission.CAR_MONITOR_CLUSTER_NAVIGATION_STATE"/>
+ <!-- Permission to read notifications -->
+ <uses-permission android:name="android.permission.ACCESS_NOTIFICATIONS"/>
<!-- To connect to media browser services in other apps, media browser clients
that target Android 11 need to add the following in their manifest -->
@@ -102,6 +104,7 @@
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.HOME"/>
+ <category android:name="android.intent.category.SECONDARY_HOME" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.LAUNCHER_APP"/>
</intent-filter>
diff --git a/app/OWNERS b/app/OWNERS
index ba386b7..6e10365 100644
--- a/app/OWNERS
+++ b/app/OWNERS
@@ -5,9 +5,8 @@
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected] # for TaskView only
[email protected] # for TaskView only
# Recents
per-file src/com/android/car/carlauncher/recents/* = [email protected]
diff --git a/app/car_launcher_flags.aconfig b/app/car_launcher_flags.aconfig
index 27d01b9..e7a16d2 100644
--- a/app/car_launcher_flags.aconfig
+++ b/app/car_launcher_flags.aconfig
@@ -1,4 +1,5 @@
package: "com.android.car.carlauncher"
+container: "system"
flag {
name: "calm_mode"
@@ -6,3 +7,17 @@
description: "This flag controls Calm mode"
bug: "295396130"
}
+
+flag {
+ name: "media_session_card"
+ namespace: "car_sys_exp"
+ description: "This flag controls whether the media card uses media sessions"
+ bug: "323250843"
+}
+
+flag {
+ name: "media_card_fullscreen"
+ namespace: "car_sys_exp"
+ description: "This flag controls Media widget redesign"
+ bug: "310686518"
+}
diff --git a/docklib-util/res/values/strings.xml b/app/res/anim/fade_in.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to app/res/anim/fade_in.xml
index 2c24df7..fe21775 100644
--- a/docklib-util/res/values/strings.xml
+++ b/app/res/anim/fade_in.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
<!--
- ~ Copyright (C) 2023 The Android Open Source Project
+ ~ Copyright (C) 2024 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -15,8 +14,8 @@
~ limitations under the License.
-->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
-</resources>
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@integer/calm_mode_activity_fade_duration"
+ android:interpolator="@android:anim/accelerate_interpolator"
+ android:fromAlpha="0.0"
+ android:toAlpha="1.0" />
diff --git a/docklib-util/res/values/strings.xml b/app/res/anim/fade_out.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to app/res/anim/fade_out.xml
index 2c24df7..3bdf9db 100644
--- a/docklib-util/res/values/strings.xml
+++ b/app/res/anim/fade_out.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
<!--
- ~ Copyright (C) 2023 The Android Open Source Project
+ ~ Copyright (C) 2024 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -15,8 +14,9 @@
~ limitations under the License.
-->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
-</resources>
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:duration="@integer/calm_mode_activity_fade_duration"
+ android:zAdjustment="top"
+ android:fromAlpha="1.0"
+ android:toAlpha="0.0" />
diff --git a/docklib-util/res/values/strings.xml b/app/res/animator/calm_mode_enter.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to app/res/animator/calm_mode_enter.xml
index 2c24df7..afa7e1a 100644
--- a/docklib-util/res/values/strings.xml
+++ b/app/res/animator/calm_mode_enter.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
<!--
- ~ Copyright (C) 2023 The Android Open Source Project
+ ~ Copyright (C) 2024 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -15,8 +14,9 @@
~ limitations under the License.
-->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
-</resources>
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@integer/calm_mode_content_fade_duration"
+ android:propertyName="alpha"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/accelerate_interpolator" />
diff --git a/docklib-util/res/values/strings.xml b/app/res/color/media_card_panel_button_background_tint_state_list.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to app/res/color/media_card_panel_button_background_tint_state_list.xml
index 2c24df7..e75e3bb 100644
--- a/docklib-util/res/values/strings.xml
+++ b/app/res/color/media_card_panel_button_background_tint_state_list.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
<!--
- ~ Copyright (C) 2023 The Android Open Source Project
+ ~ Copyright (C) 2024 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -15,8 +14,9 @@
~ limitations under the License.
-->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
-</resources>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/car_surface_container_highest"
+ android:state_selected="false"/>
+ <item android:color="@color/car_on_surface"
+ android:state_selected="true"/>
+</selector>
diff --git a/docklib-util/res/values/strings.xml b/app/res/color/media_card_panel_button_tint_state_list.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to app/res/color/media_card_panel_button_tint_state_list.xml
index 2c24df7..3cdf6ab 100644
--- a/docklib-util/res/values/strings.xml
+++ b/app/res/color/media_card_panel_button_tint_state_list.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
<!--
- ~ Copyright (C) 2023 The Android Open Source Project
+ ~ Copyright (C) 2024 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -15,8 +14,9 @@
~ limitations under the License.
-->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
-</resources>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/car_surface"
+ android:state_selected="true"/>
+ <item android:color="@color/car_on_surface"
+ android:state_selected="false"/>
+</selector>
diff --git a/docklib-util/res/values/strings.xml b/app/res/color/media_card_seekbar_thumb_color.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to app/res/color/media_card_seekbar_thumb_color.xml
index 2c24df7..f81a676 100644
--- a/docklib-util/res/values/strings.xml
+++ b/app/res/color/media_card_seekbar_thumb_color.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
<!--
- ~ Copyright (C) 2023 The Android Open Source Project
+ ~ Copyright (C) 2024 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -15,8 +14,9 @@
~ limitations under the License.
-->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
-</resources>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@android:color/transparent"
+ android:state_selected="true"/>
+ <item android:color="@color/car_on_surface"
+ android:state_selected="false"/>
+</selector>
diff --git a/docklib-util/res/values/strings.xml b/app/res/drawable/button_ripple.xml
similarity index 61%
copy from docklib-util/res/values/strings.xml
copy to app/res/drawable/button_ripple.xml
index 2c24df7..8950091 100644
--- a/docklib-util/res/values/strings.xml
+++ b/app/res/drawable/button_ripple.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
<!--
- ~ Copyright (C) 2023 The Android Open Source Project
+ ~ Copyright (C) 2024 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -15,8 +14,11 @@
~ limitations under the License.
-->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
-</resources>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/car_ui_ripple_color">
+ <item android:id="@android:id/mask">
+ <shape android:shape="oval">
+ <solid android:color="@color/car_ui_ripple_color"/>
+ </shape>
+ </item>
+</ripple>
\ No newline at end of file
diff --git a/docklib-util/res/values/strings.xml b/app/res/drawable/circle_button_background.xml
similarity index 61%
copy from docklib-util/res/values/strings.xml
copy to app/res/drawable/circle_button_background.xml
index 2c24df7..6422682 100644
--- a/docklib-util/res/values/strings.xml
+++ b/app/res/drawable/circle_button_background.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
<!--
- ~ Copyright (C) 2023 The Android Open Source Project
+ ~ Copyright (C) 2024 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -15,8 +14,12 @@
~ limitations under the License.
-->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
-</resources>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape
+ android:shape="oval">
+ <solid android:color="@color/car_surface_container_highest"/>
+ </shape>
+ </item>
+ <item android:drawable="@drawable/button_ripple"/>
+</layer-list>
diff --git a/docklib-util/res/values/strings.xml b/app/res/drawable/divider.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to app/res/drawable/divider.xml
index 2c24df7..6c48401 100644
--- a/docklib-util/res/values/strings.xml
+++ b/app/res/drawable/divider.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
<!--
- ~ Copyright (C) 2023 The Android Open Source Project
+ ~ Copyright (C) 2024 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -15,8 +14,8 @@
~ limitations under the License.
-->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
-</resources>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/car_on_background"/>
+ <size android:height="0.5dp"/>
+</shape>
\ No newline at end of file
diff --git a/app/res/drawable/empty_action_drawable.xml b/app/res/drawable/empty_action_drawable.xml
new file mode 100644
index 0000000..35bd53b
--- /dev/null
+++ b/app/res/drawable/empty_action_drawable.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2024 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="40dp"
+ android:height="40dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M480,760Q364.33,760 282.17,677.83Q200,595.67 200,480Q200,364.33 282.17,282.17Q364.33,200 480,200Q595.67,200 677.83,282.17Q760,364.33 760,480Q760,595.67 677.83,677.83Q595.67,760 480,760Z"/>
+</vector>
diff --git a/app/res/drawable/ic_history.xml b/app/res/drawable/ic_history.xml
new file mode 100644
index 0000000..9f16dd8
--- /dev/null
+++ b/app/res/drawable/ic_history.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2024 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="40dp"
+ android:height="40dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@color/car_on_surface"
+ android:pathData="M146.67,880Q119.67,880 99.83,860.17Q80,840.33 80,813.33L80,386.67Q80,359.67 99.83,339.83Q119.67,320 146.67,320L813.33,320Q840.33,320 860.17,339.83Q880,359.67 880,386.67L880,813.33Q880,840.33 860.17,860.17Q840.33,880 813.33,880L146.67,880ZM146.67,813.33L813.33,813.33Q813.33,813.33 813.33,813.33Q813.33,813.33 813.33,813.33L813.33,386.67Q813.33,386.67 813.33,386.67Q813.33,386.67 813.33,386.67L146.67,386.67Q146.67,386.67 146.67,386.67Q146.67,386.67 146.67,386.67L146.67,813.33Q146.67,813.33 146.67,813.33Q146.67,813.33 146.67,813.33ZM404.67,752.67L632,600L404.67,448L404.67,752.67ZM152.67,266.67L152.67,200L807.33,200L807.33,266.67L152.67,266.67ZM280,146.67L280,80L680,80L680,146.67L280,146.67ZM146.67,813.33Q146.67,813.33 146.67,813.33Q146.67,813.33 146.67,813.33L146.67,386.67Q146.67,386.67 146.67,386.67Q146.67,386.67 146.67,386.67L146.67,386.67Q146.67,386.67 146.67,386.67Q146.67,386.67 146.67,386.67L146.67,813.33Q146.67,813.33 146.67,813.33Q146.67,813.33 146.67,813.33Z"/>
+</vector>
diff --git a/app/res/drawable/ic_overflow_horizontal.xml b/app/res/drawable/ic_overflow_horizontal.xml
new file mode 100644
index 0000000..9cc19e1
--- /dev/null
+++ b/app/res/drawable/ic_overflow_horizontal.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2024 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="40dp"
+ android:height="40dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@color/car_on_surface"
+ android:pathData="M218.57,538.67Q194.33,538.67 177.17,521.41Q160,504.14 160,479.91Q160,455.67 177.26,438.5Q194.52,421.33 218.76,421.33Q243,421.33 260.17,438.59Q277.33,455.86 277.33,480.09Q277.33,504.33 260.07,521.5Q242.81,538.67 218.57,538.67ZM479.91,538.67Q455.67,538.67 438.5,521.41Q421.33,504.14 421.33,479.91Q421.33,455.67 438.6,438.5Q455.86,421.33 480.09,421.33Q504.33,421.33 521.5,438.59Q538.67,455.86 538.67,480.09Q538.67,504.33 521.41,521.5Q504.14,538.67 479.91,538.67ZM741.24,538.67Q717,538.67 699.83,521.41Q682.67,504.14 682.67,479.91Q682.67,455.67 699.93,438.5Q717.19,421.33 741.43,421.33Q765.67,421.33 782.83,438.59Q800,455.86 800,480.09Q800,504.33 782.74,521.5Q765.48,538.67 741.24,538.67Z"/>
+</vector>
diff --git a/app/res/drawable/ic_play_pause_selector.xml b/app/res/drawable/ic_play_pause_selector.xml
new file mode 100644
index 0000000..142869a
--- /dev/null
+++ b/app/res/drawable/ic_play_pause_selector.xml
@@ -0,0 +1,51 @@
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_selected="true" android:state_enabled="true">
+ <vector
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@color/car_surface"
+ android:pathData="M556.67,760L556.67,200L726.67,200L726.67,760L556.67,760ZM233.33,760L233.33,200L403.33,200L403.33,760L233.33,760Z"/>
+ </vector>
+ </item>
+ <item android:state_selected="false" android:state_enabled="true">
+ <vector
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@color/car_surface"
+ android:pathData="M320,758L320,198L760,478L320,758Z"/>
+ </vector>
+ </item>
+ <item android:state_enabled="false">
+ <vector
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@color/car_surface"
+ android:pathData="M642.67,557.33L328,247.33L328,198L768,478L642.67,557.33ZM792,895.33L528,630.67L328,758L328,430.67L65.33,167.33L112,120.67L840,848.67L792,895.33Z"/>
+ </vector>
+ </item>
+</selector>
diff --git a/app/res/drawable/ic_queue.xml b/app/res/drawable/ic_queue.xml
new file mode 100644
index 0000000..bd7b661
--- /dev/null
+++ b/app/res/drawable/ic_queue.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2024 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="40dp"
+ android:height="40dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@color/car_on_surface"
+ android:pathData="M641.96,800Q593.33,800 559.33,765.96Q525.33,731.92 525.33,683.29Q525.33,634.67 558.78,600.67Q592.22,566.67 640,566.67Q654.31,566.67 667.32,569.17Q680.33,571.67 692,578L692,240L880,240L880,314L758.67,314L758.67,684Q758.67,732.33 724.63,766.17Q690.59,800 641.96,800ZM120,640L120,573.33L430.67,573.33L430.67,640L120,640ZM120,473.33L120,406.67L595.33,406.67L595.33,473.33L120,473.33ZM120,306.67L120,240L595.33,240L595.33,306.67L120,306.67Z"/>
+</vector>
diff --git a/docklib-util/res/values/strings.xml b/app/res/drawable/media_card_button_panel_background.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to app/res/drawable/media_card_button_panel_background.xml
index 2c24df7..8f6cdf3 100644
--- a/docklib-util/res/values/strings.xml
+++ b/app/res/drawable/media_card_button_panel_background.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
<!--
- ~ Copyright (C) 2023 The Android Open Source Project
+ ~ Copyright (C) 2024 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -15,8 +14,9 @@
~ limitations under the License.
-->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
-</resources>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false">
+ <shape android:shape="rectangle"/>
+ </item>
+ <item android:drawable="@drawable/pill_button_shape" android:state_enabled="true"/>
+</selector>
diff --git a/docklib-util/res/values/strings.xml b/app/res/drawable/media_card_panel_button_shape.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to app/res/drawable/media_card_panel_button_shape.xml
index 2c24df7..63dd7b5 100644
--- a/docklib-util/res/values/strings.xml
+++ b/app/res/drawable/media_card_panel_button_shape.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
<!--
- ~ Copyright (C) 2023 The Android Open Source Project
+ ~ Copyright (C) 2024 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -15,8 +14,8 @@
~ limitations under the License.
-->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
-</resources>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="112dp" />
+ <solid android:color="@color/media_card_panel_button_background_tint_state_list"/>
+</shape>
diff --git a/docklib-util/res/values/strings.xml b/app/res/drawable/media_card_panel_handlebar.xml
similarity index 60%
copy from docklib-util/res/values/strings.xml
copy to app/res/drawable/media_card_panel_handlebar.xml
index 2c24df7..e98db81 100644
--- a/docklib-util/res/values/strings.xml
+++ b/app/res/drawable/media_card_panel_handlebar.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
<!--
- ~ Copyright (C) 2023 The Android Open Source Project
+ ~ Copyright (C) 2024 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -15,8 +14,13 @@
~ limitations under the License.
-->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
-</resources>
+<ripple
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/car_ui_ripple_color">
+ <item>
+ <shape android:shape="rectangle">
+ <corners android:radius="16dp"/>
+ <solid android:color="@color/car_on_surface_variant"/>
+ </shape>
+ </item>
+</ripple>
diff --git a/app/res/drawable/media_card_seekbar_progress.xml b/app/res/drawable/media_card_seekbar_progress.xml
new file mode 100644
index 0000000..f2b5d52
--- /dev/null
+++ b/app/res/drawable/media_card_seekbar_progress.xml
@@ -0,0 +1,33 @@
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:id="@android:id/background">
+ <shape android:shape="line">
+ <stroke android:width="4dp" />
+ </shape>
+ </item>
+
+ <item android:id="@android:id/progress">
+ <clip>
+ <shape android:shape="line">
+ <stroke android:width="4dp" />
+ </shape>
+ </clip>
+ </item>
+
+</layer-list>
diff --git a/app/res/drawable/media_card_seekbar_thumb.xml b/app/res/drawable/media_card_seekbar_thumb.xml
new file mode 100644
index 0000000..b9dee23
--- /dev/null
+++ b/app/res/drawable/media_card_seekbar_thumb.xml
@@ -0,0 +1,65 @@
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_selected="true">
+ <vector
+ android:width="8dp"
+ android:height="32dp"
+ android:viewportWidth="12"
+ android:viewportHeight="32">
+ <group>
+ <clip-path
+ android:pathData="M6 4C7.10457 4 8 4.89543 8 6V26C8 27.1046 7.10457 28 6 28C4.89543 28 4 27.1046 4 26V6C4 4.89543 4.89543 4 6 4Z" />
+ <path
+ android:pathData="M4 4V28H8V4"
+ android:fillColor="@android:color/transparent" />
+ </group>
+ <group>
+ <clip-path
+ android:pathData="M0 0V32H12V0M6 4C7.10457 4 8 4.89543 8 6V26C8 27.1046 7.10457 28 6 28C4.89543 28 4 27.1046 4 26V6C4 4.89543 4.89543 4 6 4Z" />
+ <path
+ android:pathData="M6 4C7.10457 4 8 4.89543 8 6V26C8 27.1046 7.10457 28 6 28C4.89543 28 4 27.1046 4 26V6C4 4.89543 4.89543 4 6 4Z"
+ android:strokeWidth="4"
+ android:strokeColor="@android:color/transparent" />
+ </group>
+ </vector>
+
+ </item>
+ <item android:state_selected="false">
+ <vector
+ android:width="12dp"
+ android:height="32dp"
+ android:viewportWidth="12"
+ android:viewportHeight="32">
+ <group>
+ <clip-path
+ android:pathData="M6 4C7.10457 4 8 4.89543 8 6V26C8 27.1046 7.10457 28 6 28C4.89543 28 4 27.1046 4 26V6C4 4.89543 4.89543 4 6 4Z" />
+ <path
+ android:pathData="M4 4V28H8V4"
+ android:fillColor="@color/car_on_surface" />
+ </group>
+ <group>
+ <clip-path
+ android:pathData="M0 0V32H12V0M6 4C7.10457 4 8 4.89543 8 6V26C8 27.1046 7.10457 28 6 28C4.89543 28 4 27.1046 4 26V6C4 4.89543 4.89543 4 6 4Z" />
+ <path
+ android:pathData="M6 4C7.10457 4 8 4.89543 8 6V26C8 27.1046 7.10457 28 6 28C4.89543 28 4 27.1046 4 26V6C4 4.89543 4.89543 4 6 4Z"
+ android:strokeWidth="4"
+ android:strokeColor="@android:color/transparent" />
+ </group>
+ </vector>
+ </item>
+</selector>
diff --git a/app/res/drawable/pill_button_shape.xml b/app/res/drawable/pill_button_shape.xml
new file mode 100644
index 0000000..69342ad
--- /dev/null
+++ b/app/res/drawable/pill_button_shape.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape
+ android:shape="rectangle">
+ <corners android:radius="@dimen/media_card_pill_radius" />
+ <solid android:color="@color/car_surface_container_highest" />
+ </shape>
+ </item>
+ <item android:drawable="@drawable/button_ripple"/>
+</layer-list>
diff --git a/docklib-util/res/values/strings.xml b/app/res/drawable/radius_16_background.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to app/res/drawable/radius_16_background.xml
index 2c24df7..df0bc57 100644
--- a/docklib-util/res/values/strings.xml
+++ b/app/res/drawable/radius_16_background.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
<!--
- ~ Copyright (C) 2023 The Android Open Source Project
+ ~ Copyright (C) 2024 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -15,8 +14,7 @@
~ limitations under the License.
-->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
-</resources>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="16dp"/>
+</shape>
diff --git a/docklib-util/res/values/strings.xml b/app/res/drawable/radius_8_background.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to app/res/drawable/radius_8_background.xml
index 2c24df7..3ade4bb 100644
--- a/docklib-util/res/values/strings.xml
+++ b/app/res/drawable/radius_8_background.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
<!--
- ~ Copyright (C) 2023 The Android Open Source Project
+ ~ Copyright (C) 2024 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -15,8 +14,7 @@
~ limitations under the License.
-->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
-</resources>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="8dp"/>
+</shape>
diff --git a/app/res/layout/calm_mode_date_and_temperature.xml b/app/res/layout/calm_mode_date_and_temperature.xml
deleted file mode 100644
index 66afd0a..0000000
--- a/app/res/layout/calm_mode_date_and_temperature.xml
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2023 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_height="wrap_content"
- android:layout_width="wrap_content">
- <TextClock
- android:id="@+id/date"
- style="@style/CalmMode.Text.Date"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:visibility="gone"
- android:layout_marginTop="@dimen/calm_mode_padding"
- android:layout_marginEnd="@dimen/calm_mode_padding"
- app:layout_constraintEnd_toStartOf="@id/temperature_icon"
- app:layout_constraintTop_toTopOf="parent" />
-
- <androidx.constraintlayout.widget.Group
- android:id="@+id/temperature_group"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:visibility="gone"
- app:constraint_referenced_ids="temperature_icon,temperature"/>
-
- <ImageView
- android:id="@+id/temperature_icon"
- style="@style/CalmMode.Icon.Temperature"
- android:layout_marginEnd="@dimen/calm_mode_padding"
- android:src="@drawable/ic_temperature"
- app:layout_constraintEnd_toStartOf="@id/temperature"
- app:layout_constraintTop_toTopOf="@id/temperature"
- app:layout_constraintBottom_toBottomOf="@id/temperature" />
-
- <TextView
- android:id="@+id/temperature"
- style="@style/CalmMode.Text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/calm_mode_padding"
- android:layout_marginEnd="@dimen/calm_mode_padding"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent"/>
-</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/res/layout/calm_mode_fragment.xml b/app/res/layout/calm_mode_fragment.xml
index bdb8095..c1d1502 100644
--- a/app/res/layout/calm_mode_fragment.xml
+++ b/app/res/layout/calm_mode_fragment.xml
@@ -22,7 +22,7 @@
android:layout_height="match_parent">
<ImageView
- style="@style/CalmMode.BackgroundImage"
+ style="@style/CalmModeBackgroundImage"
android:layout_width="match_parent"
android:layout_height="match_parent" />
@@ -30,88 +30,85 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
- <androidx.constraintlayout.widget.Group
- android:id="@+id/media_group"
+ <TextClock
+ android:id="@+id/date"
+ style="@style/CalmModeText.Date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/calm_mode_padding_vertical"
android:visibility="gone"
- app:constraint_referenced_ids="media_icon,media_title" />
-
- <ImageView
- android:id="@+id/media_icon"
- style="@style/CalmMode.Icon"
- android:scaleType="fitCenter"
- android:src="@drawable/ic_media"
- android:layout_marginTop="@dimen/calm_mode_padding"
- android:layout_marginStart="@dimen/calm_mode_padding"
- app:layout_constraintBottom_toTopOf="@id/barrier"
+ app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
+ app:layout_constraintEnd_toStartOf="@id/temperature_icon"
+ app:layout_constraintHorizontal_chainStyle="packed" />
<TextView
- android:id="@+id/media_title"
- style="@style/CalmMode.Text.MediaTitle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/calm_mode_padding"
- android:layout_marginStart="@dimen/calm_mode_icon_margin"
- app:layout_constraintBottom_toTopOf="@id/barrier"
- app:layout_constraintStart_toEndOf="@id/media_icon"
- app:layout_constraintTop_toTopOf="parent"/>
-
- <androidx.constraintlayout.widget.Group
- android:id="@+id/nav_group"
+ android:id="@+id/temperature_icon"
+ style="@style/CalmModeText.Temperature"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
- app:constraint_referenced_ids="nav_state_icon,nav_state" />
+ app:layout_constraintBottom_toBottomOf="@id/temperature"
+ app:layout_constraintStart_toEndOf="@id/date"
+ app:layout_constraintEnd_toStartOf="@id/temperature"
+ android:text="@string/calm_mode_separator" />
+
+ <TextView
+ android:id="@+id/temperature"
+ style="@style/CalmModeText.Temperature"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:layout_marginTop="@dimen/calm_mode_padding_vertical"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toEndOf="@id/temperature_icon"
+ app:layout_constraintEnd_toStartOf="@id/nav_state_icon" />
<ImageView
android:id="@+id/nav_state_icon"
- style="@style/CalmMode.Icon"
+ style="@style/CalmModeIcon"
+ android:layout_width="@dimen/calm_mode_icon_size_regular"
+ android:layout_height="@dimen/calm_mode_icon_size_regular"
+ android:visibility="gone"
+ android:layout_marginStart="@dimen/calm_mode_icon_margin"
android:layout_marginEnd="@dimen/calm_mode_icon_margin"
android:src="@drawable/ic_navigation"
- android:layout_marginTop="@dimen/calm_mode_padding"
- app:layout_constraintBottom_toTopOf="@id/barrier"
+ app:layout_constraintTop_toTopOf="@id/nav_state"
+ app:layout_constraintBottom_toBottomOf="@id/nav_state"
app:layout_constraintEnd_toStartOf="@id/nav_state"
- app:layout_constraintTop_toTopOf="parent" />
+ app:layout_constraintStart_toEndOf="@id/temperature" />
<TextView
android:id="@+id/nav_state"
- style="@style/CalmMode.Text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/calm_mode_padding"
- android:layout_marginStart="@dimen/calm_mode_icon_margin"
- app:layout_constraintBottom_toTopOf="@id/barrier"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"/>
-
- <include
- android:id="@+id/date_and_temperature_container"
- layout="@layout/calm_mode_date_and_temperature"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
-
- <androidx.constraintlayout.widget.Barrier
- android:id="@+id/barrier"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:barrierAllowsGoneWidgets="true"
- app:barrierDirection="bottom"
- app:constraint_referenced_ids="media_icon, media_title, nav_icon, nav_state, date_and_temperature_container" />
-
- <TextClock
- android:id="@+id/clock"
- style="@style/CalmMode.Text.Clock"
+ style="@style/CalmModeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
+ android:layout_marginTop="@dimen/calm_mode_padding_vertical"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toEndOf="@id/nav_state_icon"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+ <TextClock
+ android:id="@+id/clock"
+ style="@style/CalmModeClock"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ app:layout_constraintTop_toBottomOf="@id/date"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/barrier"/>
+ app:layout_constraintEnd_toEndOf="parent" />
+
+ <TextView
+ android:id="@+id/media_title"
+ style="@style/CalmModeText.MediaTitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:layout_marginTop="@dimen/calm_mode_clock_media_title_margin"
+ app:layout_constraintTop_toBottomOf="@id/clock"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/res/layout/card_fragment_audio_card.xml b/app/res/layout/card_fragment_audio_card.xml
new file mode 100644
index 0000000..bea311f
--- /dev/null
+++ b/app/res/layout/card_fragment_audio_card.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 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="match_parent"
+ android:layout_height="match_parent">
+
+ <FrameLayout
+ android:id="@+id/in_call_fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <FrameLayout
+ android:id="@+id/media_fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/app/res/layout/media_card_fullscreen.xml b/app/res/layout/media_card_fullscreen.xml
new file mode 100644
index 0000000..10ad76e
--- /dev/null
+++ b/app/res/layout/media_card_fullscreen.xml
@@ -0,0 +1,262 @@
+<!--
+ ~ Copyright (C) 2024 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.cardview.widget.CardView
+ 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"
+ app:cardBackgroundColor="@color/car_surface_container_high"
+ app:cardCornerRadius="@dimen/media_card_card_radius"
+ app:cardElevation="0dp">
+
+ <androidx.constraintlayout.motion.widget.MotionLayout
+ android:id="@+id/motion_layout"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ app:layoutDescription="@xml/panel_animation_motion_scene">
+
+ <LinearLayout
+ android:id="@+id/media_card_panel_content_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="@dimen/media_card_panel_content_margin_top"
+ android:background="@color/car_surface_container_highest"
+ android:orientation="vertical"
+ app:layout_constraintBottom_toBottomOf="parent">
+ <FrameLayout
+ android:id="@+id/media_card_panel_handlebar"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/media_card_panel_handlebar_touch_target_height"
+ android:paddingStart="@dimen/media_card_panel_handlebar_horizontal_padding"
+ android:paddingEnd="@dimen/media_card_panel_handlebar_horizontal_padding"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent">
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/media_card_panel_handlebar_height"
+ android:layout_gravity="center"
+ android:background="@drawable/media_card_panel_handlebar" />
+ </FrameLayout>
+
+ <androidx.viewpager2.widget.ViewPager2
+ android:id="@+id/view_pager"
+ android:background="@color/car_surface_container_highest"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/empty_panel"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/car_surface_container_high"
+ app:layout_constraintTop_toTopOf="parent"/>
+
+ <ImageView
+ android:id="@+id/album_art"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/media_card_album_art_size"
+ android:scaleType="fitCenter"
+ android:adjustViewBounds="true"
+ android:background="@android:color/transparent"
+ android:layout_marginStart="@dimen/media_card_horizontal_margin"
+ android:layout_marginEnd="@dimen/media_card_album_art_end_margin"
+ android:layout_marginTop="@dimen/media_card_horizontal_margin"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0"
+ app:layout_constrainedWidth="true"/>
+
+ <ImageView
+ android:id="@+id/media_widget_app_icon"
+ android:layout_width="@dimen/media_card_app_icon_size"
+ android:layout_height="@dimen/media_card_app_icon_size"
+ android:clipToOutline="true"
+ android:layout_marginEnd="@dimen/media_card_horizontal_margin"
+ android:layout_marginTop="@dimen/media_card_horizontal_margin"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/guideline"
+ app:layout_constraintGuide_begin="@dimen/media_card_text_view_guideline_start"
+ android:orientation="horizontal"/>
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:text="@string/metadata_default_title"
+ android:textColor="@color/car_text_primary"
+ android:gravity="center_vertical"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:layout_marginTop="@dimen/media_card_view_separation_margin"
+ android:layout_marginHorizontal="@dimen/media_card_horizontal_margin"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/guideline"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintVertical_bias="0"/>
+
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ style="@style/TextAppearance.Car.Body.Small"
+ android:textColor="@color/car_text_secondary"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:layout_marginTop="@dimen/media_card_artist_top_margin"
+ android:layout_marginHorizontal="@dimen/media_card_horizontal_margin"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/title" />
+
+ <SeekBar
+ android:id="@+id/playback_seek_bar"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:paddingEnd="0dp"
+ android:paddingStart="0dp"
+ android:progressBackgroundTint="@color/car_surface_container_highest"
+ android:progressDrawable="@drawable/media_card_seekbar_progress"
+ android:progressTint="@color/car_primary"
+ android:splitTrack="true"
+ android:thumb="@drawable/media_card_seekbar_thumb"
+ android:thumbTint="@color/car_on_surface"
+ android:thumbOffset="0px"
+ android:layout_marginTop="@dimen/media_card_view_separation_margin"
+ android:layout_marginStart="@dimen/media_card_horizontal_margin"
+ android:layout_marginEnd="@dimen/media_card_view_separation_margin"
+ android:clickable="true"
+ android:focusable="true"
+ app:layout_goneMarginEnd="@dimen/media_card_horizontal_margin"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/content_format"
+ app:layout_constraintTop_toBottomOf="@id/subtitle" />
+
+ <com.android.car.media.common.ContentFormatView
+ android:id="@+id/content_format"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/media_card_logo_size"
+ android:layout_gravity="center_vertical"
+ android:adjustViewBounds="true"
+ android:scaleType="fitStart"
+ app:logoTint="@color/car_on_surface_variant"
+ app:logoSize="small"
+ android:layout_marginEnd="@dimen/media_card_horizontal_margin"
+ app:layout_constraintStart_toEndOf="@id/playback_seek_bar"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="@id/playback_seek_bar"
+ app:layout_constraintBottom_toBottomOf="@id/playback_seek_bar" />
+
+ <ImageButton
+ android:id="@+id/play_pause_button"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/media_card_large_button_size"
+ android:src="@drawable/ic_play_pause_selector"
+ android:scaleType="center"
+ android:tint="@color/car_surface_container_high"
+ android:background="@drawable/pill_button_shape"
+ android:backgroundTint="@color/car_primary"
+ android:layout_marginBottom="@dimen/media_card_play_button_bottom_margin"
+ app:layout_goneMarginEnd="@dimen/media_card_horizontal_margin"
+ app:layout_goneMarginStart="@dimen/media_card_horizontal_margin"
+ app:layout_constraintStart_toEndOf="@id/playback_action_id1"
+ app:layout_constraintEnd_toStartOf="@id/playback_action_id2"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintVertical_bias="1" />
+
+ <ImageButton
+ android:id="@+id/playback_action_id1"
+ android:layout_width="@dimen/media_card_large_button_size"
+ android:layout_height="@dimen/media_card_large_button_size"
+ android:scaleType="fitCenter"
+ android:padding="@dimen/media_card_large_button_icon_padding"
+ android:cropToPadding="true"
+ android:tint="@color/car_on_surface_variant"
+ android:background="@drawable/circle_button_background"
+ android:layout_marginStart="@dimen/media_card_horizontal_margin"
+ android:layout_marginEnd="@dimen/media_card_play_button_horizontal_margin"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/play_pause_button"
+ app:layout_constraintTop_toTopOf="@id/play_pause_button"
+ app:layout_constraintBottom_toBottomOf="@id/play_pause_button"
+ app:layout_constraintHorizontal_bias="0"/>
+
+ <ImageButton
+ android:id="@+id/playback_action_id2"
+ android:layout_width="@dimen/media_card_large_button_size"
+ android:layout_height="@dimen/media_card_large_button_size"
+ android:scaleType="fitCenter"
+ android:padding="@dimen/media_card_large_button_icon_padding"
+ android:cropToPadding="true"
+ android:tint="@color/car_on_surface_variant"
+ android:background="@drawable/circle_button_background"
+ android:layout_marginEnd="@dimen/media_card_horizontal_margin"
+ android:layout_marginStart="@dimen/media_card_play_button_horizontal_margin"
+ app:layout_constraintStart_toEndOf="@id/play_pause_button"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="@id/play_pause_button"
+ app:layout_constraintBottom_toBottomOf="@id/play_pause_button"
+ app:layout_constraintHorizontal_bias="1" />
+
+ <LinearLayout
+ android:id="@+id/button_panel_background"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/media_card_bottom_panel_height"
+ android:background="@drawable/media_card_button_panel_background"
+ android:backgroundTint="@color/car_surface_container_highest"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintVertical_bias="1">
+ <ImageButton
+ android:id="@+id/overflow_button"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/media_card_bottom_panel_button_size"
+ android:layout_gravity="center"
+ android:src="@drawable/ic_overflow_horizontal"
+ android:layout_weight="1"
+ style="@style/MediaCardPanelButtonStyle"/>
+ <ImageButton
+ android:id="@+id/queue_button"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/media_card_bottom_panel_button_size"
+ android:layout_gravity="center"
+ android:src="@drawable/ic_queue"
+ android:layout_weight="1"
+ style="@style/MediaCardPanelButtonStyle"/>
+ <ImageButton
+ android:id="@+id/history_button"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/media_card_bottom_panel_button_size"
+ android:layout_gravity="center"
+ android:src="@drawable/ic_history"
+ android:layout_weight="1"
+ style="@style/MediaCardPanelButtonStyle"/>
+ </LinearLayout>
+ </androidx.constraintlayout.motion.widget.MotionLayout>
+</androidx.cardview.widget.CardView>
diff --git a/app/res/layout/media_card_history_header_item.xml b/app/res/layout/media_card_history_header_item.xml
new file mode 100644
index 0000000..5b073e3
--- /dev/null
+++ b/app/res/layout/media_card_history_header_item.xml
@@ -0,0 +1,38 @@
+<!--
+ ~ Copyright (C) 2024 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_height="wrap_content"
+ android:layout_width="match_parent"
+ android:paddingBottom="@dimen/media_card_view_separation_margin">
+
+ <ImageView
+ android:id="@+id/history_card_header_icon"
+ android:layout_width="@dimen/media_card_view_header_icon_size"
+ android:layout_height="@dimen/media_card_view_header_icon_size"
+ android:layout_marginStart="@dimen/media_card_horizontal_margin"
+ android:layout_marginEnd="@dimen/media_card_view_separation_margin"
+ android:src="@drawable/ic_history"/>
+
+ <TextView
+ android:id="@+id/history_card_history_header_title_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:includeFontPadding="false"
+ android:text="@string/media_card_history_header_title"
+ android:textAppearance="@style/TextAppearance.Car.Body.Small"
+ android:textColor="@color/car_text_primary"/>
+</LinearLayout>
diff --git a/app/res/layout/media_card_history_item.xml b/app/res/layout/media_card_history_item.xml
new file mode 100644
index 0000000..eeb9bb4
--- /dev/null
+++ b/app/res/layout/media_card_history_item.xml
@@ -0,0 +1,109 @@
+
+<!--
+ ~ Copyright (C) 2024 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.cardview.widget.CardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_height="@dimen/media_card_history_item_height"
+ android:layout_width="match_parent"
+ android:foreground="?android:attr/selectableItemBackground"
+ app:cardBackgroundColor="@android:color/transparent"
+ app:contentPaddingLeft="@dimen/media_card_horizontal_margin"
+ app:contentPaddingRight="@dimen/media_card_horizontal_margin">
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/history_card_container_active"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent">
+ <TextView
+ android:id="@+id/history_card_title_active"
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:textAppearance="@style/TextAppearance.Car.Body.Small"
+ android:textColor="@color/car_text_primary"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:layout_marginEnd="@dimen/media_card_view_separation_margin"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/history_card_album_art"
+ app:layout_constraintTop_toTopOf="@id/history_card_album_art"
+ app:layout_constraintBottom_toTopOf="@id/history_card_subtitle_active"
+ app:layout_constraintHorizontal_bias="0"/>
+ <TextView
+ android:id="@+id/history_card_subtitle_active"
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:textAppearance="@style/TextAppearance.Car.Body.Small"
+ android:textColor="@color/car_text_secondary"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:layout_marginStart="@dimen/media_card_view_separation_margin"
+ android:layout_marginEnd="@dimen/media_card_view_separation_margin"
+ app:layout_constraintStart_toEndOf="@id/history_card_app_thumbnail"
+ app:layout_constraintEnd_toStartOf="@id/history_card_album_art"
+ app:layout_constraintTop_toBottomOf="@id/history_card_title_active"
+ app:layout_constraintBottom_toBottomOf="@id/history_card_album_art"
+ app:layout_constraintHorizontal_bias="0"/>
+ <ImageView
+ android:id="@+id/history_card_album_art"
+ android:layout_width="@dimen/media_card_history_item_icon_size"
+ android:layout_height="@dimen/media_card_history_item_icon_size"
+ android:background="@drawable/radius_8_background"
+ android:clipToOutline="true"
+ android:scaleType="centerCrop"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+ <ImageView
+ android:id="@+id/history_card_app_thumbnail"
+ android:layout_width="@dimen/media_card_history_item_thumbnail_size"
+ android:layout_height="@dimen/media_card_history_item_thumbnail_size"
+ android:background="@drawable/radius_16_background"
+ android:clipToOutline="true"
+ android:scaleType="centerCrop"
+ app:layout_constraintTop_toTopOf="@id/history_card_subtitle_active"
+ app:layout_constraintBottom_toBottomOf="@id/history_card_subtitle_active"
+ app:layout_constraintStart_toStartOf="parent"/>
+ </androidx.constraintlayout.widget.ConstraintLayout>
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/history_card_container_inactive"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent">
+ <TextView
+ android:id="@+id/history_card_app_title_inactive"
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:textAppearance="@style/TextAppearance.Car.Body.Small"
+ android:textColor="@color/car_text_primary"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:layout_marginEnd="@dimen/media_card_view_separation_margin"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/history_item_app_icon_inactive"
+ app:layout_constraintTop_toTopOf="@id/history_item_app_icon_inactive"
+ app:layout_constraintBottom_toBottomOf="@id/history_item_app_icon_inactive"
+ app:layout_constraintHorizontal_bias="0"/>
+ <ImageView
+ android:id="@+id/history_item_app_icon_inactive"
+ android:layout_width="@dimen/media_card_history_item_icon_size"
+ android:layout_height="@dimen/media_card_history_item_icon_size"
+ android:background="@drawable/radius_16_background"
+ android:clipToOutline="true"
+ android:scaleType="centerCrop"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</androidx.cardview.widget.CardView>
diff --git a/app/res/layout/media_card_panel_content_item.xml b/app/res/layout/media_card_panel_content_item.xml
new file mode 100644
index 0000000..1c11c1b
--- /dev/null
+++ b/app/res/layout/media_card_panel_content_item.xml
@@ -0,0 +1,120 @@
+<!--
+ ~ Copyright (C) 2024 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"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <TableLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone"
+ android:id="@+id/overflow_grid"
+ android:stretchColumns="0,1"
+ android:background="@color/car_surface_container_highest">
+ <TableRow
+ android:layout_weight="1"
+ android:gravity="center">
+ <ImageButton
+ android:id="@+id/playback_action_id3"
+ android:layout_width="@dimen/media_card_small_button_size"
+ android:layout_height="@dimen/media_card_small_button_size"
+ style="@style/MediaCardCustomActionButtonStyle" />
+ <ImageButton
+ android:id="@+id/playback_action_id4"
+ android:layout_width="@dimen/media_card_small_button_size"
+ android:layout_height="@dimen/media_card_small_button_size"
+ style="@style/MediaCardCustomActionButtonStyle" />
+ </TableRow>
+
+ <TableRow
+ android:layout_weight="1"
+ android:gravity="center">
+ <ImageButton
+ android:id="@+id/playback_action_id5"
+ android:layout_width="@dimen/media_card_small_button_size"
+ android:layout_height="@dimen/media_card_small_button_size"
+ style="@style/MediaCardCustomActionButtonStyle" />
+ <ImageButton
+ android:id="@+id/playback_action_id6"
+ android:layout_width="@dimen/media_card_small_button_size"
+ android:layout_height="@dimen/media_card_small_button_size"
+ style="@style/MediaCardCustomActionButtonStyle" />
+ </TableRow>
+
+ <TableRow
+ android:layout_weight="1"
+ android:gravity="center">
+ <ImageButton
+ android:id="@+id/playback_action_id7"
+ android:layout_width="@dimen/media_card_small_button_size"
+ android:layout_height="@dimen/media_card_small_button_size"
+ style="@style/MediaCardCustomActionButtonStyle"/>
+ <ImageButton
+ android:id="@+id/playback_action_id8"
+ android:layout_width="@dimen/media_card_small_button_size"
+ android:layout_height="@dimen/media_card_small_button_size"
+ style="@style/MediaCardCustomActionButtonStyle" />
+ </TableRow>
+
+ <TableRow
+ android:layout_weight="1"
+ android:gravity="center">
+ <ImageButton
+ android:id="@+id/playback_action_id9"
+ android:layout_width="@dimen/media_card_small_button_size"
+ android:layout_height="@dimen/media_card_small_button_size"
+ style="@style/MediaCardCustomActionButtonStyle"/>
+ <ImageButton
+ android:id="@+id/playback_action_id10"
+ android:layout_width="@dimen/media_card_small_button_size"
+ android:layout_height="@dimen/media_card_small_button_size"
+ style="@style/MediaCardCustomActionButtonStyle"/>
+ </TableRow>
+ </TableLayout>
+
+ <FrameLayout
+ android:id="@+id/queue_list_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingStart="@dimen/media_card_horizontal_margin"
+ android:paddingEnd="@dimen/media_card_horizontal_margin"
+ android:background="@color/car_surface_container_highest"
+ android:visibility="gone">
+ <com.android.car.apps.common.CarUiRecyclerViewNoScrollbar
+ android:id="@+id/queue_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:enableDivider="true"
+ android:requiresFadingEdge="vertical"
+ android:fadingEdgeLength="@dimen/media_card_recycler_view_fading_edge_length"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:id="@+id/history_list_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/car_surface_container_highest"
+ android:visibility="gone">
+ <com.android.car.apps.common.CarUiRecyclerViewNoScrollbar
+ android:id="@+id/history_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:enableDivider="true"
+ android:requiresFadingEdge="vertical"
+ android:fadingEdgeLength="@dimen/media_card_recycler_view_fading_edge_length"/>
+ </FrameLayout>
+</FrameLayout>
diff --git a/app/res/layout/media_card_queue_header_item.xml b/app/res/layout/media_card_queue_header_item.xml
new file mode 100644
index 0000000..e7b1111
--- /dev/null
+++ b/app/res/layout/media_card_queue_header_item.xml
@@ -0,0 +1,36 @@
+<!--
+ ~ Copyright (C) 2024 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_height="wrap_content"
+ android:layout_width="match_parent"
+ android:paddingBottom="@dimen/media_card_view_separation_margin">
+
+ <ImageView
+ android:id="@+id/header_app_icon"
+ android:layout_width="@dimen/media_card_queue_header_app_icon_size"
+ android:layout_height="@dimen/media_card_queue_header_app_icon_size"
+ android:layout_marginEnd="@dimen/media_card_view_separation_margin" />
+
+ <TextView
+ android:id="@+id/media_card_queue_header_title_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:includeFontPadding="false"
+ android:text="@string/media_card_queue_header_title"
+ android:textAppearance="@style/TextAppearance.Car.Body.Small"
+ android:textColor="@color/car_text_primary"/>
+</LinearLayout>
diff --git a/app/res/layout/media_card_queue_item.xml b/app/res/layout/media_card_queue_item.xml
new file mode 100644
index 0000000..966bb2e
--- /dev/null
+++ b/app/res/layout/media_card_queue_item.xml
@@ -0,0 +1,85 @@
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<!--
+ ~ CopyEnd (C) 2024 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_height="wrap_content"
+ android:layout_width="match_parent"
+ android:paddingTop="@dimen/media_card_view_separation_margin"
+ android:paddingBottom="@dimen/media_card_view_separation_margin">
+
+ <TextView
+ android:id="@+id/queue_list_item_title"
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:textAppearance="@style/TextAppearance.Car.Body.Small"
+ android:textColor="@color/car_text_primary"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:layout_marginEnd="@dimen/media_card_view_separation_margin"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/thumbnail"
+ app:layout_constraintTop_toTopOf="@id/thumbnail"
+ app:layout_constraintBottom_toBottomOf="@id/thumbnail"
+ app:layout_constraintVertical_bias="0"
+ app:layout_constraintHorizontal_bias="0"/>
+
+ <TextView
+ android:id="@+id/queue_list_item_subtitle"
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:textAppearance="@style/TextAppearance.Car.Body.Small"
+ android:textColor="@color/car_text_secondary"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:layout_marginEnd="@dimen/media_card_view_separation_margin"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/thumbnail"
+ app:layout_constraintTop_toTopOf="@id/thumbnail"
+ app:layout_constraintBottom_toBottomOf="@id/thumbnail"
+ app:layout_constraintVertical_bias="1"
+ app:layout_constraintHorizontal_bias="0"/>
+
+ <ImageView
+ android:id="@+id/thumbnail"
+ android:layout_width="@dimen/media_card_queue_item_thumbnail_size"
+ android:layout_height="@dimen/media_card_queue_item_thumbnail_size"
+ android:background="@drawable/radius_8_background"
+ android:clipToOutline="true"
+ android:scaleType="centerCrop"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@id/queue_list_item_title"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="1" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/res/values-af/strings.xml b/app/res/values-af/strings.xml
index 026c73c..b9deb8d 100644
--- a/app/res/values-af/strings.xml
+++ b/app/res/values-af/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Vee alles uit"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"App is nie beskikbaar nie"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Kalmmodus"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Waglys"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Mediabron"</string>
</resources>
diff --git a/app/res/values-am/strings.xml b/app/res/values-am/strings.xml
index 6b1b6c1..d7a0acd 100644
--- a/app/res/values-am/strings.xml
+++ b/app/res/values-am/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"ሁሉንም አጽዳ"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"መተግበሪያ አይገኝም"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"የእርጋታ ሁነታ"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"ሰልፍ"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"የሚዲያ ምንጭ"</string>
</resources>
diff --git a/app/res/values-ar/strings.xml b/app/res/values-ar/strings.xml
index 345de0d..bd1730a 100644
--- a/app/res/values-ar/strings.xml
+++ b/app/res/values-ar/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"محو الكل"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"التطبيق غير متاح."</string>
<string name="calm_mode_title" msgid="4364804976931157567">"وضع الهدوء"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"قائمة المحتوى التالي"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"مصدر الوسائط"</string>
</resources>
diff --git a/app/res/values-as/strings.xml b/app/res/values-as/strings.xml
index 23500ec..0edbaba 100644
--- a/app/res/values-as/strings.xml
+++ b/app/res/values-as/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"আটাইবোৰ মচক"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"এপ্টো উপলব্ধ নহয়"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"শান্ত ম’ড"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"শাৰী"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"মিডিয়াৰ উৎস"</string>
</resources>
diff --git a/app/res/values-az/strings.xml b/app/res/values-az/strings.xml
index 130ebd6..111c8f6 100644
--- a/app/res/values-az/strings.xml
+++ b/app/res/values-az/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Hamısını silin"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Tətbiq əlçatan deyil"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Sakit rejim"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Növbə"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Media mənbəyi"</string>
</resources>
diff --git a/app/res/values-b+sr+Latn/strings.xml b/app/res/values-b+sr+Latn/strings.xml
index 2061112..35e35e4 100644
--- a/app/res/values-b+sr+Latn/strings.xml
+++ b/app/res/values-b+sr+Latn/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Obriši sve"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Aplikacija nije dostupna"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Režim opuštanja"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Redosled"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Izvor medija"</string>
</resources>
diff --git a/app/res/values-be/strings.xml b/app/res/values-be/strings.xml
index 2e6f4da..d4dedb0 100644
--- a/app/res/values-be/strings.xml
+++ b/app/res/values-be/strings.xml
@@ -27,12 +27,14 @@
<string name="projected_launch_text" msgid="5034079820478748609">"Запусціць Android Auto"</string>
<string name="projected_onclick_launch_error_toast_text" msgid="8853804785626030351">"Не ўдалося запусціць Android Auto. Дзеянні не знойдзены."</string>
<string name="projection_devices" msgid="2556503818120676439">"{count,plural, =1{# прылада}one{# прылада}few{# прылады}many{# прылад}other{# прылады}}"</string>
- <string name="weather_app_name" msgid="4356705068077942048">"Надвор\'е"</string>
+ <string name="weather_app_name" msgid="4356705068077942048">"Надвор’е"</string>
<string name="fake_weather_main_text" msgid="2545755284647327839">"--°, пераважна сонечна"</string>
- <string name="fake_weather_footer_text" msgid="8640814250285014485">"Маўтын-В\'ю • макс.: --°, мін.: --°"</string>
+ <string name="fake_weather_footer_text" msgid="8640814250285014485">"Маўтын-В’ю • макс.: --°, мін.: --°"</string>
<string name="times_separator" msgid="1962841895013564645">"/"</string>
<string name="recents_empty_state_text" msgid="8228569970506899117">"Няма нядаўніх элементаў"</string>
<string name="recents_clear_all_text" msgid="3594272268167720553">"Ачысціць усё"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Праграма недаступная"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Рэжым спакою"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Чарга"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Крыніца мультымедыя"</string>
</resources>
diff --git a/app/res/values-bg/strings.xml b/app/res/values-bg/strings.xml
index 8425793..37e0d59 100644
--- a/app/res/values-bg/strings.xml
+++ b/app/res/values-bg/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Изчистване на всичко"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Приложението не е налично"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Режим на покой"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Опашка"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Източник на мултимедията"</string>
</resources>
diff --git a/app/res/values-bn/strings.xml b/app/res/values-bn/strings.xml
index d9b579c..cf7c2fd 100644
--- a/app/res/values-bn/strings.xml
+++ b/app/res/values-bn/strings.xml
@@ -34,5 +34,7 @@
<string name="recents_empty_state_text" msgid="8228569970506899117">"কোনও সাম্প্রতিক আইটেম নেই"</string>
<string name="recents_clear_all_text" msgid="3594272268167720553">"সব মুছে দিন"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"অ্যাপ উপলভ্য নেই"</string>
- <string name="calm_mode_title" msgid="4364804976931157567">"\'কাম\' মোড"</string>
+ <string name="calm_mode_title" msgid="4364804976931157567">"Calm মোড"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"সারি"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"মিডিয়া সোর্স"</string>
</resources>
diff --git a/app/res/values-bs/strings.xml b/app/res/values-bs/strings.xml
index 23c696f..c17c375 100644
--- a/app/res/values-bs/strings.xml
+++ b/app/res/values-bs/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Obriši sve"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Aplikacija nije dostupna"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Način rada za opuštanje"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Red čekanja"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Izvor medijskog sadržaja"</string>
</resources>
diff --git a/app/res/values-ca/strings.xml b/app/res/values-ca/strings.xml
index e2d8eec..2db4151 100644
--- a/app/res/values-ca/strings.xml
+++ b/app/res/values-ca/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Esborra-ho tot"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"L\'aplicació no està disponible"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Mode de calma"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Cua"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Font del contingut multimèdia"</string>
</resources>
diff --git a/app/res/values-cs/strings.xml b/app/res/values-cs/strings.xml
index 8fd05bd..bbf2d9d 100644
--- a/app/res/values-cs/strings.xml
+++ b/app/res/values-cs/strings.xml
@@ -31,8 +31,10 @@
<string name="fake_weather_main_text" msgid="2545755284647327839">"--° Většinou slunečno"</string>
<string name="fake_weather_footer_text" msgid="8640814250285014485">"Mountain View • Nejvyšší: --° Nejnižší: --°"</string>
<string name="times_separator" msgid="1962841895013564645">"/"</string>
- <string name="recents_empty_state_text" msgid="8228569970506899117">"Žádné nedávné položky"</string>
+ <string name="recents_empty_state_text" msgid="8228569970506899117">"Žádné položky z nedávné doby"</string>
<string name="recents_clear_all_text" msgid="3594272268167720553">"Vymazat vše"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Aplikace není k dispozici"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Klidný režim"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Fronta"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Zdroj médií"</string>
</resources>
diff --git a/app/res/values-da/strings.xml b/app/res/values-da/strings.xml
index f17d3fa..f6f6125 100644
--- a/app/res/values-da/strings.xml
+++ b/app/res/values-da/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Ryd alt"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Appen er ikke tilgængelig"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Beroligende tilstand"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Kø"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Mediekilde"</string>
</resources>
diff --git a/app/res/values-de/strings.xml b/app/res/values-de/strings.xml
index ef60b01..4bae3c9 100644
--- a/app/res/values-de/strings.xml
+++ b/app/res/values-de/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Alles löschen"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"App nicht verfügbar"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Ruhemodus"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Wiedergabeliste"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Medienquelle"</string>
</resources>
diff --git a/app/res/values-el/strings.xml b/app/res/values-el/strings.xml
index 9c9bf06..469d2c7 100644
--- a/app/res/values-el/strings.xml
+++ b/app/res/values-el/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Διαγραφή όλων"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Η εφαρμογή δεν είναι διαθέσιμη"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Λειτουργία ηρεμίας"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Ουρά"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Πηγή μέσων"</string>
</resources>
diff --git a/app/res/values-en-rAU/strings.xml b/app/res/values-en-rAU/strings.xml
index 9c42c4d..1c7025f 100644
--- a/app/res/values-en-rAU/strings.xml
+++ b/app/res/values-en-rAU/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Clear all"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"App isn\'t available"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Calm mode"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Queue"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Media source"</string>
</resources>
diff --git a/app/res/values-en-rCA/strings.xml b/app/res/values-en-rCA/strings.xml
index a62433b..f3a023b 100644
--- a/app/res/values-en-rCA/strings.xml
+++ b/app/res/values-en-rCA/strings.xml
@@ -17,39 +17,24 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <!-- no translation found for app_title (1056886619192068947) -->
- <skip />
+ <string name="app_title" msgid="1056886619192068947">"Car Launcher"</string>
<string name="default_media_song_title" msgid="7837564242036091946"></string>
- <!-- no translation found for tap_for_more_info_text (4240146824238692769) -->
- <skip />
- <!-- no translation found for tap_to_launch_text (7150379866796152196) -->
- <skip />
- <!-- no translation found for ongoing_call_duration_text_separator (2140398350095052096) -->
- <skip />
- <!-- no translation found for ongoing_call_text (7160701768924041827) -->
- <skip />
- <!-- no translation found for dialing_call_text (3286036311692512894) -->
- <skip />
- <!-- no translation found for projected_launch_text (5034079820478748609) -->
- <skip />
- <!-- no translation found for projected_onclick_launch_error_toast_text (8853804785626030351) -->
- <skip />
- <!-- no translation found for projection_devices (2556503818120676439) -->
- <skip />
- <!-- no translation found for weather_app_name (4356705068077942048) -->
- <skip />
- <!-- no translation found for fake_weather_main_text (2545755284647327839) -->
- <skip />
- <!-- no translation found for fake_weather_footer_text (8640814250285014485) -->
- <skip />
- <!-- no translation found for times_separator (1962841895013564645) -->
- <skip />
- <!-- no translation found for recents_empty_state_text (8228569970506899117) -->
- <skip />
- <!-- no translation found for recents_clear_all_text (3594272268167720553) -->
- <skip />
- <!-- no translation found for failure_opening_recent_task_message (963567570097465902) -->
- <skip />
- <!-- no translation found for calm_mode_title (4364804976931157567) -->
- <skip />
+ <string name="tap_for_more_info_text" msgid="4240146824238692769">"Tap card for more info"</string>
+ <string name="tap_to_launch_text" msgid="7150379866796152196">"Tap card to launch"</string>
+ <string name="ongoing_call_duration_text_separator" msgid="2140398350095052096">" • "</string>
+ <string name="ongoing_call_text" msgid="7160701768924041827">"Ongoing call"</string>
+ <string name="dialing_call_text" msgid="3286036311692512894">"Dialing…"</string>
+ <string name="projected_launch_text" msgid="5034079820478748609">"Launch Android Auto"</string>
+ <string name="projected_onclick_launch_error_toast_text" msgid="8853804785626030351">"Unable to launch Android Auto. No activity found."</string>
+ <string name="projection_devices" msgid="2556503818120676439">"{count,plural, =1{# device}other{# devices}}"</string>
+ <string name="weather_app_name" msgid="4356705068077942048">"Weather"</string>
+ <string name="fake_weather_main_text" msgid="2545755284647327839">"--° Mostly sunny"</string>
+ <string name="fake_weather_footer_text" msgid="8640814250285014485">"Mountain View • H: --° L: --°"</string>
+ <string name="times_separator" msgid="1962841895013564645">"/"</string>
+ <string name="recents_empty_state_text" msgid="8228569970506899117">"No recent items"</string>
+ <string name="recents_clear_all_text" msgid="3594272268167720553">"Clear All"</string>
+ <string name="failure_opening_recent_task_message" msgid="963567570097465902">"App isn\'t available"</string>
+ <string name="calm_mode_title" msgid="4364804976931157567">"Calm mode"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Queue"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Media Source"</string>
</resources>
diff --git a/app/res/values-en-rGB/strings.xml b/app/res/values-en-rGB/strings.xml
index 9c42c4d..1c7025f 100644
--- a/app/res/values-en-rGB/strings.xml
+++ b/app/res/values-en-rGB/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Clear all"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"App isn\'t available"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Calm mode"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Queue"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Media source"</string>
</resources>
diff --git a/app/res/values-en-rIN/strings.xml b/app/res/values-en-rIN/strings.xml
index 9c42c4d..1c7025f 100644
--- a/app/res/values-en-rIN/strings.xml
+++ b/app/res/values-en-rIN/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Clear all"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"App isn\'t available"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Calm mode"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Queue"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Media source"</string>
</resources>
diff --git a/app/res/values-en-rXC/strings.xml b/app/res/values-en-rXC/strings.xml
index 3a23f19..879e1c5 100644
--- a/app/res/values-en-rXC/strings.xml
+++ b/app/res/values-en-rXC/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Clear All"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"App isn\'t available"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Calm mode"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Queue"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Media Source"</string>
</resources>
diff --git a/app/res/values-es-rUS/strings.xml b/app/res/values-es-rUS/strings.xml
index bc3a90d..f87067d 100644
--- a/app/res/values-es-rUS/strings.xml
+++ b/app/res/values-es-rUS/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Borrar todo"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"La app no está disponible"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Modo calma"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Fila"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Fuente multimedia"</string>
</resources>
diff --git a/app/res/values-es/strings.xml b/app/res/values-es/strings.xml
index 961c95b..31e209c 100644
--- a/app/res/values-es/strings.xml
+++ b/app/res/values-es/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Borrar todo"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"La aplicación no está disponible"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Modo Calma"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Cola"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Fuente de contenido multimedia"</string>
</resources>
diff --git a/app/res/values-et/strings.xml b/app/res/values-et/strings.xml
index db6e3e6..473b29d 100644
--- a/app/res/values-et/strings.xml
+++ b/app/res/values-et/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Kustuta kõik"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Rakendus ei ole saadaval"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Lõõgastusrežiim"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Järjekord"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Meediaallikas"</string>
</resources>
diff --git a/app/res/values-eu/strings.xml b/app/res/values-eu/strings.xml
index c53b647..aee645e 100644
--- a/app/res/values-eu/strings.xml
+++ b/app/res/values-eu/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Garbitu guztia"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Ez dago erabilgarri aplikazioa"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Modu lasaia"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Ilara"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Multimedia-iturburua"</string>
</resources>
diff --git a/app/res/values-fa/strings.xml b/app/res/values-fa/strings.xml
index c4ceec8..244e395 100644
--- a/app/res/values-fa/strings.xml
+++ b/app/res/values-fa/strings.xml
@@ -19,8 +19,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_title" msgid="1056886619192068947">"راهانداز خودرو"</string>
<string name="default_media_song_title" msgid="7837564242036091946"></string>
- <string name="tap_for_more_info_text" msgid="4240146824238692769">"برای اطلاعات بیشتر، روی کارت ضربه بزنید"</string>
- <string name="tap_to_launch_text" msgid="7150379866796152196">"برای راهاندازی، روی کارت ضربه بزنید"</string>
+ <string name="tap_for_more_info_text" msgid="4240146824238692769">"برای اطلاعات بیشتر، روی کارت تکضرب بزنید"</string>
+ <string name="tap_to_launch_text" msgid="7150379866796152196">"برای راهاندازی، روی کارت تکضرب بزنید"</string>
<string name="ongoing_call_duration_text_separator" msgid="2140398350095052096">" • "</string>
<string name="ongoing_call_text" msgid="7160701768924041827">"تماس درحال انجام"</string>
<string name="dialing_call_text" msgid="3286036311692512894">"درحال شمارهگیری…"</string>
@@ -31,8 +31,10 @@
<string name="fake_weather_main_text" msgid="2545755284647327839">"--° بیشتر آفتابی"</string>
<string name="fake_weather_footer_text" msgid="8640814250285014485">"مانتین ویو • بیشینه: --° کمینه: --°"</string>
<string name="times_separator" msgid="1962841895013564645">"/"</string>
- <string name="recents_empty_state_text" msgid="8228569970506899117">"مورد جدیدی وجود ندارد"</string>
+ <string name="recents_empty_state_text" msgid="8228569970506899117">"چیز جدیدی اینجا نیست"</string>
<string name="recents_clear_all_text" msgid="3594272268167720553">"پاک کردن همه"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"برنامه دردسترس نیست"</string>
- <string name="calm_mode_title" msgid="4364804976931157567">"حالت آرام"</string>
+ <string name="calm_mode_title" msgid="4364804976931157567">"حالت «آرام»"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"صف پخش"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"منبع رسانه"</string>
</resources>
diff --git a/app/res/values-fi/strings.xml b/app/res/values-fi/strings.xml
index 8d31ddc..f2be9a3 100644
--- a/app/res/values-fi/strings.xml
+++ b/app/res/values-fi/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Tyhjennä kaikki"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Sovellus ei ole käytettävissä"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Rauhallinen tila"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Jono"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Medialähde"</string>
</resources>
diff --git a/app/res/values-fr-rCA/strings.xml b/app/res/values-fr-rCA/strings.xml
index e638bd1..ed8adb3 100644
--- a/app/res/values-fr-rCA/strings.xml
+++ b/app/res/values-fr-rCA/strings.xml
@@ -34,6 +34,7 @@
<string name="recents_empty_state_text" msgid="8228569970506899117">"Aucun élément récent"</string>
<string name="recents_clear_all_text" msgid="3594272268167720553">"Tout effacer"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"L\'application n\'est pas accessible"</string>
- <!-- no translation found for calm_mode_title (4364804976931157567) -->
- <skip />
+ <string name="calm_mode_title" msgid="4364804976931157567">"Mode Calme"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"File d\'attente"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Source du contenu multimédia"</string>
</resources>
diff --git a/app/res/values-fr/strings.xml b/app/res/values-fr/strings.xml
index 8573c01..bee99a1 100644
--- a/app/res/values-fr/strings.xml
+++ b/app/res/values-fr/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Tout effacer"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Appli indisponible"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Mode calme"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"File d\'attente"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Source multimédia"</string>
</resources>
diff --git a/app/res/values-gl/strings.xml b/app/res/values-gl/strings.xml
index b581569..5183865 100644
--- a/app/res/values-gl/strings.xml
+++ b/app/res/values-gl/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Borrar todo"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"A aplicación non está dispoñible"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Modo de calma"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Cola"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Fonte do contido multimedia"</string>
</resources>
diff --git a/app/res/values-gu/strings.xml b/app/res/values-gu/strings.xml
index ff9c5b7..e04a037 100644
--- a/app/res/values-gu/strings.xml
+++ b/app/res/values-gu/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"બધું સાફ કરો"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"ઍપ ઉપલબ્ધ નથી"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"શાંત મોડ"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"કતાર"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"મીડિયા સૉર્સ"</string>
</resources>
diff --git a/app/res/values-hi/strings.xml b/app/res/values-hi/strings.xml
index bbc3ee2..80f72bf 100644
--- a/app/res/values-hi/strings.xml
+++ b/app/res/values-hi/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"सभी मिटाएं"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"ऐप्लिकेशन उपलब्ध नहीं है"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"काम (शांत) मोड"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"सूची"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"मीडिया सोर्स"</string>
</resources>
diff --git a/app/res/values-hr/strings.xml b/app/res/values-hr/strings.xml
index 1c33dff..efc3cb1 100644
--- a/app/res/values-hr/strings.xml
+++ b/app/res/values-hr/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Izbriši sve"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Aplikacija nije dostupna"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Način opuštanja"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Red čekanja"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Izvor medija"</string>
</resources>
diff --git a/app/res/values-hu/strings.xml b/app/res/values-hu/strings.xml
index 4c08fa7..6b62690 100644
--- a/app/res/values-hu/strings.xml
+++ b/app/res/values-hu/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Összes törlése"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Az alkalmazás nem áll rendelkezésre"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Nyugalom mód"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Lejátszási sor"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Médiaforrás"</string>
</resources>
diff --git a/app/res/values-hy/strings.xml b/app/res/values-hy/strings.xml
index 906fb6b..269c47a 100644
--- a/app/res/values-hy/strings.xml
+++ b/app/res/values-hy/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Ջնջել բոլորը"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Հավելվածը հասանելի չէ"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Հանգստի ռեժիմ"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Հերթացանկ"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Մեդիաֆայլի աղբյուրը"</string>
</resources>
diff --git a/app/res/values-in/strings.xml b/app/res/values-in/strings.xml
index ab9c491..41544df 100644
--- a/app/res/values-in/strings.xml
+++ b/app/res/values-in/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Hapus Semua"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Aplikasi tidak tersedia"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Mode tenang"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Antrean"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Sumber Media"</string>
</resources>
diff --git a/app/res/values-is/strings.xml b/app/res/values-is/strings.xml
index 3362bd4..3cc559e 100644
--- a/app/res/values-is/strings.xml
+++ b/app/res/values-is/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Hreinsa allt"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Forritið er ekki í boði"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Róleg stilling"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Röð"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Uppruni efnis"</string>
</resources>
diff --git a/app/res/values-it/strings.xml b/app/res/values-it/strings.xml
index 15b9890..2c21e8e 100644
--- a/app/res/values-it/strings.xml
+++ b/app/res/values-it/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Cancella tutto"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"App non disponibile"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Modalità Calma"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Coda"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Fonte di contenuti multimediali"</string>
</resources>
diff --git a/app/res/values-iw/strings.xml b/app/res/values-iw/strings.xml
index 050d187..d809ed0 100644
--- a/app/res/values-iw/strings.xml
+++ b/app/res/values-iw/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"ניקוי הכול"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"האפליקציה לא זמינה"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"מצב רגיעה"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"הבאים בתור"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"מקור המדיה"</string>
</resources>
diff --git a/app/res/values-ja/strings.xml b/app/res/values-ja/strings.xml
index ce968b5..2a37dd9 100644
--- a/app/res/values-ja/strings.xml
+++ b/app/res/values-ja/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"すべて消去"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"このアプリは使用できません"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Calm モード"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"キュー"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"メディアソース"</string>
</resources>
diff --git a/app/res/values-ka/strings.xml b/app/res/values-ka/strings.xml
index 9920b17..324a74f 100644
--- a/app/res/values-ka/strings.xml
+++ b/app/res/values-ka/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"ყველას გასუფთავება"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"აპი მიუწვდომელია"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"წყნარი რეჟიმი"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"რიგი"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"მედიაწყარო"</string>
</resources>
diff --git a/app/res/values-kk/strings.xml b/app/res/values-kk/strings.xml
index fe1b8d9..bbfa91a 100644
--- a/app/res/values-kk/strings.xml
+++ b/app/res/values-kk/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Барлығын өшіру"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Қолданба қолжетімді емес."</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Тыныштық режимі"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Кезек"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Медиа дереккөздері"</string>
</resources>
diff --git a/app/res/values-km/strings.xml b/app/res/values-km/strings.xml
index 0ca0780..9a3b520 100644
--- a/app/res/values-km/strings.xml
+++ b/app/res/values-km/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"សម្អាតទាំងអស់"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"មិនមានកម្មវិធី"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"មុខងារស្ងាត់"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"ជួរ"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"ប្រភពមេឌៀ"</string>
</resources>
diff --git a/app/res/values-kn/strings.xml b/app/res/values-kn/strings.xml
index 2d42f2f..67259ee 100644
--- a/app/res/values-kn/strings.xml
+++ b/app/res/values-kn/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"ಎಲ್ಲವನ್ನೂ ಅಳಿಸಿ"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"ಆ್ಯಪ್ ಲಭ್ಯವಿಲ್ಲ"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"ಶಾಂತ ಮೋಡ್"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"ಸರದಿ"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"ಮಾಧ್ಯಮದ ಮೂಲ"</string>
</resources>
diff --git a/app/res/values-ko/strings.xml b/app/res/values-ko/strings.xml
index b85a262..4c5bd05 100644
--- a/app/res/values-ko/strings.xml
+++ b/app/res/values-ko/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"모두 지우기"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"앱을 사용할 수 없음"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"고요 모드"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"현재 재생목록"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"미디어 소스"</string>
</resources>
diff --git a/app/res/values-ky/strings.xml b/app/res/values-ky/strings.xml
index 5ff6cb1..ebc3ad4 100644
--- a/app/res/values-ky/strings.xml
+++ b/app/res/values-ky/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Баарын тазалоо"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Колдонмо жеткиликтүү эмес"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Тынчтык режими"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Кезек"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Мультимедия булагы"</string>
</resources>
diff --git a/app/res/values-lo/strings.xml b/app/res/values-lo/strings.xml
index 6cd8049..3aa1070 100644
--- a/app/res/values-lo/strings.xml
+++ b/app/res/values-lo/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"ລຶບລ້າງທັງໝົດ"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"ແອັບບໍ່ພ້ອມໃຫ້ນຳໃຊ້"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"ໂໝດສະຫງົບ"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"ຄິວ"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"ແຫຼ່ງທີ່ມາຂອງສື່"</string>
</resources>
diff --git a/app/res/values-lt/strings.xml b/app/res/values-lt/strings.xml
index 1c0d051..917f0ec 100644
--- a/app/res/values-lt/strings.xml
+++ b/app/res/values-lt/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Išvalyti viską"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Programa nepasiekiama"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Ramybės režimas"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Eilė"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Medijos šaltinis"</string>
</resources>
diff --git a/app/res/values-lv/strings.xml b/app/res/values-lv/strings.xml
index 4d8b539..5bef76d 100644
--- a/app/res/values-lv/strings.xml
+++ b/app/res/values-lv/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Notīrīt visu"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Lietotne nav pieejama"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Miera režīms"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Rinda"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Multivides avots"</string>
</resources>
diff --git a/app/res/values-mk/strings.xml b/app/res/values-mk/strings.xml
index a7f5d5c..a27ca54 100644
--- a/app/res/values-mk/strings.xml
+++ b/app/res/values-mk/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Избриши ги сите"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Апликацијата не е достапна"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Режим на мирување"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Редица"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Извор на аудиовизуелни содржини"</string>
</resources>
diff --git a/app/res/values-ml/strings.xml b/app/res/values-ml/strings.xml
index c65d5bb..4149487 100644
--- a/app/res/values-ml/strings.xml
+++ b/app/res/values-ml/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"എല്ലാം മായ്ക്കുക"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"ആപ്പ് ലഭ്യമല്ല"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"\'ശാന്തം\' മോഡ്"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"ക്യൂ"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"മീഡിയാ ഉറവിടം"</string>
</resources>
diff --git a/app/res/values-mn/strings.xml b/app/res/values-mn/strings.xml
index 7a0ec37..7b38c04 100644
--- a/app/res/values-mn/strings.xml
+++ b/app/res/values-mn/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Бүгдийг арилгах"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Апп боломжгүй байна"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Тайван горим"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Дараалал"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Медиагийн эх сурвалж"</string>
</resources>
diff --git a/app/res/values-mr/strings.xml b/app/res/values-mr/strings.xml
index dc2bce3..4b16e02 100644
--- a/app/res/values-mr/strings.xml
+++ b/app/res/values-mr/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"सर्व साफ करा"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"अॅप उपलब्ध नाही"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"शांत मोड"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"क्यू"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"मीडिया स्रोत"</string>
</resources>
diff --git a/app/res/values-ms/strings.xml b/app/res/values-ms/strings.xml
index fe43262..a5c1f39 100644
--- a/app/res/values-ms/strings.xml
+++ b/app/res/values-ms/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Kosongkan Semua"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Apl tidak tersedia"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Mod Calm"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Baris gilir"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Sumber Media"</string>
</resources>
diff --git a/app/res/values-my/strings.xml b/app/res/values-my/strings.xml
index 57b5005..4401c9f 100644
--- a/app/res/values-my/strings.xml
+++ b/app/res/values-my/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"အားလုံးရှင်းရန်"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"အက်ပ် မရနိုင်ပါ"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"အငြိမ်မုဒ်"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"စာရင်းစဉ်"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"မီဒီယာရင်းမြစ်"</string>
</resources>
diff --git a/app/res/values-nb/strings.xml b/app/res/values-nb/strings.xml
index e9f4b4a..83901e8 100644
--- a/app/res/values-nb/strings.xml
+++ b/app/res/values-nb/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Fjern alt"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Appen er ikke tilgjengelig"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Roligmodus"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Kø"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Mediekilde"</string>
</resources>
diff --git a/app/res/values-ne/strings.xml b/app/res/values-ne/strings.xml
index 4053971..d70c2d2 100644
--- a/app/res/values-ne/strings.xml
+++ b/app/res/values-ne/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"सबै सामग्री मेटाउनुहोस्"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"एप उपलब्ध छैन"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"शान्त मोड"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"लाइन"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"मिडियाको स्रोत"</string>
</resources>
diff --git a/app/res/values-nl/strings.xml b/app/res/values-nl/strings.xml
index 86a9cdb..57fabfa 100644
--- a/app/res/values-nl/strings.xml
+++ b/app/res/values-nl/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Alles wissen"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"App is niet beschikbaar"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Kalme modus"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Wachtrij"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Mediabron"</string>
</resources>
diff --git a/app/res/values-or/strings.xml b/app/res/values-or/strings.xml
index 410bac8..0bb6c16 100644
--- a/app/res/values-or/strings.xml
+++ b/app/res/values-or/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"ସବୁ ଖାଲି କରନ୍ତୁ"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"ଆପ ଉପଲବ୍ଧ ନାହିଁ"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"ଶାନ୍ତ ମୋଡ"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"ଧାଡ଼ି"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"ମିଡିଆ ସୋର୍ସ"</string>
</resources>
diff --git a/app/res/values-pa/strings.xml b/app/res/values-pa/strings.xml
index 1e52c85..8659649 100644
--- a/app/res/values-pa/strings.xml
+++ b/app/res/values-pa/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"ਸਭ ਕਲੀਅਰ ਕਰੋ"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"ਐਪ ਉਪਲਬਧ ਨਹੀਂ ਹੈ"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"ਸ਼ਾਂਤ ਮੋਡ"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"ਕਤਾਰ"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"ਮੀਡੀਆ ਸਰੋਤ"</string>
</resources>
diff --git a/app/res/values-pl/strings.xml b/app/res/values-pl/strings.xml
index 3fbe11a..da3000c 100644
--- a/app/res/values-pl/strings.xml
+++ b/app/res/values-pl/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Wyczyść wszystko"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Aplikacja jest niedostępna"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Tryb cichy"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Kolejka"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Źródło multimediów"</string>
</resources>
diff --git a/app/res/values-pt-rPT/strings.xml b/app/res/values-pt-rPT/strings.xml
index ff491a2..a3f7bd2 100644
--- a/app/res/values-pt-rPT/strings.xml
+++ b/app/res/values-pt-rPT/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Limpar tudo"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"A app não está disponível"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Modo Calm"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Fila"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Origem de multimédia"</string>
</resources>
diff --git a/app/res/values-pt/strings.xml b/app/res/values-pt/strings.xml
index daa7dd9..6c90235 100644
--- a/app/res/values-pt/strings.xml
+++ b/app/res/values-pt/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Limpar tudo"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"O app não está disponível"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Modo foco"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Fila"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Fonte de mídia"</string>
</resources>
diff --git a/app/res/values-ro/strings.xml b/app/res/values-ro/strings.xml
index 5d46cd8..0880f88 100644
--- a/app/res/values-ro/strings.xml
+++ b/app/res/values-ro/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Șterge tot"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Aplicația nu este disponibilă"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Modul Calm"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Coadă"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Sursă media"</string>
</resources>
diff --git a/app/res/values-ru/strings.xml b/app/res/values-ru/strings.xml
index 33dc160..4b5409d 100644
--- a/app/res/values-ru/strings.xml
+++ b/app/res/values-ru/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Очистить"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Приложение недоступно"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Режим покоя"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Очередь"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Источник мультимедиа"</string>
</resources>
diff --git a/app/res/values-si/strings.xml b/app/res/values-si/strings.xml
index c18a55c..77bf213 100644
--- a/app/res/values-si/strings.xml
+++ b/app/res/values-si/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"සියල්ල හිස් කරන්න"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"යෙදුම නොතිබේ"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"සන්සුන් ප්රකාරය"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"පෝලිම"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"මාධ්ය ප්රභවය"</string>
</resources>
diff --git a/app/res/values-sk/strings.xml b/app/res/values-sk/strings.xml
index 46f6548..14839ef 100644
--- a/app/res/values-sk/strings.xml
+++ b/app/res/values-sk/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Vymazať všetko"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Aplikácia nie je k dispozícii"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Pokojný režim"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Poradie"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Zdroj médií"</string>
</resources>
diff --git a/app/res/values-sl/strings.xml b/app/res/values-sl/strings.xml
index 9a408fc..66a86ff 100644
--- a/app/res/values-sl/strings.xml
+++ b/app/res/values-sl/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Izbriši vse"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Aplikacija ni na voljo"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Umirjeni način"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Čakalna vrsta"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Vir predstavnosti"</string>
</resources>
diff --git a/app/res/values-sq/strings.xml b/app/res/values-sq/strings.xml
index 3d7d39b..2d5b5f9 100644
--- a/app/res/values-sq/strings.xml
+++ b/app/res/values-sq/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Pastro të gjitha"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Aplikacioni nuk ofrohet"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Modaliteti i qetësisë"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Radha"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Burimi i medias"</string>
</resources>
diff --git a/app/res/values-sr/strings.xml b/app/res/values-sr/strings.xml
index 3a3ae8f..33e5e34 100644
--- a/app/res/values-sr/strings.xml
+++ b/app/res/values-sr/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Обриши све"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Апликација није доступна"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Режим опуштања"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Редослед"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Извор медија"</string>
</resources>
diff --git a/app/res/values-sv/strings.xml b/app/res/values-sv/strings.xml
index af06154..31e5d96 100644
--- a/app/res/values-sv/strings.xml
+++ b/app/res/values-sv/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Rensa allt"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Appen är inte tillgänglig"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Lugnt läge"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Kö"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Mediekälla"</string>
</resources>
diff --git a/app/res/values-sw/strings.xml b/app/res/values-sw/strings.xml
index 9e71a9d..0e4b207 100644
--- a/app/res/values-sw/strings.xml
+++ b/app/res/values-sw/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Futa Zote"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Programu haipatikani"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Hali ya utulivu"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Foleni"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Chanzo cha Maudhui"</string>
</resources>
diff --git a/app/res/values-ta/strings.xml b/app/res/values-ta/strings.xml
index fb29415..633389b 100644
--- a/app/res/values-ta/strings.xml
+++ b/app/res/values-ta/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"அனைத்தையும் அழி"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"ஆப்ஸ் கிடைக்கவில்லை"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"அமைதிப் பயன்முறை"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"வரிசை"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"மீடியா ஆதாரம்"</string>
</resources>
diff --git a/app/res/values-te/strings.xml b/app/res/values-te/strings.xml
index 887b13f..08b5b89 100644
--- a/app/res/values-te/strings.xml
+++ b/app/res/values-te/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"అన్నీ తీసివేయండి"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"యాప్ అందుబాటులో లేదు"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"క్లెయిమ్ మోడ్"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"క్యూ"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"మీడియా సోర్స్"</string>
</resources>
diff --git a/app/res/values-th/strings.xml b/app/res/values-th/strings.xml
index f84e327..f3c88ed 100644
--- a/app/res/values-th/strings.xml
+++ b/app/res/values-th/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"ล้างทั้งหมด"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"แอปไม่พร้อมใช้งาน"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"โหมด Calm"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"คิว"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"แหล่งที่มาของสื่อ"</string>
</resources>
diff --git a/app/res/values-tl/strings.xml b/app/res/values-tl/strings.xml
index 3c76a8b..f391d00 100644
--- a/app/res/values-tl/strings.xml
+++ b/app/res/values-tl/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"I-clear Lahat"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Hindi available ang app"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Calm mode"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Queue"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Source ng Media"</string>
</resources>
diff --git a/app/res/values-tr/strings.xml b/app/res/values-tr/strings.xml
index eecca89..fed16ee 100644
--- a/app/res/values-tr/strings.xml
+++ b/app/res/values-tr/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Tümünü Temizle"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Uygulama kullanılamıyor"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Sakin mod"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Sıra"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Medya Kaynağı"</string>
</resources>
diff --git a/app/res/values-uk/strings.xml b/app/res/values-uk/strings.xml
index 1aa273e..c4768a6 100644
--- a/app/res/values-uk/strings.xml
+++ b/app/res/values-uk/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Очистити все"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Додаток недоступний"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Спокійний режим"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Черга"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Джерело мультимедіа"</string>
</resources>
diff --git a/app/res/values-ur/strings.xml b/app/res/values-ur/strings.xml
index 230dc64..d720ba9 100644
--- a/app/res/values-ur/strings.xml
+++ b/app/res/values-ur/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"تمام صاف کریں"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"ایپ دستیاب نہیں ہے"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"پُرسکون وضع"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"قطار"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"میڈیا کا ماخذ"</string>
</resources>
diff --git a/app/res/values-uz/strings.xml b/app/res/values-uz/strings.xml
index 97bc0cc..651ea49 100644
--- a/app/res/values-uz/strings.xml
+++ b/app/res/values-uz/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Hammasini yopish"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Ilova mavjud emas"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Dam olish rejimi"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Navbat"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Media manbasi"</string>
</resources>
diff --git a/app/res/values-vi/strings.xml b/app/res/values-vi/strings.xml
index 8e84aae..347cf9e 100644
--- a/app/res/values-vi/strings.xml
+++ b/app/res/values-vi/strings.xml
@@ -34,6 +34,7 @@
<string name="recents_empty_state_text" msgid="8228569970506899117">"Không có mục nào gần đây"</string>
<string name="recents_clear_all_text" msgid="3594272268167720553">"Xoá tất cả"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"Hiện không có ứng dụng"</string>
- <!-- no translation found for calm_mode_title (4364804976931157567) -->
- <skip />
+ <string name="calm_mode_title" msgid="4364804976931157567">"Chế độ Tĩnh lặng"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Danh sách chờ"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Nguồn nội dung nghe nhìn"</string>
</resources>
diff --git a/app/res/values-zh-rCN/strings.xml b/app/res/values-zh-rCN/strings.xml
index 1992d3b..c88a3bd 100644
--- a/app/res/values-zh-rCN/strings.xml
+++ b/app/res/values-zh-rCN/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"全部清除"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"应用无法打开"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"平静模式"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"队列"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"媒体来源"</string>
</resources>
diff --git a/app/res/values-zh-rHK/strings.xml b/app/res/values-zh-rHK/strings.xml
index 031c0af..76fa14f 100644
--- a/app/res/values-zh-rHK/strings.xml
+++ b/app/res/values-zh-rHK/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"全部清除"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"目前無法使用這個應用程式"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"平靜模式"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"序列"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"媒體來源"</string>
</resources>
diff --git a/app/res/values-zh-rTW/strings.xml b/app/res/values-zh-rTW/strings.xml
index 128f739..44faf22 100644
--- a/app/res/values-zh-rTW/strings.xml
+++ b/app/res/values-zh-rTW/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"全部清除"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"應用程式目前無法使用"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"平靜模式"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"待播清單"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"媒體來源"</string>
</resources>
diff --git a/app/res/values-zu/strings.xml b/app/res/values-zu/strings.xml
index 7abc29f..a5557e5 100644
--- a/app/res/values-zu/strings.xml
+++ b/app/res/values-zu/strings.xml
@@ -35,4 +35,6 @@
<string name="recents_clear_all_text" msgid="3594272268167720553">"Sula Konke"</string>
<string name="failure_opening_recent_task_message" msgid="963567570097465902">"I-app ayitholakali"</string>
<string name="calm_mode_title" msgid="4364804976931157567">"Imodi ezolile"</string>
+ <string name="media_card_queue_header_title" msgid="8801994125708995575">"Ulayini"</string>
+ <string name="media_card_history_header_title" msgid="8337396297165848931">"Umthombo Wemidiya"</string>
</resources>
diff --git a/app/res/values/colors.xml b/app/res/values/colors.xml
index 954c3cf..d741960 100644
--- a/app/res/values/colors.xml
+++ b/app/res/values/colors.xml
@@ -15,15 +15,17 @@
-->
<resources>
<color name="date_divider_bar_color">@*android:color/car_grey_500</color>
- <color name="media_button_tint">@*android:color/car_tint</color>
<color name="card_background_scrim">#11151B</color>
<color name="tap_for_more_text_color">#DADCE0</color>
<color name="dialer_button_icon_color">#FFFFFF</color>
<color name="dialer_end_call_button_color">#EE675C</color>
- <color name="minimized_progress_bar_background">#5CFFFFFF</color>
<color name="launcher_home_icon_color">@*android:color/car_accent_light</color>
<color name="seek_bar_color">@*android:color/car_accent</color>
<color name="recents_background_color">@*android:color/car_grey_900</color>
<color name="default_recents_thumbnail_color">@*android:color/car_grey_846</color>
<color name="clear_all_recents_text_color">@*android:color/car_accent</color>
+
+ <!-- CarUiPortraitLauncherReferenceRRO relies on overlaying these values -->
+ <color name="media_button_tint">@*android:color/car_tint</color>
+ <color name="minimized_progress_bar_background">#5CFFFFFF</color>
</resources>
diff --git a/app/res/values/config.xml b/app/res/values/config.xml
index 1bf9bae..bd4bbba 100644
--- a/app/res/values/config.xml
+++ b/app/res/values/config.xml
@@ -61,22 +61,13 @@
<string-array name="config_taskViewPackages" translatable="false">
</string-array>
- <!--
- Determines whether or not to use the RemoteCarTaskView from car-lib instead of the
- CarTaskView that exists inside the CarLauncher.
- -->
- <bool name="config_useRemoteCarTaskView">true</bool>
-
- <!--
- The Activity to use as a passenger Launcher, if empty, it assumes CarLauncher can do
- the passenger Launcher role too.
- -->
- <string name="config_passengerLauncherComponent">com.android.car.multidisplay/.launcher.LauncherActivity</string>
-
<!-- Boolean value to indicate if the secondary descriptive text of homescreen cards
without controls should have multiple lines -->
<bool name="config_homecard_single_line_secondary_descriptive_text">true</bool>
+ <!-- Boolean value to indicate if the recents activity should open the most recent task on dismiss. -->
+ <bool name="config_launch_most_recent_task_on_recents_dismiss">true</bool>
+
<!-- Config values for Calm mode and information shown on the Calm mode screen -->
<bool name="config_enableCalmMode">true</bool>
<bool name="config_calmMode_showClock">true</bool>
@@ -84,7 +75,6 @@
<bool name="config_calmMode_showNavigation">true</bool>
<bool name="config_calmMode_showDate">true</bool>
<bool name="config_calmMode_showTemperature">true</bool>
- <string name="config_calmMode_packageName">com.android.car.carlauncher</string>
- <string name="config_calmMode_activityName">com.android.car.carlauncher.calmmode.CalmModeActivity</string>
+ <string name="config_calmMode_componentName">com.android.car.carlauncher/com.android.car.carlauncher.calmmode.CalmModeActivity</string>
</resources>
diff --git a/app/res/values/dimens.xml b/app/res/values/dimens.xml
index 7398e4b..48c8722 100644
--- a/app/res/values/dimens.xml
+++ b/app/res/values/dimens.xml
@@ -29,7 +29,7 @@
<dimen name="main_screen_widget_margin">16dp</dimen>
<!-- Card dimensions -->
- <dimen name="card_width">388dp</dimen>
+ <dimen name="card_width">400dp</dimen>
<!-- Card Header dimensions -->
<dimen name="card_content_margin">24dp</dimen>
@@ -42,8 +42,8 @@
<dimen name="descriptive_text_with_controls_top_margin">24dp</dimen>
<!-- Percent transparency of the scrim applied to the image used for the card's background as a float between 0 and 1, where 0 applies no darkening scrim-->
<dimen name="card_background_scrim_alpha" format="float">0.72</dimen>
- <!--Percent by which to blur the image used for the card's background as a float between 0 and 1, where 0 is not blurred-->
- <dimen name="card_background_image_blur_radius" format="float">0.36</dimen>
+ <!--Blur radius for the RenderEffect for the card's background image as a float, where 0 is not blurred -->
+ <dimen name="card_background_image_blur_radius" format="float">25</dimen>
<!-- card_content_descriptive_text dimensions -->
<dimen name="card_content_image_size">76dp</dimen>
@@ -88,9 +88,55 @@
<dimen name="recent_task_width">432dp</dimen>
<!-- Calm mode sizes -->
- <dimen name="calm_mode_padding">12dp</dimen>
+ <dimen name="calm_mode_padding_vertical">40dp</dimen>
+ <dimen name="calm_mode_padding_horizontal">12dp</dimen>
<dimen name="calm_mode_icon_size_regular">32dp</dimen>
<dimen name="calm_mode_icon_size_small">16dp</dimen>
- <dimen name="calm_mode_icon_margin">4dp</dimen>
+ <dimen name="calm_mode_icon_margin">24dp</dimen>
<dimen name="calm_mode_text_size">24sp</dimen>
+ <dimen name="calm_mode_clock_translationY">40dp</dimen>
+ <dimen name="calm_mode_clock_media_title_margin">-16dp</dimen>
+
+ <!-- Home fullscreen media card dimens -->
+ <dimen name="media_card_large_button_icon_padding">25dp</dimen>
+ <dimen name="media_card_panel_button_icon_padding">12dp</dimen>
+ <dimen name="media_card_app_icon_size">32dp</dimen>
+ <dimen name="media_card_horizontal_margin">32dp</dimen>
+ <dimen name="media_card_view_separation_margin">16dp</dimen>
+ <dimen name="media_card_artist_top_margin">4dp</dimen>
+ <dimen name="media_card_album_art_size">200dp</dimen>
+ <dimen name="media_card_album_art_end_margin">96dp</dimen>
+ <item name="media_card_album_art_drawable_corner_ratio" format="float" type="dimen">0.08</item>
+ <dimen name="media_card_logo_size">32dp</dimen>
+ <dimen name="media_card_small_button_size">40dp</dimen>
+ <dimen name="media_card_large_button_size">80dp</dimen>
+ <dimen name="media_card_bottom_panel_button_size">56dp</dimen>
+ <dimen name="media_card_bottom_panel_height">96dp</dimen>
+ <dimen name="media_card_card_radius">32dp</dimen>
+ <dimen name="media_card_play_button_bottom_margin">120dp</dimen>
+ <dimen name="media_card_play_button_horizontal_margin">8dp</dimen>
+ <dimen name="media_card_margin_panel_open">24dp</dimen>
+ <dimen name="media_card_bottom_panel_margin_top">208dp</dimen>
+ <dimen name="media_card_bottom_panel_animated_final_position">128dp</dimen>
+ <dimen name="media_card_panel_handlebar_offscreen_start_position">1000dp</dimen>
+ <dimen name="media_card_bottom_panel_animated_size">@dimen/media_card_bottom_panel_button_size</dimen>
+ <dimen name="media_card_bottom_panel_animated_horizontal_margin">0dp</dimen>
+ <dimen name="media_card_pill_radius">160dp</dimen>
+ <dimen name="media_card_panel_handlebar_height">8dp</dimen>
+ <dimen name="media_card_panel_handlebar_touch_target_height">60dp</dimen>
+ <dimen name="media_card_panel_handlebar_horizontal_padding">164dp</dimen>
+ <dimen name="media_card_queue_header_app_icon_size">26dp</dimen>
+ <dimen name="media_card_queue_item_thumbnail_size">80dp</dimen>
+ <dimen name="media_card_panel_content_margin_top">216dp</dimen>
+ <dimen name="media_card_title_animated_line_height">36dp</dimen>
+ <dimen name="media_card_title_default_line_height">40dp</dimen>
+ <dimen name="media_card_title_animated_text_size">28sp</dimen>
+ <dimen name="media_card_title_default_text_size">32sp</dimen>
+ <dimen name="media_card_recycler_view_fading_edge_length">80dp</dimen>
+ <dimen name="media_card_history_item_height">112dp</dimen>
+ <dimen name="media_card_history_item_vertical_margin">16dp</dimen>
+ <dimen name="media_card_history_item_icon_size">80dp</dimen>
+ <dimen name="media_card_history_item_thumbnail_size">32dp</dimen>
+ <dimen name="media_card_view_header_icon_size">32dp</dimen>
+ <dimen name="media_card_text_view_guideline_start">248dp</dimen>
</resources>
diff --git a/app/res/values/integers.xml b/app/res/values/integers.xml
index bddc3f9..cbd5746 100644
--- a/app/res/values/integers.xml
+++ b/app/res/values/integers.xml
@@ -20,4 +20,11 @@
<!-- Max for seekbar progress -->
<integer name="optional_seekbar_max">1000</integer>
+
+ <!-- Calm mode animation duration in ms -->
+ <integer name="calm_mode_activity_fade_duration">500</integer>
+ <integer name="calm_mode_content_fade_duration">750</integer>
+
+ <!-- Fullscreen media card animation duration in ms -->
+ <integer name="media_card_bottom_panel_open_duration">400</integer>
</resources>
diff --git a/app/res/values/overlayable.xml b/app/res/values/overlayable.xml
index 8a71715..d4c72ef 100644
--- a/app/res/values/overlayable.xml
+++ b/app/res/values/overlayable.xml
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
-<!-- Copyright (C) 2023 The Android Open Source Project
+<!-- Copyright (C) 2024 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
@@ -16,7 +16,10 @@
<resources>
<overlayable name="CarLauncher">
<policy type="system|product|signature">
+ <item type="animator" name="calm_mode_enter"/>
<item type="animator" name="recents_clear_all"/>
+ <item type="anim" name="fade_in"/>
+ <item type="anim" name="fade_out"/>
<item type="array" name="config_homeCardModuleClasses"/>
<item type="array" name="config_homeCardPreferredMapActivities"/>
<item type="array" name="config_taskViewPackages"/>
@@ -35,7 +38,7 @@
<item type="bool" name="config_calmMode_showTemperature"/>
<item type="bool" name="config_enableCalmMode"/>
<item type="bool" name="config_homecard_single_line_secondary_descriptive_text"/>
- <item type="bool" name="config_useRemoteCarTaskView"/>
+ <item type="bool" name="config_launch_most_recent_task_on_recents_dismiss"/>
<item type="bool" name="show_seek_bar"/>
<item type="bool" name="use_media_source_color_for_seek_bar"/>
<item type="color" name="card_background_scrim"/>
@@ -47,6 +50,9 @@
<item type="color" name="dialer_icon_tint_state_list"/>
<item type="color" name="launcher_home_icon_color"/>
<item type="color" name="media_button_tint"/>
+ <item type="color" name="media_card_panel_button_background_tint_state_list"/>
+ <item type="color" name="media_card_panel_button_tint_state_list"/>
+ <item type="color" name="media_card_seekbar_thumb_color"/>
<item type="color" name="minimized_progress_bar_background"/>
<item type="color" name="recents_background_color"/>
<item type="color" name="seek_bar_color"/>
@@ -55,10 +61,13 @@
<item type="dimen" name="button_tap_target_icon_padding"/>
<item type="dimen" name="button_tap_target_size"/>
<item type="dimen" name="button_trio_margin"/>
+ <item type="dimen" name="calm_mode_clock_media_title_margin"/>
+ <item type="dimen" name="calm_mode_clock_translationY"/>
<item type="dimen" name="calm_mode_icon_margin"/>
<item type="dimen" name="calm_mode_icon_size_regular"/>
<item type="dimen" name="calm_mode_icon_size_small"/>
- <item type="dimen" name="calm_mode_padding"/>
+ <item type="dimen" name="calm_mode_padding_horizontal"/>
+ <item type="dimen" name="calm_mode_padding_vertical"/>
<item type="dimen" name="calm_mode_text_size"/>
<item type="dimen" name="card_background_image_blur_radius"/>
<item type="dimen" name="card_background_scrim_alpha"/>
@@ -78,6 +87,47 @@
<item type="dimen" name="horizontal_border_size"/>
<item type="dimen" name="launcher_card_corner_radius"/>
<item type="dimen" name="main_screen_widget_margin"/>
+ <item type="dimen" name="media_card_album_art_drawable_corner_ratio"/>
+ <item type="dimen" name="media_card_album_art_end_margin"/>
+ <item type="dimen" name="media_card_album_art_size"/>
+ <item type="dimen" name="media_card_app_icon_size"/>
+ <item type="dimen" name="media_card_artist_top_margin"/>
+ <item type="dimen" name="media_card_bottom_panel_animated_final_position"/>
+ <item type="dimen" name="media_card_bottom_panel_animated_horizontal_margin"/>
+ <item type="dimen" name="media_card_bottom_panel_animated_size"/>
+ <item type="dimen" name="media_card_bottom_panel_button_size"/>
+ <item type="dimen" name="media_card_bottom_panel_height"/>
+ <item type="dimen" name="media_card_bottom_panel_margin_top"/>
+ <item type="dimen" name="media_card_card_radius"/>
+ <item type="dimen" name="media_card_history_item_height"/>
+ <item type="dimen" name="media_card_history_item_icon_size"/>
+ <item type="dimen" name="media_card_history_item_thumbnail_size"/>
+ <item type="dimen" name="media_card_history_item_vertical_margin"/>
+ <item type="dimen" name="media_card_horizontal_margin"/>
+ <item type="dimen" name="media_card_large_button_icon_padding"/>
+ <item type="dimen" name="media_card_large_button_size"/>
+ <item type="dimen" name="media_card_logo_size"/>
+ <item type="dimen" name="media_card_margin_panel_open"/>
+ <item type="dimen" name="media_card_panel_button_icon_padding"/>
+ <item type="dimen" name="media_card_panel_content_margin_top"/>
+ <item type="dimen" name="media_card_panel_handlebar_height"/>
+ <item type="dimen" name="media_card_panel_handlebar_horizontal_padding"/>
+ <item type="dimen" name="media_card_panel_handlebar_offscreen_start_position"/>
+ <item type="dimen" name="media_card_panel_handlebar_touch_target_height"/>
+ <item type="dimen" name="media_card_pill_radius"/>
+ <item type="dimen" name="media_card_play_button_bottom_margin"/>
+ <item type="dimen" name="media_card_play_button_horizontal_margin"/>
+ <item type="dimen" name="media_card_queue_header_app_icon_size"/>
+ <item type="dimen" name="media_card_queue_item_thumbnail_size"/>
+ <item type="dimen" name="media_card_recycler_view_fading_edge_length"/>
+ <item type="dimen" name="media_card_small_button_size"/>
+ <item type="dimen" name="media_card_text_view_guideline_start"/>
+ <item type="dimen" name="media_card_title_animated_line_height"/>
+ <item type="dimen" name="media_card_title_animated_text_size"/>
+ <item type="dimen" name="media_card_title_default_line_height"/>
+ <item type="dimen" name="media_card_title_default_text_size"/>
+ <item type="dimen" name="media_card_view_header_icon_size"/>
+ <item type="dimen" name="media_card_view_separation_margin"/>
<item type="dimen" name="media_top_margin"/>
<item type="dimen" name="playback_controls_margin"/>
<item type="dimen" name="recent_task_col_space"/>
@@ -91,33 +141,50 @@
<item type="dimen" name="tap_text_margin"/>
<item type="dimen" name="text_block_top_margin"/>
<item type="dimen" name="vertical_border_size"/>
+ <item type="drawable" name="button_ripple"/>
<item type="drawable" name="car_button_background"/>
+ <item type="drawable" name="circle_button_background"/>
<item type="drawable" name="control_bar_contact_image_background"/>
<item type="drawable" name="control_bar_image_background"/>
<item type="drawable" name="default_audio_background"/>
<item type="drawable" name="dialer_button_active_state_circle"/>
+ <item type="drawable" name="divider"/>
+ <item type="drawable" name="empty_action_drawable"/>
<item type="drawable" name="ic_apps_black"/>
<item type="drawable" name="ic_arrow_back_black"/>
<item type="drawable" name="ic_call_end"/>
<item type="drawable" name="ic_call_end_button"/>
<item type="drawable" name="ic_clear_black"/>
<item type="drawable" name="ic_dialpad"/>
+ <item type="drawable" name="ic_history"/>
<item type="drawable" name="ic_launcher_home"/>
<item type="drawable" name="ic_media"/>
<item type="drawable" name="ic_mic_on"/>
<item type="drawable" name="ic_mute_activatable"/>
<item type="drawable" name="ic_navigation"/>
+ <item type="drawable" name="ic_overflow_horizontal"/>
+ <item type="drawable" name="ic_play_pause_selector"/>
+ <item type="drawable" name="ic_queue"/>
<item type="drawable" name="ic_recent_dismiss"/>
<item type="drawable" name="ic_search_black"/>
<item type="drawable" name="ic_temperature"/>
+ <item type="drawable" name="media_card_button_panel_background"/>
+ <item type="drawable" name="media_card_panel_button_shape"/>
+ <item type="drawable" name="media_card_panel_handlebar"/>
+ <item type="drawable" name="media_card_seekbar_progress"/>
+ <item type="drawable" name="media_card_seekbar_thumb"/>
+ <item type="drawable" name="pill_button_shape"/>
+ <item type="drawable" name="radius_16_background"/>
+ <item type="drawable" name="radius_8_background"/>
<item type="drawable" name="recent_clear_all_button_background"/>
<item type="drawable" name="recent_dismiss_button_background"/>
- <item type="id" name="barrier"/>
+ <item type="id" name="album_art"/>
<item type="id" name="bottom_card"/>
<item type="id" name="bottom_edge"/>
<item type="id" name="bottom_line"/>
<item type="id" name="button_center"/>
<item type="id" name="button_left"/>
+ <item type="id" name="button_panel_background"/>
<item type="id" name="button_right"/>
<item type="id" name="button_trio"/>
<item type="id" name="calm_mode_container"/>
@@ -128,22 +195,42 @@
<item type="id" name="card_name"/>
<item type="id" name="card_view"/>
<item type="id" name="clock"/>
+ <item type="id" name="content_format"/>
<item type="id" name="date"/>
- <item type="id" name="date_and_temperature_container"/>
<item type="id" name="descriptive_text_layout"/>
<item type="id" name="descriptive_text_with_controls_layout"/>
<item type="id" name="divider_horizontal"/>
+ <item type="id" name="empty_panel"/>
<item type="id" name="empty_state"/>
<item type="id" name="end_edge"/>
<item type="id" name="fragment_container_view"/>
+ <item type="id" name="guideline"/>
+ <item type="id" name="header_app_icon"/>
+ <item type="id" name="history_button"/>
+ <item type="id" name="history_card_album_art"/>
+ <item type="id" name="history_card_app_thumbnail"/>
+ <item type="id" name="history_card_app_title_inactive"/>
+ <item type="id" name="history_card_container_active"/>
+ <item type="id" name="history_card_container_inactive"/>
+ <item type="id" name="history_card_header_icon"/>
+ <item type="id" name="history_card_history_header_title_view"/>
+ <item type="id" name="history_card_subtitle_active"/>
+ <item type="id" name="history_card_title_active"/>
+ <item type="id" name="history_item_app_icon_inactive"/>
+ <item type="id" name="history_list"/>
+ <item type="id" name="history_list_container"/>
+ <item type="id" name="in_call_fragment_container"/>
<item type="id" name="maps_card"/>
+ <item type="id" name="media_card_panel_content_container"/>
+ <item type="id" name="media_card_panel_handlebar"/>
+ <item type="id" name="media_card_queue_header_title_view"/>
<item type="id" name="media_descriptive_text"/>
- <item type="id" name="media_group"/>
- <item type="id" name="media_icon"/>
+ <item type="id" name="media_fragment_container"/>
<item type="id" name="media_layout"/>
<item type="id" name="media_playback_controls_bar"/>
<item type="id" name="media_title"/>
- <item type="id" name="nav_group"/>
+ <item type="id" name="media_widget_app_icon"/>
+ <item type="id" name="motion_layout"/>
<item type="id" name="nav_state"/>
<item type="id" name="nav_state_icon"/>
<item type="id" name="optional_image"/>
@@ -153,32 +240,56 @@
<item type="id" name="optional_timer"/>
<item type="id" name="optional_timer_separator"/>
<item type="id" name="optional_times"/>
+ <item type="id" name="overflow_button"/>
+ <item type="id" name="overflow_grid"/>
+ <item type="id" name="play_pause_button"/>
+ <item type="id" name="playback_action_id1"/>
+ <item type="id" name="playback_action_id10"/>
+ <item type="id" name="playback_action_id2"/>
+ <item type="id" name="playback_action_id3"/>
+ <item type="id" name="playback_action_id4"/>
+ <item type="id" name="playback_action_id5"/>
+ <item type="id" name="playback_action_id6"/>
+ <item type="id" name="playback_action_id7"/>
+ <item type="id" name="playback_action_id8"/>
+ <item type="id" name="playback_action_id9"/>
+ <item type="id" name="playback_seek_bar"/>
<item type="id" name="primary_text"/>
+ <item type="id" name="queue_button"/>
+ <item type="id" name="queue_list"/>
+ <item type="id" name="queue_list_container"/>
+ <item type="id" name="queue_list_item_subtitle"/>
+ <item type="id" name="queue_list_item_title"/>
<item type="id" name="recent_tasks_group"/>
<item type="id" name="recent_tasks_list"/>
<item type="id" name="recent_tasks_list_focus_area"/>
<item type="id" name="recents_clear_all_button"/>
<item type="id" name="secondary_text"/>
<item type="id" name="start_edge"/>
+ <item type="id" name="subtitle"/>
<item type="id" name="tap_for_more_text"/>
<item type="id" name="task_dismiss_button"/>
<item type="id" name="task_icon"/>
<item type="id" name="task_thumbnail"/>
<item type="id" name="temperature"/>
- <item type="id" name="temperature_group"/>
<item type="id" name="temperature_icon"/>
<item type="id" name="text_block"/>
<item type="id" name="text_block_layout"/>
+ <item type="id" name="thumbnail"/>
+ <item type="id" name="title"/>
<item type="id" name="top_card"/>
<item type="id" name="top_edge"/>
<item type="id" name="top_line"/>
<item type="id" name="vertical_barrier"/>
+ <item type="id" name="view_pager"/>
+ <item type="integer" name="calm_mode_activity_fade_duration"/>
+ <item type="integer" name="calm_mode_content_fade_duration"/>
<item type="integer" name="card_content_text_block_max_lines"/>
+ <item type="integer" name="media_card_bottom_panel_open_duration"/>
<item type="integer" name="optional_seekbar_max"/>
<item type="integer" name="playback_controls_bar_columns"/>
<item type="layout" name="button_trio"/>
<item type="layout" name="calm_mode_activity"/>
- <item type="layout" name="calm_mode_date_and_temperature"/>
<item type="layout" name="calm_mode_fragment"/>
<item type="layout" name="car_launcher"/>
<item type="layout" name="car_launcher_multiwindow"/>
@@ -187,8 +298,15 @@
<item type="layout" name="card_content_media"/>
<item type="layout" name="card_content_text_block"/>
<item type="layout" name="card_fragment"/>
+ <item type="layout" name="card_fragment_audio_card"/>
<item type="layout" name="control_bar_container"/>
<item type="layout" name="descriptive_text"/>
+ <item type="layout" name="media_card_fullscreen"/>
+ <item type="layout" name="media_card_history_header_item"/>
+ <item type="layout" name="media_card_history_item"/>
+ <item type="layout" name="media_card_panel_content_item"/>
+ <item type="layout" name="media_card_queue_header_item"/>
+ <item type="layout" name="media_card_queue_item"/>
<item type="layout" name="optional_seek_bar_with_times"/>
<item type="layout" name="recent_clear_all_view"/>
<item type="layout" name="recent_task_view"/>
@@ -198,10 +316,9 @@
<item type="layout" name="tap_for_more_text"/>
<item type="layout" name="text_block"/>
<item type="string" name="app_title"/>
+ <item type="string" name="calm_mode_separator"/>
<item type="string" name="calm_mode_title"/>
- <item type="string" name="config_calmMode_activityName"/>
- <item type="string" name="config_calmMode_packageName"/>
- <item type="string" name="config_passengerLauncherComponent"/>
+ <item type="string" name="config_calmMode_componentName"/>
<item type="string" name="config_smallCanvasOptimizedMapIntent"/>
<item type="string" name="config_tosMapIntent"/>
<item type="string" name="default_media_song_title"/>
@@ -209,6 +326,8 @@
<item type="string" name="failure_opening_recent_task_message"/>
<item type="string" name="fake_weather_footer_text"/>
<item type="string" name="fake_weather_main_text"/>
+ <item type="string" name="media_card_history_header_title"/>
+ <item type="string" name="media_card_queue_header_title"/>
<item type="string" name="ongoing_call_duration_text_separator"/>
<item type="string" name="ongoing_call_text"/>
<item type="string" name="projected_launch_text"/>
@@ -220,18 +339,19 @@
<item type="string" name="tap_to_launch_text"/>
<item type="string" name="times_separator"/>
<item type="string" name="weather_app_name"/>
- <item type="style" name="CalmMode"/>
- <item type="style" name="CalmMode.BackgroundImage"/>
- <item type="style" name="CalmMode.Icon"/>
- <item type="style" name="CalmMode.Icon.Temperature"/>
- <item type="style" name="CalmMode.Text"/>
- <item type="style" name="CalmMode.Text.Clock"/>
- <item type="style" name="CalmMode.Text.Date"/>
- <item type="style" name="CalmMode.Text.MediaTitle"/>
+ <item type="style" name="CalmModeBackgroundImage"/>
+ <item type="style" name="CalmModeClock"/>
+ <item type="style" name="CalmModeIcon"/>
+ <item type="style" name="CalmModeText"/>
+ <item type="style" name="CalmModeText.Date"/>
+ <item type="style" name="CalmModeText.MediaTitle"/>
+ <item type="style" name="CalmModeText.Temperature"/>
<item type="style" name="CardViewStyle"/>
<item type="style" name="ClearAllRecentTasksButton"/>
<item type="style" name="ContextualSpace"/>
<item type="style" name="HiddenRecentTaskThumbnail"/>
+ <item type="style" name="MediaCardCustomActionButtonStyle"/>
+ <item type="style" name="MediaCardPanelButtonStyle"/>
<item type="style" name="RecentTaskDismissButton"/>
<item type="style" name="RecentTaskIcon"/>
<item type="style" name="RecentTaskThumbnail"/>
@@ -241,6 +361,7 @@
<item type="style" name="Theme.CalmMode"/>
<item type="style" name="Theme.Launcher"/>
<item type="style" name="TitleText"/>
+ <item type="xml" name="panel_animation_motion_scene"/>
</policy>
</overlayable>
</resources>
diff --git a/app/res/values/strings.xml b/app/res/values/strings.xml
index 2ec8f42..bcb5546 100644
--- a/app/res/values/strings.xml
+++ b/app/res/values/strings.xml
@@ -56,4 +56,9 @@
<!-- Calm mode strings -->
<string name="calm_mode_title">Calm mode</string>
+ <string name="calm_mode_separator" translatable="false">\u0020\u0020\u0020\u2022\u0020\u0020\u0020</string>
+
+ <!-- Fullscreen media card strings -->
+ <string name="media_card_queue_header_title">Queue</string>
+ <string name="media_card_history_header_title">Media Source</string>
</resources>
diff --git a/app/res/values/styles.xml b/app/res/values/styles.xml
index c0ea7b3..de9c13f 100644
--- a/app/res/values/styles.xml
+++ b/app/res/values/styles.xml
@@ -86,55 +86,65 @@
<item name="cornerSize">8dp</item>
</style>
- <style name="CalmMode">
- </style>
-
- <style name="CalmMode.Text" parent="CalmMode">
+ <style name="CalmModeText" parent="TextAppearance.Car.Body.Small">
<item name="android:textColor">@android:color/white</item>
- <item name="android:textSize">@dimen/calm_mode_text_size</item>
- <item name="android:minHeight">@dimen/calm_mode_icon_size_regular</item>
</style>
- <style name="CalmMode.Icon" parent="CalmMode">
- <item name="android:layout_width">@dimen/calm_mode_icon_size_regular</item>
- <item name="android:layout_height">@dimen/calm_mode_icon_size_regular</item>
+ <style name="CalmModeIcon">
<item name="android:scaleType">fitCenter</item>
<item name="android:tint">@android:color/white</item>
</style>
- <style name="CalmMode.BackgroundImage" parent="CalmMode">
+ <style name="CalmModeBackgroundImage">
<item name="android:src">@android:color/black</item>
<item name="android:scaleType">centerCrop</item>
</style>
- <style name="CalmMode.Text.Date" parent="CalmMode.Text">
+ <style name="CalmModeText.Date">
+ <item name="android:textColor">@color/car_outline</item>
<item name="android:format12Hour">EE, MMM dd</item>
<item name="android:format24Hour">EE, MMM dd</item>
</style>
- <style name="CalmMode.Text.MediaTitle" parent="CalmMode.Text">
+ <style name="CalmModeText.MediaTitle">
+ <item name="android:textColor">@color/car_outline</item>
<item name="android:ellipsize">end</item>
- <item name="android:maxWidth">300dp</item>
+ <item name="android:maxWidth">1024dp</item>
<item name="android:maxLines">2</item>
- <item name="android:textAlignment">viewStart</item>
- </style>
-
- <style name="CalmMode.Text.Clock" parent="CalmMode.Text">
- <item name="android:gravity">center</item>
<item name="android:textAlignment">center</item>
- <item name="android:textSize">200sp</item>
- <item name="android:lineSpacingMultiplier">0.75</item>
- <item name="android:format12Hour">hh\nmm</item>
- <item name="android:format24Hour">HH\nmm</item>
- <item name="android:letterSpacing">0.02</item>
- <item name="android:lineSpacingExtra">-10sp</item>
- <item name="android:fontVariationSettings">"'wght' 100, 'wdth' 10"</item>
- <item name="android:fontFamily">roboto-flex</item>
</style>
- <style name="CalmMode.Icon.Temperature" parent="CalmMode.Icon">
- <item name="android:layout_width">@dimen/calm_mode_icon_size_small</item>
- <item name="android:layout_height">@dimen/calm_mode_icon_size_small</item>
+ <style name="CalmModeText.Temperature">
+ <item name="android:textColor">@color/car_outline</item>
+ </style>
+
+ <style name="CalmModeClock" parent="TextAppearance.Car.Display">
+ <item name="android:fontFamily">roboto-flex</item>
+ <item name="android:textFontWeight">300</item>
+ <item name="android:fontVariationSettings">"'wght' 300, 'wdth' 100, 'xtra' 468, 'xopq' 96, 'yopq' 79, 'ytuc' 712, 'ytas' 750, 'ytde' -203, 'ytfi' 738, 'opsz' 144"</item>
+ <item name="android:textColor">@color/car_neutral_90</item>
+ <item name="android:textSize">300sp</item>
+ <item name="android:textAlignment">center</item>
+ <item name="android:format12Hour">hh:mm</item>
+ <item name="android:format24Hour">HH:mm</item>
+ <item name="android:includeFontPadding">false</item>
+ <item name="android:lineSpacingMultiplier">1</item>
+ <item name="android:lineSpacingExtra">0pt</item>
+
+ </style>
+
+ <style name="MediaCardPanelButtonStyle">
+ <item name="android:scaleType">centerInside</item>
+ <item name="android:padding">@dimen/media_card_panel_button_icon_padding</item>
+ <item name="android:cropToPadding">true</item>
+ <item name="android:tint">@color/media_card_panel_button_tint_state_list</item>
+ <item name="android:background">@drawable/media_card_panel_button_shape</item>
+ </style>
+
+ <style name="MediaCardCustomActionButtonStyle">
+ <item name="android:scaleType">fitCenter</item>
+ <item name="android:tint">@color/car_on_surface</item>
+ <item name="android:background">@android:color/transparent</item>
</style>
</resources>
diff --git a/app/res/values/themes.xml b/app/res/values/themes.xml
index 06183e3..0ce0fd4 100644
--- a/app/res/values/themes.xml
+++ b/app/res/values/themes.xml
@@ -23,8 +23,10 @@
<style name="Theme.CalmMode" parent="Theme.CarUi.NoToolbar">
<item name="android:windowNoTitle">true</item>
- <item name="android:windowFullscreen">true</item>
+ <item name="android:windowIsTranslucent">true</item>
<item name="android:windowSplashScreenAnimatedIcon">@android:color/transparent</item>
<item name="android:windowBackground">@android:color/black</item>
+ <item name="android:activityOpenEnterAnimation">@anim/fade_in</item>
+ <item name="android:activityOpenExitAnimation">@anim/fade_out</item>
</style>
</resources>
diff --git a/app/res/xml/panel_animation_motion_scene.xml b/app/res/xml/panel_animation_motion_scene.xml
new file mode 100644
index 0000000..2ef5eb4
--- /dev/null
+++ b/app/res/xml/panel_animation_motion_scene.xml
@@ -0,0 +1,256 @@
+<!--
+ ~ Copyright (C) 2024 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 motionlicable 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.
+ -->
+
+<MotionScene
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:motion="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- layoutDuringTransition respects queue RecyclerView scroll animations -->
+ <Transition
+ motion:constraintSetEnd="@id/end"
+ motion:constraintSetStart="@id/start"
+ motion:duration="@integer/media_card_bottom_panel_open_duration"
+ motion:motionInterpolator="standard"
+ motion:layoutDuringTransition="honorRequest">
+ <KeyFrameSet>
+ <KeyAttribute
+ android:alpha="1"
+ motion:framePosition="0"
+ motion:motionTarget="@id/playback_action_id1" />
+ <KeyAttribute
+ android:alpha="0"
+ motion:framePosition="60"
+ motion:motionTarget="@id/playback_action_id1" />
+ <KeyAttribute
+ android:alpha="1"
+ motion:framePosition="0"
+ motion:motionTarget="@id/playback_action_id2" />
+ <KeyAttribute
+ android:alpha="0"
+ motion:framePosition="60"
+ motion:motionTarget="@id/playback_action_id2" />
+ <KeyAttribute
+ android:alpha="0"
+ motion:framePosition="40"
+ motion:motionTarget="@id/button_panel_background" />
+ <KeyAttribute
+ android:alpha="0"
+ motion:framePosition="60"
+ motion:motionTarget="@id/button_panel_background" />
+ <KeyTrigger
+ motion:framePosition="30"
+ motion:motionTarget="@id/button_panel_background"
+ motion:onPositiveCross="."
+ tools:ignore="MotionSceneFileValidationError">
+ <CustomMethod motion:methodName="setEnabled" motion:customBoolean="true"/>
+ </KeyTrigger>
+ <KeyTrigger
+ motion:framePosition="40"
+ motion:motionTarget="@id/button_panel_background"
+ motion:onNegativeCross="."
+ tools:ignore="MotionSceneFileValidationError">
+ <CustomMethod motion:methodName="setEnabled" motion:customBoolean="false"/>
+ </KeyTrigger>
+ <KeyTrigger
+ motion:framePosition="30"
+ motion:motionTarget="@id/playback_seek_bar"
+ motion:onPositiveCross="."
+ tools:ignore="MotionSceneFileValidationError">
+ <CustomMethod motion:methodName="setSelected" motion:customBoolean="true"/>
+ </KeyTrigger>
+ <KeyTrigger
+ motion:framePosition="40"
+ motion:motionTarget="@id/playback_seek_bar"
+ motion:onNegativeCross="."
+ tools:ignore="MotionSceneFileValidationError">
+ <CustomMethod motion:methodName="setSelected" motion:customBoolean="false"/>
+ </KeyTrigger>
+ </KeyFrameSet>
+ </Transition>
+
+ <ConstraintSet
+ android:id="@+id/start">
+ <ConstraintOverride
+ android:id="@id/media_widget_app_icon">
+ <PropertySet
+ motion:visibilityMode="ignore"/>
+ </ConstraintOverride>
+ <ConstraintOverride
+ android:id="@id/title">
+ <PropertySet
+ motion:visibilityMode="ignore"/>
+ <CustomAttribute
+ motion:attributeName="lineHeight"
+ motion:customDimension="@dimen/media_card_title_default_line_height" />
+ <CustomAttribute
+ motion:attributeName="textSize"
+ motion:customDimension="@dimen/media_card_title_default_text_size" />
+ </ConstraintOverride>
+ <ConstraintOverride
+ android:id="@id/subtitle">
+ <PropertySet
+ motion:visibilityMode="ignore"/>
+ </ConstraintOverride>
+ <ConstraintOverride
+ android:id="@id/content_format">
+ <PropertySet
+ motion:visibilityMode="ignore"/>
+ </ConstraintOverride>
+ <ConstraintOverride
+ android:id="@id/playback_action_id1">
+ <PropertySet
+ motion:visibilityMode="ignore"/>
+ </ConstraintOverride>
+ <ConstraintOverride
+ android:id="@id/playback_action_id2">
+ <PropertySet
+ motion:visibilityMode="ignore"/>
+ </ConstraintOverride>
+ <ConstraintOverride
+ android:id="@id/album_art">
+ <PropertySet
+ motion:visibilityMode="ignore"/>
+ </ConstraintOverride>
+ <ConstraintOverride
+ android:id="@id/playback_seek_bar">
+ <PropertySet
+ motion:visibilityMode="ignore"/>
+ </ConstraintOverride>
+ </ConstraintSet>
+
+ <ConstraintSet
+ android:id="@+id/end">
+ <Constraint
+ android:id="@+id/play_pause_button"
+ android:layout_width="@dimen/media_card_large_button_size"
+ android:layout_height="@dimen/media_card_large_button_size"
+ android:src="@drawable/ic_play_pause_selector"
+ android:scaleType="center"
+ android:tint="@color/car_surface_container_high"
+ android:background="@drawable/pill_button_shape"
+ android:backgroundTint="@color/car_primary"
+ android:layout_marginStart="@dimen/media_card_horizontal_margin"
+ android:layout_marginTop="@dimen/media_card_margin_panel_open"
+ motion:layout_constraintStart_toStartOf="parent"
+ motion:layout_constraintTop_toTopOf="parent">
+ </Constraint>
+ <Constraint
+ android:id="@+id/button_panel_background"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/media_card_bottom_panel_animated_size"
+ android:background="@drawable/media_card_button_panel_background"
+ android:backgroundTint="@color/car_surface_container_highest"
+ android:layout_marginStart="@dimen/media_card_horizontal_margin"
+ android:layout_marginEnd="@dimen/media_card_horizontal_margin"
+ android:layout_marginTop="@dimen/media_card_margin_panel_open"
+ motion:layout_constraintStart_toStartOf="parent"
+ motion:layout_constraintEnd_toEndOf="parent"
+ motion:layout_constraintTop_toBottomOf="@id/play_pause_button">
+ </Constraint>
+ <Constraint
+ android:id="@+id/empty_panel"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:background="@color/car_surface_container_high"
+ motion:layout_constraintTop_toTopOf="parent">
+ </Constraint>
+ <ConstraintOverride
+ android:id="@id/media_widget_app_icon"
+ android:alpha="0">
+ <PropertySet
+ motion:visibilityMode="ignore"/>
+ </ConstraintOverride>
+ <Constraint
+ android:id="@+id/title"
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:text="@string/metadata_default_title"
+ android:textColor="@color/car_text_primary"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:layout_marginStart="@dimen/media_card_margin_panel_open"
+ android:layout_marginTop="@dimen/media_card_view_separation_margin"
+ android:layout_marginEnd="@dimen/media_card_horizontal_margin"
+ motion:layout_constraintStart_toEndOf="@id/play_pause_button"
+ motion:layout_constraintEnd_toEndOf="parent"
+ motion:layout_constraintTop_toTopOf="@id/play_pause_button"
+ motion:layout_constraintBottom_toTopOf="@id/playback_seek_bar"
+ motion:layout_constraintVertical_bias="0">
+ <PropertySet
+ motion:visibilityMode="ignore"/>
+ <CustomAttribute
+ motion:attributeName="lineHeight"
+ motion:customDimension="@dimen/media_card_title_animated_line_height" />
+ <CustomAttribute
+ motion:attributeName="textSize"
+ motion:customDimension="@dimen/media_card_title_animated_text_size" />
+ </Constraint>
+ <ConstraintOverride
+ android:id="@id/subtitle"
+ android:alpha="0">
+ <PropertySet
+ motion:visibilityMode="ignore"/>
+ </ConstraintOverride>
+ <ConstraintOverride
+ android:id="@id/content_format"
+ android:alpha="0">
+ <PropertySet
+ motion:visibilityMode="ignore"/>
+ </ConstraintOverride>
+ <Constraint
+ android:id="@+id/playback_seek_bar"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:clickable="false"
+ android:paddingEnd="0dp"
+ android:paddingStart="0dp"
+ android:progressBackgroundTint="@color/car_surface_container_highest"
+ android:progressDrawable="@drawable/media_card_seekbar_progress"
+ android:progressTint="@color/car_primary"
+ android:splitTrack="false"
+ android:thumb="@drawable/media_card_seekbar_thumb"
+ android:thumbTint="@color/car_on_surface"
+ android:thumbOffset="0px"
+ android:layout_marginStart="@dimen/media_card_margin_panel_open"
+ android:layout_marginEnd="@dimen/media_card_horizontal_margin"
+ motion:layout_constraintStart_toEndOf="@id/play_pause_button"
+ motion:layout_constraintEnd_toEndOf="parent"
+ motion:layout_constraintBottom_toBottomOf="@id/play_pause_button"
+ motion:layout_constraintTop_toBottomOf="@id/title">
+ <PropertySet
+ motion:visibilityMode="ignore"/>
+ </Constraint>
+ <ConstraintOverride
+ android:id="@id/playback_action_id1"
+ android:alpha="0">
+ <PropertySet
+ motion:visibilityMode="ignore"/>
+ </ConstraintOverride>
+ <ConstraintOverride
+ android:id="@id/playback_action_id2"
+ android:alpha="0">
+ <PropertySet
+ motion:visibilityMode="ignore"/>
+ </ConstraintOverride>
+ <ConstraintOverride
+ android:id="@id/album_art"
+ android:alpha="0">
+ <PropertySet
+ motion:visibilityMode="ignore"/>
+ </ConstraintOverride>
+ </ConstraintSet>
+</MotionScene>
diff --git a/app/src/com/android/car/carlauncher/CarFullscreenTaskMonitorListener.java b/app/src/com/android/car/carlauncher/CarFullscreenTaskMonitorListener.java
deleted file mode 100644
index 2ac0eed..0000000
--- a/app/src/com/android/car/carlauncher/CarFullscreenTaskMonitorListener.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2022 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.carlauncher;
-
-import android.app.ActivityManager;
-import android.car.app.CarActivityManager;
-import android.util.Log;
-import android.view.SurfaceControl;
-
-import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.fullscreen.FullscreenTaskListener;
-
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * The Car version of FullscreenTaskListener, which reports Task lifecycle to CarService.
- */
-public class CarFullscreenTaskMonitorListener extends FullscreenTaskListener {
- private static final String TAG = CarFullscreenTaskMonitorListener.class.getSimpleName();
- private final AtomicReference<CarActivityManager> mCarActivityManagerRef;
-
- public CarFullscreenTaskMonitorListener(
- AtomicReference<CarActivityManager> carActivityManagerRef,
- SyncTransactionQueue syncQueue) {
- super(syncQueue);
- mCarActivityManagerRef = carActivityManagerRef;
- }
- @Override
- public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo,
- SurfaceControl leash) {
- super.onTaskAppeared(taskInfo, leash);
- CarActivityManager carAM = mCarActivityManagerRef.get();
- if (carAM != null) {
- carAM.onTaskAppeared(taskInfo, leash);
- } else {
- Log.w(TAG, "CarActivityManager is null, skip onTaskAppeared: taskInfo=" + taskInfo);
- }
- }
-
- @Override
- public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
- super.onTaskInfoChanged(taskInfo);
- CarActivityManager carAM = mCarActivityManagerRef.get();
- if (carAM != null) {
- carAM.onTaskInfoChanged(taskInfo);
- } else {
- Log.w(TAG, "CarActivityManager is null, skip onTaskInfoChanged: taskInfo=" + taskInfo);
- }
- }
-
- @Override
- public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
- super.onTaskVanished(taskInfo);
- CarActivityManager carAM = mCarActivityManagerRef.get();
- if (carAM != null) {
- carAM.onTaskVanished(taskInfo);
- } else {
- Log.w(TAG, "CarActivityManager is null, skip onTaskVanished: taskInfo=" + taskInfo);
- }
- }
-}
diff --git a/app/src/com/android/car/carlauncher/CarLauncher.java b/app/src/com/android/car/carlauncher/CarLauncher.java
index c686584..d256a05 100644
--- a/app/src/com/android/car/carlauncher/CarLauncher.java
+++ b/app/src/com/android/car/carlauncher/CarLauncher.java
@@ -17,17 +17,16 @@
package com.android.car.carlauncher;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-import static android.car.settings.CarSettings.Secure.KEY_USER_TOS_ACCEPTED;
+import static android.car.settings.CarSettings.Secure.KEY_UNACCEPTED_TOS_DISABLED_APPS;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
+import static com.android.car.carlauncher.AppGridFragment.Mode.ALL_APPS;
import static com.android.car.carlauncher.CarLauncherViewModel.CarLauncherViewModelFactory;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.TaskStackListener;
import android.car.Car;
-import android.car.user.CarUserManager;
-import android.content.ComponentName;
import android.content.Intent;
import android.content.res.Configuration;
import android.database.ContentObserver;
@@ -37,6 +36,7 @@
import android.provider.Settings;
import android.util.Log;
import android.view.Display;
+import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
@@ -46,6 +46,8 @@
import androidx.lifecycle.ViewModelProvider;
import com.android.car.carlauncher.homescreen.HomeCardModule;
+import com.android.car.carlauncher.homescreen.audio.IntentHandler;
+import com.android.car.carlauncher.homescreen.audio.media.MediaIntentRouter;
import com.android.car.carlauncher.taskstack.TaskStackChangeListeners;
import com.android.car.internal.common.UserHelperLite;
import com.android.wm.shell.taskview.TaskView;
@@ -72,21 +74,18 @@
public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private ActivityManager mActivityManager;
- private TaskViewManager mTaskViewManager;
-
private Car mCar;
- private CarTaskView mTaskView;
private int mCarLauncherTaskId = INVALID_TASK_ID;
private Set<HomeCardModule> mHomeCardModules;
/** Set to {@code true} once we've logged that the Activity is fully drawn. */
private boolean mIsReadyLogged;
private boolean mUseSmallCanvasOptimizedMap;
- private boolean mUseRemoteCarTaskView;
private ViewGroup mMapsCard;
- private CarLauncherViewModel mCarLauncherViewModel;
@VisibleForTesting
+ CarLauncherViewModel mCarLauncherViewModel;
+ @VisibleForTesting
ContentObserver mTosContentObserver;
private final TaskStackListener mTaskStackListener = new TaskStackListener() {
@@ -107,20 +106,19 @@
// The embedded map component received an intent, therefore forcibly bringing the
// launcher to the foreground.
bringToForeground();
- return;
}
}
};
- @VisibleForTesting
- void setCarUserManager(CarUserManager carUserManager) {
- if (mTaskViewManager == null) {
- Log.w(TAG, "Task view manager is null, cannot set CarUserManager on taskview "
- + "manager");
- return;
+ private final IntentHandler mMediaIntentHandler = new IntentHandler() {
+ @Override
+ public void handleIntent(Intent intent) {
+ if (intent != null) {
+ ActivityOptions options = ActivityOptions.makeBasic();
+ startActivity(intent, options.toBundle());
+ }
}
- mTaskViewManager.setCarUserManager(carUserManager);
- }
+ };
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -129,56 +127,13 @@
if (DEBUG) {
Log.d(TAG, "onCreate(" + getUserId() + ") displayId=" + getDisplayId());
}
- // Since MUMD is introduced, CarLauncher can be called in the main display of visible users.
- // In ideal shape, CarLauncher should handle both driver and passengers together.
- // But, in the mean time, we have separate launchers for driver and passengers, so
- // CarLauncher needs to reroute the request to Passenger launcher if it is invoked from
- // the main display of passengers (not driver).
- // For MUPAND, PassengerLauncher should be the default launcher.
- // For non-main displays, ATM will invoke SECONDARY_HOME Intent, so the secondary launcher
- // should handle them.
+ // Since MUMD/MUPAND is introduced, CarLauncher can be called in the main display of
+ // visible background users.
+ // For Passenger scenarios, replace the maps_card with AppGridActivity, as currently
+ // there is no maps use-case for passengers.
UserManager um = getSystemService(UserManager.class);
boolean isPassengerDisplay = getDisplayId() != Display.DEFAULT_DISPLAY
|| um.isVisibleBackgroundUsersOnDefaultDisplaySupported();
- if (isPassengerDisplay) {
- String passengerLauncherName = getString(R.string.config_passengerLauncherComponent);
- Intent passengerHomeIntent;
- if (!passengerLauncherName.isEmpty()) {
- ComponentName component = ComponentName.unflattenFromString(passengerLauncherName);
- if (component == null) {
- throw new IllegalStateException(
- "Invalid passengerLauncher name=" + passengerLauncherName);
- }
- passengerHomeIntent = new Intent(Intent.ACTION_MAIN)
- // passenger launcher should be launched in home task in order to
- // fix TaskView layering issue
- .addCategory(Intent.CATEGORY_HOME)
- .setComponent(component);
- } else {
- // No passenger launcher is specified, then use AppsGrid as a fallback.
- passengerHomeIntent = CarLauncherUtils.getAppsGridIntent();
- }
- ActivityOptions options = ActivityOptions
- // No animation for the trampoline.
- .makeCustomAnimation(this, /* enterResId=*/ 0, /* exitResId= */ 0)
- .setLaunchDisplayId(getDisplayId());
- startActivity(passengerHomeIntent, options.toBundle());
- finish();
- return;
- }
-
- mUseSmallCanvasOptimizedMap =
- CarLauncherUtils.isSmallCanvasOptimizedMapIntentConfigured(this);
- mUseRemoteCarTaskView = getResources().getBoolean(R.bool.config_useRemoteCarTaskView);
-
- mActivityManager = getSystemService(ActivityManager.class);
- mCarLauncherTaskId = getTaskId();
- TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
-
- // Setting as trusted overlay to let touches pass through.
- getWindow().addPrivateFlags(PRIVATE_FLAG_TRUSTED_OVERLAY);
- // To pass touches to the underneath task.
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
// Don't show the maps panel in multi window mode.
// NOTE: CTS tests for split screen are not compatible with activity views on the default
@@ -187,78 +142,66 @@
setContentView(R.layout.car_launcher_multiwindow);
} else {
setContentView(R.layout.car_launcher);
- // We don't want to show Map card unnecessarily for the headless user 0.
- if (!UserHelperLite.isHeadlessSystemUser(getUserId())) {
- mMapsCard = findViewById(R.id.maps_card);
- if (mMapsCard != null) {
- if (mUseRemoteCarTaskView) {
+ // Passenger displays do not require TaskView Embedding
+ if (!isPassengerDisplay) {
+ mUseSmallCanvasOptimizedMap =
+ CarLauncherUtils.isSmallCanvasOptimizedMapIntentConfigured(this);
+
+ mActivityManager = getSystemService(ActivityManager.class);
+ mCarLauncherTaskId = getTaskId();
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(
+ mTaskStackListener);
+
+ // Setting as trusted overlay to let touches pass through.
+ getWindow().addPrivateFlags(PRIVATE_FLAG_TRUSTED_OVERLAY);
+ // To pass touches to the underneath task.
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
+ // We don't want to show Map card unnecessarily for the headless user 0
+ if (!UserHelperLite.isHeadlessSystemUser(getUserId())) {
+ mMapsCard = findViewById(R.id.maps_card);
+ if (mMapsCard != null) {
setupRemoteCarTaskView(mMapsCard);
- } else {
- setUpTaskView(mMapsCard);
}
}
+ } else {
+ // For Passenger display show the AppGridFragment in place of the Maps view.
+ // Also we can skip initializing all the TaskView related objects as they are not
+ // used in this case.
+ getSupportFragmentManager().beginTransaction().replace(R.id.maps_card,
+ AppGridFragment.newInstance(ALL_APPS)).commit();
+
}
}
+
+ MediaIntentRouter.getInstance().registerMediaIntentHandler(mMediaIntentHandler);
initializeCards();
setupContentObserversForTos();
}
private void setupRemoteCarTaskView(ViewGroup parent) {
mCarLauncherViewModel = new ViewModelProvider(this,
- new CarLauncherViewModelFactory(this, getMapsIntent()))
+ new CarLauncherViewModelFactory(this))
.get(CarLauncherViewModel.class);
+ mCarLauncherViewModel.initializeRemoteCarTaskView(getMapsIntent());
getLifecycle().addObserver(mCarLauncherViewModel);
+ addOnNewIntentListener(mCarLauncherViewModel.getNewIntentListener());
mCarLauncherViewModel.getRemoteCarTaskView().observe(this, taskView -> {
- if (taskView != null && taskView.getParent() == null) {
- parent.addView(taskView);
+ if (taskView == null || taskView.getParent() == parent) {
+ // Discard if the parent is still the same because it doesn't signify a config
+ // change.
+ return;
}
+ if (taskView.getParent() != null) {
+ // Discard the previous parent as its invalid now.
+ ((ViewGroup) taskView.getParent()).removeView(taskView);
+ }
+ parent.removeAllViews(); // Just a defense against a dirty parent.
+ parent.addView(taskView);
});
}
- private void setUpTaskView(ViewGroup parent) {
- Set<String> taskViewPackages = new ArraySet<>(getResources().getStringArray(
- R.array.config_taskViewPackages));
- mTaskViewManager = new TaskViewManager(this, getMainThreadHandler());
-
- mTaskViewManager.createControlledCarTaskView(
- getMainExecutor(),
- ControlledCarTaskViewConfig.builder()
- .setActivityIntent(getMapsIntent())
- // TODO(b/263876526): Enable auto restart after ensuring no CTS failure.
- .setAutoRestartOnCrash(false)
- .build(),
- new ControlledCarTaskViewCallbacks() {
- @Override
- public void onTaskViewCreated(CarTaskView taskView) {
- parent.addView(taskView);
- mTaskView = taskView;
- }
-
- @Override
- public void onTaskViewReady() {
- maybeLogReady();
- }
-
- @Override
- public Set<String> getDependingPackageNames() {
- return taskViewPackages;
- }
- });
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- // The TaskViewManager might have been released if the user was switched to some other user
- // and then switched back to the previous user before the previous user is stopped.
- // In such a case, the TaskViewManager should be recreated.
- if (!mUseRemoteCarTaskView && mMapsCard != null && mTaskViewManager.isReleased()) {
- setUpTaskView(mMapsCard);
- }
- }
-
@Override
protected void onResume() {
super.onResume();
@@ -269,18 +212,19 @@
protected void onDestroy() {
super.onDestroy();
TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
+ unregisterTosContentObserver();
+ release();
+ }
+
+ private void unregisterTosContentObserver() {
if (mTosContentObserver != null) {
Log.i(TAG, "Unregister content observer for tos state");
getContentResolver().unregisterContentObserver(mTosContentObserver);
mTosContentObserver = null;
}
- release();
}
private int getTaskViewTaskId() {
- if (mTaskView != null) {
- return mTaskView.getTaskId();
- }
if (mCarLauncherViewModel != null) {
return mCarLauncherViewModel.getRemoteCarTaskViewTaskId();
}
@@ -288,11 +232,13 @@
}
private void release() {
- mTaskView = null;
- // When using a ViewModel for the RemoteCarTaskViews, the task view can still be attached
- // to the mMapsCard due to which the CarLauncher activity does not get garbage collected
- // during activity recreation.
- mMapsCard = null;
+ if (mMapsCard != null) {
+ // This is important as the TaskView is preserved during config change in ViewModel and
+ // to avoid the memory leak, it should be plugged out of the View hierarchy.
+ mMapsCard.removeAllViews();
+ mMapsCard = null;
+ }
+
if (mCar != null) {
mCar.disconnect();
mCar = null;
@@ -314,6 +260,11 @@
long reflectionStartTime = System.currentTimeMillis();
HomeCardModule cardModule = (HomeCardModule)
Class.forName(providerClassName).newInstance();
+ if (Flags.mediaCardFullscreen()) {
+ if (cardModule.getCardResId() == R.id.top_card) {
+ findViewById(R.id.top_card).setVisibility(View.GONE);
+ }
+ }
cardModule.setViewModelProvider(new ViewModelProvider(/* owner= */this));
mHomeCardModules.add(cardModule);
if (DEBUG) {
@@ -337,13 +288,7 @@
/** Logs that the Activity is ready. Used for startup time diagnostics. */
private void maybeLogReady() {
boolean isResumed = isResumed();
- boolean taskViewInitialized = mTaskView != null && mTaskView.isInitialized();
- if (DEBUG) {
- Log.d(TAG, "maybeLogReady(" + getUserId() + "): mapsReady="
- + taskViewInitialized + ", started=" + isResumed + ", alreadyLogged: "
- + mIsReadyLogged);
- }
- if (taskViewInitialized && isResumed) {
+ if (isResumed) {
// We should report every time - the Android framework will take care of logging just
// when it's effectively drawn for the first time, but....
reportFullyDrawn();
@@ -392,21 +337,36 @@
|| !AppLauncherUtils.tosAccepted(/* context = */ this)) {
Log.i(TAG, "TOS not accepted, setting up content observers for TOS state");
} else {
- Log.i(TAG, "TOS accepted, state will remain accepted, "
- + "don't need to observe this value");
+ Log.i(TAG,
+ "TOS accepted, state will remain accepted, don't need to observe this value");
return;
}
mTosContentObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
- // TODO (b/280077391): Release the remote task view and recreate the map activity
- Log.i(TAG, "TOS state updated:" + AppLauncherUtils.tosAccepted(getBaseContext()));
- recreate();
+ // Release the task view and re-initialize the remote car task view with the new
+ // maps intent whenever an onChange is received. This is because the TOS state
+ // can go from uninitialized to not accepted during which there could be a race
+ // condition in which the maps activity is from the uninitialized state.
+ Set<String> tosDisabledApps = AppLauncherUtils.getTosDisabledPackages(
+ getBaseContext());
+ boolean tosAccepted = AppLauncherUtils.tosAccepted(getBaseContext());
+ Log.i(TAG, "TOS state updated:" + tosAccepted);
+ if (DEBUG) {
+ Log.d(TAG, "TOS disabled apps:" + tosDisabledApps);
+ }
+ if (mCarLauncherViewModel.getRemoteCarTaskView().getValue() != null) {
+ mCarLauncherViewModel.getRemoteCarTaskView().getValue().release();
+ setupRemoteCarTaskView(mMapsCard);
+ }
+ if (tosAccepted) {
+ unregisterTosContentObserver();
+ }
}
};
getContentResolver().registerContentObserver(
- Settings.Secure.getUriFor(KEY_USER_TOS_ACCEPTED),
+ Settings.Secure.getUriFor(KEY_UNACCEPTED_TOS_DISABLED_APPS),
/* notifyForDescendants*/ false,
mTosContentObserver);
}
diff --git a/app/src/com/android/car/carlauncher/CarLauncherViewModel.java b/app/src/com/android/car/carlauncher/CarLauncherViewModel.java
index 3bafbd5..3b2b081 100644
--- a/app/src/com/android/car/carlauncher/CarLauncherViewModel.java
+++ b/app/src/com/android/car/carlauncher/CarLauncherViewModel.java
@@ -40,6 +40,7 @@
import android.os.Build;
import android.util.Log;
+import androidx.core.util.Consumer;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
@@ -57,19 +58,30 @@
private final CarActivityManager mCarActivityManager;
private final Car mCar;
- private final CarTaskViewControllerHostLifecycle mHostLifecycle;
@SuppressLint("StaticFieldLeak") // We're not leaking this context as it is the window context.
private final Context mWindowContext;
- private final Intent mMapsIntent;
- private final MutableLiveData<RemoteCarTaskView> mRemoteCarTaskView;
- public CarLauncherViewModel(@UiContext Context context, @NonNull Intent mapsIntent) {
+ // Do not make this final because the maps intent can be changed based on the state of TOS.
+ private Intent mMapsIntent;
+ private CarTaskViewControllerHostLifecycle mHostLifecycle;
+ private MutableLiveData<RemoteCarTaskView> mRemoteCarTaskView;
+
+ public CarLauncherViewModel(@UiContext Context context) {
mWindowContext = context.createWindowContext(TYPE_APPLICATION_STARTING, /* options */ null);
- mMapsIntent = mapsIntent;
mCar = Car.createCar(mWindowContext);
mCarActivityManager = mCar.getCarManager(CarActivityManager.class);
- mHostLifecycle = new CarTaskViewControllerHostLifecycle();
+ }
+
+ /**
+ * Initialize the remote car task view with the maps intent.
+ */
+ void initializeRemoteCarTaskView(@NonNull Intent mapsIntent) {
+ if (DEBUG) {
+ Log.d(TAG, "Maps intent in the task view = " + mapsIntent.getComponent());
+ }
+ mMapsIntent = mapsIntent;
mRemoteCarTaskView = new MutableLiveData<>(null);
+ mHostLifecycle = new CarTaskViewControllerHostLifecycle();
ControlledRemoteCarTaskViewCallback controlledRemoteCarTaskViewCallback =
new ControlledRemoteCarTaskViewCallbackImpl(mRemoteCarTaskView);
@@ -101,7 +113,15 @@
@Override
public void onResume(@NonNull LifecycleOwner owner) {
DefaultLifecycleObserver.super.onResume(owner);
- mHostLifecycle.hostAppeared();
+ // Do not trigger 'hostAppeared()' in onResume.
+ // If the host Activity was hidden by an Activity, the Activity is moved to the other
+ // display, what the system expects would be the new moved Activity becomes the top one.
+ // But, at the time, the host Activity became visible and 'onResume()' is triggered.
+ // If 'hostAppeared()' is called in onResume, which moves the embeddedTask to the top and
+ // breaks the contract (the newly moved Activity becomes top).
+ // The contract is maintained by android.server.wm.multidisplay.MultiDisplayClientTests.
+ // BTW, if we don't invoke 'hostAppeared()', which makes the embedded task invisible if
+ // the host Activity gets the new Intent, so we'd call 'hostAppeared()' in onNewIntent.
}
@Override
@@ -122,6 +142,17 @@
super.onCleared();
}
+ public Consumer<Intent> getNewIntentListener() {
+ return mNewIntentConsumer;
+ }
+
+ private final Consumer<Intent> mNewIntentConsumer = new Consumer<Intent>() {
+ @Override
+ public void accept(Intent intent) {
+ mHostLifecycle.hostAppeared();
+ }
+ };
+
private static final class ControlledRemoteCarTaskViewCallbackImpl implements
ControlledRemoteCarTaskViewCallback {
private final MutableLiveData<RemoteCarTaskView> mRemoteCarTaskView;
@@ -198,17 +229,15 @@
static final class CarLauncherViewModelFactory implements ViewModelProvider.Factory {
private final Context mContext;
- private final Intent mMapsIntent;
- CarLauncherViewModelFactory(@UiContext Context context, @NonNull Intent mapsIntent) {
- mMapsIntent = requireNonNull(mapsIntent);
+ CarLauncherViewModelFactory(@UiContext Context context) {
mContext = requireNonNull(context);
}
@NonNull
@Override
public <T extends ViewModel> T create(Class<T> modelClass) {
- return modelClass.cast(new CarLauncherViewModel(mContext, mMapsIntent));
+ return modelClass.cast(new CarLauncherViewModel(mContext));
}
}
}
diff --git a/app/src/com/android/car/carlauncher/CarTaskView.java b/app/src/com/android/car/carlauncher/CarTaskView.java
deleted file mode 100644
index 5091ea2..0000000
--- a/app/src/com/android/car/carlauncher/CarTaskView.java
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- * Copyright (C) 2022 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.carlauncher;
-
-import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-
-import static com.android.car.carlauncher.TaskViewManager.DBG;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.ActivityManager;
-import android.content.Context;
-import android.graphics.Rect;
-import android.os.Binder;
-import android.util.Log;
-import android.util.SparseArray;
-import android.view.InsetsSource;
-import android.view.SurfaceControl;
-import android.view.SurfaceHolder;
-import android.window.WindowContainerToken;
-import android.window.WindowContainerTransaction;
-
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.taskview.TaskView;
-import com.android.wm.shell.taskview.TaskViewTaskController;
-import com.android.wm.shell.taskview.TaskViewTransitions;
-
-/**
- * CarLauncher version of {@link TaskView} which solves some CarLauncher specific issues:
- * <ul>
- * <li>b/228092608: Clears the hidden flag to make it TopFocusedRootTask.</li>
- * <li>b/225388469: Moves the embedded task to the top to make it resumed.</li>
- * </ul>
- */
-
-public class CarTaskView extends TaskView {
- private static final String TAG = CarTaskView.class.getSimpleName();
- @Nullable
- private WindowContainerToken mTaskToken;
- private final SyncTransactionQueue mSyncQueue;
- private final Binder mInsetsOwner = new Binder();
- private final SparseArray<Rect> mInsets = new SparseArray<>();
- private boolean mTaskViewReadySent;
- private TaskViewTaskController mTaskViewTaskController;
-
- public CarTaskView(Context context, ShellTaskOrganizer organizer,
- TaskViewTransitions taskViewTransitions, SyncTransactionQueue syncQueue,
- boolean shouldHideTask) {
- this(context, syncQueue, shouldHideTask,
- new TaskViewTaskController(context, organizer, taskViewTransitions, syncQueue));
- }
-
- public CarTaskView(Context context, SyncTransactionQueue syncQueue, boolean shouldHideTask,
- TaskViewTaskController taskViewTaskController) {
- super(context, taskViewTaskController);
- mTaskViewTaskController = taskViewTaskController;
- mTaskViewTaskController.setHideTaskWithSurface(shouldHideTask);
- mSyncQueue = syncQueue;
- }
-
- /**
- * Calls {@link TaskViewTaskController#onTaskAppeared(ActivityManager.RunningTaskInfo,
- * SurfaceControl)}.
- */
- public void dispatchTaskAppeared(ActivityManager.RunningTaskInfo taskInfo,
- SurfaceControl leash) {
- mTaskViewTaskController.onTaskAppeared(taskInfo, leash);
- }
-
- /**
- * Calls {@link TaskViewTaskController#onTaskVanished(ActivityManager.RunningTaskInfo)}.
- */
- public void dispatchTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
- mTaskViewTaskController.onTaskVanished(taskInfo);
- }
-
- /**
- * Calls {@link TaskViewTaskController#onTaskInfoChanged(ActivityManager.RunningTaskInfo)}.
- */
- public void dispatchTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
- mTaskViewTaskController.onTaskInfoChanged(taskInfo);
- }
-
- @Override
- public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
- mTaskToken = taskInfo.token;
- super.onTaskAppeared(taskInfo, leash);
-
- applyAllInsets();
- }
-
- /**
- * Triggers the change in the WM bounds as per the {@code newBounds} received.
- *
- * Should be called when the surface has changed. Can also be called before an animation if
- * the final bounds are already known.
- */
- public void setWindowBounds(Rect newBounds) {
- mTaskViewTaskController.setWindowBounds(newBounds);
- }
-
- @Override
- public void surfaceCreated(SurfaceHolder holder) {
- super.surfaceCreated(holder);
- if (mTaskViewReadySent) {
- if (DBG) Log.i(TAG, "car task view ready already sent");
- return;
- }
- onCarTaskViewInitialized();
- mTaskViewReadySent = true;
- }
-
- /**
- * Called only once when the {@link CarTaskView} is ready.
- */
- protected void onCarTaskViewInitialized() {}
-
- /**
- * Moves the embedded task over the embedding task to make it shown.
- */
- void showEmbeddedTask(WindowContainerTransaction wct) {
- if (mTaskToken == null) {
- return;
- }
- // Clears the hidden flag to make it TopFocusedRootTask: b/228092608
- wct.setHidden(mTaskToken, /* hidden= */ false);
- // Moves the embedded task to the top to make it resumed: b/225388469
- wct.reorder(mTaskToken, /* onTop= */ true);
- }
-
- // TODO(b/238473897): Consider taking insets one by one instead of taking all insets.
- /**
- * Adds & applies the given insets on the Task.
- *
- * <p>
- * The insets that were specified in an earlier call but not specified later, will remain
- * applied to the task. Clients should explicitly call
- * {@link #removeInsets(int, int)} to remove the insets from the underlying task.
- * </p>
- *
- * @param index The caller might add multiple insets sources with the same type.
- * This identifies them.
- * @param type The insets type of the insets source.
- * @param frame The rectangle area of the insets source.
- */
- public void addInsets(int index, int type, @NonNull Rect frame) {
- mInsets.append(InsetsSource.createId(mInsetsOwner, index, type), frame);
-
- if (mTaskToken == null) {
- // The insets will be applied later as part of onTaskAppeared.
- Log.w(TAG, "Cannot apply insets as the task token is not present.");
- return;
- }
- WindowContainerTransaction wct = new WindowContainerTransaction();
- wct.addInsetsSource(mTaskToken, mInsetsOwner, index, type, frame);
- mSyncQueue.queue(wct);
- }
-
- /**
- * Removes the given insets from the Task.
- *
- * @param index The caller might add multiple insets sources with the same type.
- * This identifies them.
- * @param type The insets type of the insets source.
- */
- public void removeInsets(int index, int type) {
- if (mInsets.size() == 0) {
- Log.w(TAG, "No insets set.");
- return;
- }
- int id = InsetsSource.createId(mInsetsOwner, index, type);
- if (!mInsets.contains(id)) {
- Log.w(TAG, "Insets type: " + type + " can't be removed as it was not "
- + "applied as part of the last addInsets()");
- return;
- }
- mInsets.remove(id);
-
- if (mTaskToken == null) {
- Log.w(TAG, "Cannot remove insets as the task token is not present.");
- return;
- }
- WindowContainerTransaction wct = new WindowContainerTransaction();
- wct.removeInsetsSource(mTaskToken, mInsetsOwner, index, type);
- mSyncQueue.queue(wct);
- }
-
- private void applyAllInsets() {
- if (mInsets.size() == 0) {
- Log.w(TAG, "Cannot apply null or empty insets");
- return;
- }
- if (mTaskToken == null) {
- Log.w(TAG, "Cannot apply insets as the task token is not present.");
- return;
- }
- WindowContainerTransaction wct = new WindowContainerTransaction();
- for (int i = 0; i < mInsets.size(); i++) {
- final int id = mInsets.keyAt(i);
- final Rect frame = mInsets.valueAt(i);
- wct.addInsetsSource(mTaskToken, mInsetsOwner, InsetsSource.getIndex(id),
- InsetsSource.getType(id), frame);
- }
- mSyncQueue.queue(wct);
- }
-
- /**
- * @return the taskId of the currently running task.
- */
- public int getTaskId() {
- if (mTaskViewTaskController.getTaskInfo() == null) {
- return INVALID_TASK_ID;
- }
- return mTaskViewTaskController.getTaskInfo().taskId;
- }
-}
diff --git a/app/src/com/android/car/carlauncher/CarTaskViewCallbacks.java b/app/src/com/android/car/carlauncher/CarTaskViewCallbacks.java
deleted file mode 100644
index 03edce5..0000000
--- a/app/src/com/android/car/carlauncher/CarTaskViewCallbacks.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2022 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.carlauncher;
-
-/**
- * A callback interface for the host activity that uses {@link CarTaskView} and its derivatives.
- */
-interface CarTaskViewCallbacks {
- /**
- * Called when the underlying {@link CarTaskView} instance is created.
- *
- * @param taskView the new newly created {@link CarTaskView} instance.
- */
- void onTaskViewCreated(CarTaskView taskView);
-
- /**
- * Called when the underlying {@link CarTaskView} is ready. A {@link CarTaskView} can be
- * considered ready when it has completed all the set up that is required.
- * This callback is only triggered once.
- *
- * For {@link LaunchRootCarTaskView}, this is called once the launch root task has been
- * fully set up.
- * For {@link SemiControlledCarTaskView} & {@link ControlledCarTaskView} this is called when
- * the surface is created.
- */
- void onTaskViewReady();
-}
-
-
diff --git a/app/src/com/android/car/carlauncher/ControlledCarTaskView.java b/app/src/com/android/car/carlauncher/ControlledCarTaskView.java
deleted file mode 100644
index b2b0531..0000000
--- a/app/src/com/android/car/carlauncher/ControlledCarTaskView.java
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * Copyright (C) 2022 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.carlauncher;
-
-import static com.android.car.carlauncher.TaskViewManager.DBG;
-
-import android.annotation.Nullable;
-import android.app.Activity;
-import android.app.ActivityManager;
-import android.app.ActivityOptions;
-import android.app.PendingIntent;
-import android.content.Intent;
-import android.graphics.Rect;
-import android.os.UserManager;
-import android.util.Log;
-import android.view.Display;
-import android.view.SurfaceControl;
-import android.window.WindowContainerTransaction;
-
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.taskview.TaskViewTransitions;
-
-import java.util.Set;
-import java.util.concurrent.Executor;
-
-/**
- * A controlled {@link CarTaskView} is fully managed by the {@link TaskViewManager}.
- * The underlying task will be restarted if it is crashed.
- *
- * It should be used when:
- * <ul>
- * <li>The underlying task is meant to be started by the host and be there forever.</li>
- * </ul>
- */
-final class ControlledCarTaskView extends CarTaskView {
- private static final String TAG = ControlledCarTaskView.class.getSimpleName();
-
- private final Executor mCallbackExecutor;
- private final ControlledCarTaskViewCallbacks mCallbacks;
- private final UserManager mUserManager;
- private final TaskViewManager mTaskViewManager;
- private final ControlledCarTaskViewConfig mConfig;
- @Nullable private RunnerWithBackoff mStartActivityWithBackoff;
-
- ControlledCarTaskView(
- Activity context,
- ShellTaskOrganizer organizer,
- TaskViewTransitions taskViewTransitions,
- SyncTransactionQueue syncQueue,
- Executor callbackExecutor,
- ControlledCarTaskViewConfig controlledCarTaskViewConfig,
- ControlledCarTaskViewCallbacks callbacks,
- UserManager userManager,
- TaskViewManager taskViewManager) {
- super(context, organizer, taskViewTransitions, syncQueue, true);
- mCallbackExecutor = callbackExecutor;
- mConfig = controlledCarTaskViewConfig;
- mCallbacks = callbacks;
- mUserManager = userManager;
- mTaskViewManager = taskViewManager;
-
- mCallbackExecutor.execute(() -> mCallbacks.onTaskViewCreated(this));
- if (mConfig.mAutoRestartOnCrash) {
- mStartActivityWithBackoff = new RunnerWithBackoff(this::startActivityInternal);
- }
- }
-
- @Override
- protected void onCarTaskViewInitialized() {
- super.onCarTaskViewInitialized();
- startActivity();
- mCallbackExecutor.execute(() -> mCallbacks.onTaskViewReady());
- }
-
- /**
- * Starts the underlying activity.
- */
- public void startActivity() {
- if (mStartActivityWithBackoff == null) {
- startActivityInternal();
- return;
- }
- mStartActivityWithBackoff.stop();
- mStartActivityWithBackoff.start();
- }
-
- private void stopTheStartActivityBackoffIfExists() {
- if (mStartActivityWithBackoff == null) {
- if (DBG) {
- Log.d(TAG, "mStartActivityWithBackoff is not present.");
- }
- return;
- }
- mStartActivityWithBackoff.stop();
- }
-
- private void startActivityInternal() {
- if (!mUserManager.isUserUnlocked()) {
- if (DBG) Log.d(TAG, "Can't start activity due to user is isn't unlocked");
- return;
- }
-
- // Don't start activity when the display is off. This can happen when the taskview is not
- // attached to a window.
- if (getDisplay() == null) {
- Log.w(TAG, "Can't start activity because display is not available in "
- + "taskview yet.");
- return;
- }
- // Don't start activity when the display is off for ActivityVisibilityTests.
- if (getDisplay().getState() != Display.STATE_ON) {
- Log.w(TAG, "Can't start activity due to the display is off");
- return;
- }
-
- ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext,
- /* enterResId= */ 0, /* exitResId= */ 0);
- Rect launchBounds = new Rect();
- getBoundsOnScreen(launchBounds);
- if (DBG) {
- Log.d(TAG, "Starting (" + mConfig.mActivityIntent.getComponent() + ") on "
- + launchBounds);
- }
- Intent fillInIntent = null;
- if ((mConfig.mActivityIntent.getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0) {
- fillInIntent = new Intent().addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
- }
- startActivity(
- PendingIntent.getActivity(mContext, /* requestCode= */ 0,
- mConfig.mActivityIntent,
- PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT),
- fillInIntent, options, launchBounds);
- }
-
- /** Gets the config used to build this controlled car task view. */
- ControlledCarTaskViewConfig getConfig() {
- return mConfig;
- }
-
- /**
- * See {@link ControlledCarTaskViewCallbacks#getDependingPackageNames()}.
- */
- Set<String> getDependingPackageNames() {
- return mCallbacks.getDependingPackageNames();
- }
-
- @Override
- public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
- super.onTaskAppeared(taskInfo, leash);
- // Stop the start activity backoff because a task has already appeared.
- stopTheStartActivityBackoffIfExists();
- }
-
- @Override
- public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
- super.onTaskVanished(taskInfo);
- if (mConfig.mAutoRestartOnCrash && mTaskViewManager.isHostVisible()) {
- // onTaskVanished can be called when the host is in the background. In this case
- // embedded activity should not be started.
- Log.i(TAG, "Restarting task " + taskInfo.baseActivity
- + " in ControlledCarTaskView");
- startActivity();
- }
- }
-
- @Override
- void showEmbeddedTask(WindowContainerTransaction wct) {
- if (getTaskInfo() == null) {
- if (DBG) {
- Log.d(TAG, "Embedded task not available, starting it now.");
- }
- startActivity();
- return;
- }
- super.showEmbeddedTask(wct);
- }
-
- @Override
- public void release() {
- super.release();
- stopTheStartActivityBackoffIfExists();
- }
-}
diff --git a/app/src/com/android/car/carlauncher/ControlledCarTaskViewCallbacks.java b/app/src/com/android/car/carlauncher/ControlledCarTaskViewCallbacks.java
deleted file mode 100644
index 56ef8ea..0000000
--- a/app/src/com/android/car/carlauncher/ControlledCarTaskViewCallbacks.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2022 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.carlauncher;
-
-import java.util.Collections;
-import java.util.Set;
-
-/**
- * A callback interface for {@link ControlledCarTaskView}.
- */
-public interface ControlledCarTaskViewCallbacks extends
- CarTaskViewCallbacks {
- /**
- * @return a set of package names which the task in the ControlledCarTaskView depends upon.
- * When any of these packages are changed, it will lead to restart of the task.
- */
- default Set<String> getDependingPackageNames() {
- return Collections.emptySet();
- }
-}
diff --git a/app/src/com/android/car/carlauncher/ControlledCarTaskViewConfig.java b/app/src/com/android/car/carlauncher/ControlledCarTaskViewConfig.java
deleted file mode 100644
index e07a0af..0000000
--- a/app/src/com/android/car/carlauncher/ControlledCarTaskViewConfig.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2022 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.carlauncher;
-
-import android.content.Intent;
-
-/** This class provides the required configuration to create a {@link ControlledCarTaskView}. */
-public final class ControlledCarTaskViewConfig {
- final Intent mActivityIntent;
- // TODO(b/242861717): When mAutoRestartOnCrash is enabled, mPackagesThatCanRestart doesn't make
- // a lot of sense. Consider removing it when there is more confidence with mAutoRestartOnCrash.
- final boolean mAutoRestartOnCrash;
- final boolean mCaptureGestures;
- final boolean mCaptureLongPress;
-
- private ControlledCarTaskViewConfig(
- Intent activityIntent,
- boolean autoRestartOnCrash,
- boolean captureGestures,
- boolean captureLongPress) {
- mActivityIntent = activityIntent;
- mAutoRestartOnCrash = autoRestartOnCrash;
- mCaptureGestures = captureGestures;
- mCaptureLongPress = captureLongPress;
- }
-
- /**
- * Creates a {@link Builder} object that is used to create instances of {@link
- * ControlledCarTaskViewConfig}.
- */
- public static Builder builder() {
- return new Builder();
- }
-
- /** A builder class for {@link ControlledCarTaskViewConfig}. */
- public static final class Builder {
- private Intent mActivityIntent;
- private boolean mAutoRestartOnCrash;
- private boolean mCaptureGestures;
- private boolean mCaptureLongPress;
-
- private Builder() {}
-
- /**
- * The intent of the activity that is meant to be started in this {@link
- * ControlledCarTaskView}.
- */
- public Builder setActivityIntent(Intent activityIntent) {
- mActivityIntent = activityIntent;
- return this;
- }
-
- /**
- * Sets the auto restart functionality. If set, the {@link ControlledCarTaskView} will
- * restart the task by re-launching the intent set via {@link #setActivityIntent(Intent)}
- * when the task crashes.
- */
- public Builder setAutoRestartOnCrash(boolean autoRestartOnCrash) {
- mAutoRestartOnCrash = autoRestartOnCrash;
- return this;
- }
-
- /**
- * Enables the swipe gesture capturing over {@link ControlledCarTaskView}. When enabled, the
- * swipe gestures won't be sent to the embedded app and will instead be forwarded to the
- * host activity.
- */
- public Builder setCaptureGestures(boolean captureGestures) {
- mCaptureGestures = captureGestures;
- return this;
- }
-
- /**
- * Enables the long press capturing over {@link ControlledCarTaskView}. When enabled, the
- * long press won't be sent to the embedded app and will instead be sent to the listener
- * specified via {@link
- * ControlledCarTaskView#setOnLongClickListener(View.OnLongClickListener)}.
- *
- * <p>If disabled, the listener supplied via {@link
- * ControlledCarTaskView#setOnLongClickListener(View.OnLongClickListener)} won't be called.
- */
- public Builder setCaptureLongPress(boolean captureLongPress) {
- mCaptureLongPress = captureLongPress;
- return this;
- }
-
- /** Creates the {@link ControlledCarTaskViewConfig} object. */
- public ControlledCarTaskViewConfig build() {
- if (mActivityIntent == null) {
- throw new IllegalArgumentException("mActivityIntent can't be null");
- }
- return new ControlledCarTaskViewConfig(
- mActivityIntent, mAutoRestartOnCrash, mCaptureGestures, mCaptureLongPress);
- }
- }
-}
diff --git a/app/src/com/android/car/carlauncher/LaunchRootCarTaskView.java b/app/src/com/android/car/carlauncher/LaunchRootCarTaskView.java
deleted file mode 100644
index 7f78a5c..0000000
--- a/app/src/com/android/car/carlauncher/LaunchRootCarTaskView.java
+++ /dev/null
@@ -1,277 +0,0 @@
-/*
- * Copyright (C) 2022 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.carlauncher;
-
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static android.view.Display.DEFAULT_DISPLAY;
-
-import static com.android.car.carlauncher.TaskViewManager.DBG;
-
-import android.app.Activity;
-import android.app.ActivityManager;
-import android.car.app.CarActivityManager;
-import android.util.Log;
-import android.view.SurfaceControl;
-import android.window.WindowContainerTransaction;
-
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.taskview.TaskViewTransitions;
-
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * A {@link CarTaskView} that can act as a default app container. A default app container is the
- * container where all apps open by default.
- */
-final class LaunchRootCarTaskView extends CarTaskView {
- private static final String TAG = LaunchRootCarTaskView.class.getSimpleName();
-
- private final Executor mCallbackExecutor;
- private final LaunchRootCarTaskViewCallbacks mCallbacks;
- private final ShellTaskOrganizer mShellTaskOrganizer;
- private final SyncTransactionQueue mSyncQueue;
- // Linked hash map is used to keep the tasks ordered as per the actual stack inside the root
- // task. Whenever a task becomes visible, it is bumped to the top of the stack.
- private final LinkedHashMap<Integer, ActivityManager.RunningTaskInfo> mLaunchRootStack =
- new LinkedHashMap<>();
- private final AtomicReference<CarActivityManager> mCarActivityManagerRef;
-
- private ActivityManager.RunningTaskInfo mLaunchRootTask;
-
- private final ShellTaskOrganizer.TaskListener mRootTaskListener =
- new ShellTaskOrganizer.TaskListener() {
- @Override
- public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo,
- SurfaceControl leash) {
- // The first call to onTaskAppeared() is always for the root-task.
- if (mLaunchRootTask == null && !taskInfo.hasParentTask()) {
- setRootTaskAsLaunchRoot(taskInfo);
- LaunchRootCarTaskView.this.dispatchTaskAppeared(taskInfo, leash);
- mCallbackExecutor.execute(() -> mCallbacks.onTaskViewReady());
- if (DBG) {
- Log.d(TAG, "got onTaskAppeared for the launch root task. Not "
- + "forwarding this to car activity manager");
- }
- return;
- }
-
- if (DBG) {
- Log.d(TAG, "launchRootCarTaskView onTaskAppeared " + taskInfo.taskId
- + " - " + taskInfo.baseActivity);
- }
-
- // TODO(b/228077499): Fix for the case when a task is started in the
- // launch-root-task right after the initialization of launch-root-task, it
- // remains blank.
- mSyncQueue.runInSync(t -> t.show(leash));
-
- CarActivityManager carAm = mCarActivityManagerRef.get();
- if (carAm != null) {
- carAm.onTaskAppeared(taskInfo);
- mLaunchRootStack.put(taskInfo.taskId, taskInfo);
- } else {
- Log.w(TAG, "CarActivityManager is null, skip onTaskAppeared: TaskInfo"
- + " = " + taskInfo);
- }
- }
-
- @Override
- public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
- if (mLaunchRootTask != null
- && mLaunchRootTask.taskId == taskInfo.taskId) {
- LaunchRootCarTaskView.this.dispatchTaskInfoChanged(taskInfo);
- if (DBG) {
- Log.d(TAG, "got onTaskInfoChanged for the launch root task. Not "
- + "forwarding this to car activity manager");
- }
- return;
- }
- if (DBG) {
- Log.d(TAG, "launchRootCarTaskView onTaskInfoChanged "
- + taskInfo.taskId + " - base=" + taskInfo.baseActivity + " - top="
- + taskInfo.topActivity);
- }
-
- // Uncontrolled apps by default launch in the launch root so nothing needs to
- // be done here for them.
- CarActivityManager carAm = mCarActivityManagerRef.get();
- if (carAm != null) {
- carAm.onTaskInfoChanged(taskInfo);
- if (taskInfo.isVisible && mLaunchRootStack.containsKey(taskInfo.taskId)) {
- // Remove the task and insert again so that it jumps to the end of
- // the queue.
- mLaunchRootStack.remove(taskInfo.taskId);
- mLaunchRootStack.put(taskInfo.taskId, taskInfo);
- }
- } else {
- Log.w(TAG, "CarActivityManager is null, skip onTaskInfoChanged: TaskInfo"
- + " = " + taskInfo);
- }
- }
-
- @Override
- public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
- if (DBG) {
- Log.d(TAG, "launchRootCarTaskView onTaskVanished " + taskInfo.taskId
- + " - " + taskInfo.baseActivity);
- }
- if (mLaunchRootTask != null
- && mLaunchRootTask.taskId == taskInfo.taskId) {
- LaunchRootCarTaskView.this.dispatchTaskVanished(taskInfo);
- if (DBG) {
- Log.d(TAG, "got onTaskVanished for the launch root task. Not "
- + "forwarding this to car activity manager");
- }
- return;
- }
-
- CarActivityManager carAm = mCarActivityManagerRef.get();
- if (carAm != null) {
- carAm.onTaskVanished(taskInfo);
- if (mLaunchRootStack.containsKey(taskInfo.taskId)) {
- mLaunchRootStack.remove(taskInfo.taskId);
- }
- } else {
- Log.w(TAG, "CarActivityManager is null, skip onTaskAppeared: TaskInfo"
- + " = " + taskInfo);
- }
- }
-
- @Override
- public void onBackPressedOnTaskRoot(ActivityManager.RunningTaskInfo taskInfo) {
- if (mLaunchRootStack.size() == 1) {
- Log.d(TAG, "Cannot remove last task from launch root.");
- return;
- }
- if (mLaunchRootStack.size() == 0) {
- Log.d(TAG, "Launch root is empty, do nothing.");
- return;
- }
-
- ActivityManager.RunningTaskInfo topTask = getTopTaskInLaunchRootTask();
- WindowContainerTransaction wct = new WindowContainerTransaction();
- // removeTask() will trigger onTaskVanished which will remove the task locally
- // from mLaunchRootStack
- wct.removeTask(topTask.token);
- mSyncQueue.queue(wct);
- }
- };
-
- public LaunchRootCarTaskView(Activity context,
- ShellTaskOrganizer organizer,
- TaskViewTransitions taskViewTransitions,
- SyncTransactionQueue syncQueue,
- Executor callbackExecutor,
- LaunchRootCarTaskViewCallbacks callbacks,
- AtomicReference<CarActivityManager> carActivityManager) {
- super(context, organizer, taskViewTransitions, syncQueue, false);
- mCallbacks = callbacks;
- mCallbackExecutor = callbackExecutor;
- mShellTaskOrganizer = organizer;
- mSyncQueue = syncQueue;
- mCarActivityManagerRef = carActivityManager;
-
- mCallbackExecutor.execute(() -> mCallbacks.onTaskViewCreated(this));
- }
-
- @Override
- protected void onCarTaskViewInitialized() {
- super.onCarTaskViewInitialized();
- mShellTaskOrganizer.getExecutor().execute(() -> {
- // removeWithTaskOrganizer should be true to signal the system that this root task is
- // inside a TaskView and should not be animated by the core.
- mShellTaskOrganizer.createRootTask(DEFAULT_DISPLAY,
- WINDOWING_MODE_MULTI_WINDOW,
- mRootTaskListener, /* removeWithTaskOrganizer= */ true);
- });
- }
-
- @Override
- public void release() {
- super.release();
- clearLaunchRootTask();
- }
-
- private void clearLaunchRootTask() {
- if (mLaunchRootTask == null) {
- Log.w(TAG, "Unable to clear launch root task because it is not created.");
- return;
- }
- WindowContainerTransaction wct = new WindowContainerTransaction();
- wct.setLaunchRoot(mLaunchRootTask.token, null, null);
- mSyncQueue.queue(wct);
- // Should run on shell's executor
- mShellTaskOrganizer.deleteRootTask(mLaunchRootTask.token);
- mLaunchRootTask = null;
- }
-
- private void setRootTaskAsLaunchRoot(ActivityManager.RunningTaskInfo taskInfo) {
- mLaunchRootTask = taskInfo;
- WindowContainerTransaction wct = new WindowContainerTransaction();
- wct.setLaunchRoot(taskInfo.token,
- new int[]{WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED},
- new int[]{ACTIVITY_TYPE_STANDARD, ACTIVITY_TYPE_RECENTS})
- .reorder(taskInfo.token, true);
- mSyncQueue.queue(wct);
- }
-
- public int getRootTaskCount() {
- return mLaunchRootStack.size();
- }
-
- /**
- * Returns the {@link android.app.ActivityManager.RunningTaskInfo} of the top task inside the
- * launch root car task view.
- */
- public ActivityManager.RunningTaskInfo getTopTaskInLaunchRootTask() {
- if (mLaunchRootStack.isEmpty()) {
- return null;
- }
- ActivityManager.RunningTaskInfo topTask = null;
- Iterator<ActivityManager.RunningTaskInfo> iterator = mLaunchRootStack.values().iterator();
- while (iterator.hasNext()) {
- topTask = iterator.next();
- }
- return topTask;
- }
-
- /**
- * Updates the window visibility associated with the root task of this LaunchRootCarTaskView.
- */
- public void updateRootTaskVisibility(boolean visibility) {
- if (mLaunchRootTask == null) {
- return;
- }
- WindowContainerTransaction wct = new WindowContainerTransaction();
- wct.setHidden(mLaunchRootTask.token, !visibility);
- // TODO(304309584): remove showEmbeddedTask if better solution is found.
- // Reorder the LaunchRootCarTaskView to avoid bringing host activity to the front when
- // touching host activity.
- if (!visibility) {
- showEmbeddedTask(wct);
- }
- mSyncQueue.queue(wct);
- }
-}
diff --git a/app/src/com/android/car/carlauncher/RunnerWithBackoff.java b/app/src/com/android/car/carlauncher/RunnerWithBackoff.java
deleted file mode 100644
index 9e3ce14..0000000
--- a/app/src/com/android/car/carlauncher/RunnerWithBackoff.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright (C) 2023 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.carlauncher;
-
-import static com.android.car.carlauncher.TaskViewManager.DBG;
-
-import android.os.Handler;
-import android.os.Looper;
-import android.util.Slog;
-
-/** A wrapper class for {@link Runnable} which retries in an exponential backoff manner. */
-final class RunnerWithBackoff {
- private static final String TAG = RunnerWithBackoff.class.getSimpleName();
- private static final int MAXIMUM_ATTEMPTS = 5;
- private static final int FIRST_BACKOFF_TIME_MS = 1_000; // 1 second
- private static final int MAXIMUM_BACKOFF_TIME_MS = 8000; // 8 seconds
- private final Handler mHandler = new Handler(Looper.myLooper());
- private final Runnable mAction;
-
- private int mBackoffTimeMs;
- private int mAttempts;
-
- RunnerWithBackoff(Runnable action) {
- mAction = action;
- }
-
- private final Runnable mRetryRunnable = new Runnable() {
- @Override
- public void run() {
- if (mAttempts >= MAXIMUM_ATTEMPTS) {
- Slog.e(TAG, "Failed to perform action, even after " + mAttempts + " attempts");
- return;
- }
- if (DBG) {
- Slog.d(TAG, "Executing the action. Attempt number " + mAttempts);
- }
- mAction.run();
-
- mHandler.postDelayed(mRetryRunnable, mBackoffTimeMs);
- increaseBackoff();
- mAttempts++;
- }
- };
-
- private void increaseBackoff() {
- mBackoffTimeMs *= 2;
- if (mBackoffTimeMs > MAXIMUM_BACKOFF_TIME_MS) {
- mBackoffTimeMs = MAXIMUM_BACKOFF_TIME_MS;
- }
- }
-
- /** Starts the retrying. The first try happens synchronously. */
- public void start() {
- if (DBG) {
- Slog.d(TAG, "start backoff runner");
- }
- // Stop the existing retrying as a safeguard to prevent multiple starts.
- stopInternal();
-
- mBackoffTimeMs = FIRST_BACKOFF_TIME_MS;
- mAttempts = 0;
- // Call .run() instead of posting to handler so that first try can happen synchronously.
- mRetryRunnable.run();
- }
-
- /** Stops the retrying. */
- public void stop() {
- if (DBG) {
- Slog.d(TAG, "stop backoff runner");
- }
- stopInternal();
- }
-
- private void stopInternal() {
- mHandler.removeCallbacks(mRetryRunnable);
- }
-}
diff --git a/app/src/com/android/car/carlauncher/SemiControlledCarTaskView.java b/app/src/com/android/car/carlauncher/SemiControlledCarTaskView.java
deleted file mode 100644
index 3a7b650..0000000
--- a/app/src/com/android/car/carlauncher/SemiControlledCarTaskView.java
+++ /dev/null
@@ -1,327 +0,0 @@
-/*
- * Copyright (C) 2022 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.carlauncher;
-
-import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
-import static android.view.Display.DEFAULT_DISPLAY;
-
-import static com.android.car.carlauncher.TaskViewManager.DBG;
-
-import android.app.Activity;
-import android.app.ActivityManager;
-import android.car.app.CarActivityManager;
-import android.content.ComponentName;
-import android.util.Log;
-import android.view.SurfaceControl;
-import android.window.WindowContainerTransaction;
-
-import androidx.annotation.VisibleForTesting;
-
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.taskview.TaskViewTransitions;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * A Semi-controlled {@link CarTaskView} is where the apps are meant to stay temporarily. It always
- * works when a {@link LaunchRootCarTaskView} has been set up.
- *
- * It serves these use-cases:
- * <ul>
- * <li>Should be used when the apps that are meant to be in it can be started from anywhere
- * in the system. i.e. when the host app has no control over their launching.</li>
- * <li>Suitable for apps like Assistant or Setup-Wizard.</li>
- * </ul>
- */
-final class SemiControlledCarTaskView extends CarTaskView {
- private static final String TAG = SemiControlledCarTaskView.class.getSimpleName();
- private final Executor mCallbackExecutor;
- private final SemiControlledCarTaskViewCallbacks mCallbacks;
- private final ShellTaskOrganizer mShellTaskOrganizer;
- private final SyncTransactionQueue mSyncQueue;
- private final LinkedHashMap<Integer, ActivityManager.RunningTaskInfo> mChildrenTaskStack =
- new LinkedHashMap<>();
- private final ArrayList<ComponentName> mAllowListedActivities;
- private final AtomicReference<CarActivityManager> mCarActivityManagerRef;
-
- private ActivityManager.RunningTaskInfo mRootTask;
-
- private final ShellTaskOrganizer.TaskListener mRootTaskListener =
- new ShellTaskOrganizer.TaskListener() {
- @Override
- public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo,
- SurfaceControl leash) {
- // The first call to onTaskAppeared() is always for the root-task.
- if (mRootTask == null && !taskInfo.hasParentTask()) {
- setRootTask(taskInfo);
- SemiControlledCarTaskView.this.dispatchTaskAppeared(taskInfo, leash);
-
- CarActivityManager carAm = mCarActivityManagerRef.get();
- if (carAm != null) {
- carAm.setPersistentActivitiesOnRootTask(
- mAllowListedActivities,
- taskInfo.token.asBinder());
- } else {
- Log.wtf(TAG, "CarActivityManager is null, cannot call "
- + "setPersistentActivitiesOnRootTask " + taskInfo);
- }
- mCallbackExecutor.execute(() -> mCallbacks.onTaskViewReady());
- return;
- }
-
- if (DBG) {
- Log.d(TAG, "onTaskAppeared " + taskInfo.taskId + " - "
- + taskInfo.baseActivity);
- }
-
- mChildrenTaskStack.put(taskInfo.taskId, taskInfo);
- }
-
- @Override
- public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
- if (mRootTask != null && mRootTask.taskId == taskInfo.taskId) {
- SemiControlledCarTaskView.this.dispatchTaskInfoChanged(taskInfo);
- }
- if (DBG) {
- Log.d(TAG, "onTaskInfoChanged " + taskInfo.taskId + " - "
- + taskInfo.baseActivity);
- }
- if (taskInfo.isVisible && mChildrenTaskStack.containsKey(taskInfo.taskId)) {
- // Remove the task and insert again so that it jumps to the end of
- // the queue.
- mChildrenTaskStack.remove(taskInfo.taskId);
- mChildrenTaskStack.put(taskInfo.taskId, taskInfo);
- }
- }
-
- @Override
- public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
- if (mRootTask != null && mRootTask.taskId == taskInfo.taskId) {
- SemiControlledCarTaskView.this.dispatchTaskVanished(taskInfo);
- }
- if (DBG) {
- Log.d(TAG, "onTaskVanished " + taskInfo.taskId + " - "
- + taskInfo.baseActivity);
- }
- if (mChildrenTaskStack.containsKey(taskInfo.taskId)) {
- mChildrenTaskStack.remove(taskInfo.taskId);
- }
- }
-
- @Override
- public void onBackPressedOnTaskRoot(ActivityManager.RunningTaskInfo taskInfo) {
- if (mChildrenTaskStack.size() == 0) {
- Log.d(TAG, "Root task is empty, do nothing.");
- return;
- }
-
- ActivityManager.RunningTaskInfo topTask = getTopTaskInTheRootTask();
- WindowContainerTransaction wct = new WindowContainerTransaction();
- // removeTask() will trigger onTaskVanished which will remove the task locally
- // from mChildrenTaskStack
- wct.removeTask(topTask.token);
- mSyncQueue.queue(wct);
- }
- };
-
- public SemiControlledCarTaskView(Activity context,
- ShellTaskOrganizer organizer,
- TaskViewTransitions taskViewTransitions,
- SyncTransactionQueue syncQueue,
- Executor callbackExecutor,
- List<ComponentName> allowListedActivities,
- SemiControlledCarTaskViewCallbacks callbacks,
- AtomicReference<CarActivityManager> carActivityManager) {
- super(context, organizer, taskViewTransitions, syncQueue, true);
- mCallbacks = callbacks;
- mCallbackExecutor = callbackExecutor;
- mCallbackExecutor.execute(() -> mCallbacks.onTaskViewCreated(this));
- mShellTaskOrganizer = organizer;
- mSyncQueue = syncQueue;
- mAllowListedActivities = new ArrayList<>(allowListedActivities);
- mCarActivityManagerRef = carActivityManager;
- }
-
- /**
- * @return the underlying {@link SemiControlledCarTaskViewCallbacks}.
- */
- SemiControlledCarTaskViewCallbacks getCallbacks() {
- return mCallbacks;
- }
-
- @Override
- protected void onCarTaskViewInitialized() {
- super.onCarTaskViewInitialized();
- mShellTaskOrganizer.getExecutor().execute(() -> {
- // removeWithTaskOrganizer should be true to signal the system that this root task is
- // inside a TaskView and should not be animated by the core.
- mShellTaskOrganizer.createRootTask(DEFAULT_DISPLAY,
- WINDOWING_MODE_MULTI_WINDOW,
- mRootTaskListener, /* removeWithTaskOrganizer= */ true);
- });
- }
-
- @Override
- public void release() {
- super.release();
- clearRootTask();
- }
-
- public ActivityManager.RunningTaskInfo getTopTaskInTheRootTask() {
- if (mChildrenTaskStack.isEmpty()) {
- return null;
- }
- ActivityManager.RunningTaskInfo topTask = null;
- Iterator<ActivityManager.RunningTaskInfo> iterator = mChildrenTaskStack.values().iterator();
- while (iterator.hasNext()) {
- topTask = iterator.next();
- }
- return topTask;
- }
-
- private void clearRootTask() {
- if (mRootTask == null) {
- Log.w(TAG, "Unable to clear root task because it is not created.");
- return;
- }
- // Should run on shell's executor
- mShellTaskOrganizer.deleteRootTask(mRootTask.token);
- mRootTask = null;
- }
-
- private void setRootTask(ActivityManager.RunningTaskInfo taskInfo) {
- mRootTask = taskInfo;
- }
-
- /**
- * Designates the given {@code activities} to be launched in this SemiControlledCarTaskView.
- * <p>Note: If an activity is already associated with another SemiControlledCarTaskView, it's
- * designates will be overridden.
- *
- * @param activities list of {@link ComponentName} of activities to be designated on the
- * SemiControlledCarTaskView
- */
- public void addAllowListedActivities(List<ComponentName> activities) {
- CarActivityManager carAm = mCarActivityManagerRef.get();
- if (carAm == null) {
- Log.wtf(TAG,
- "CarActivityManager is null, cannot call setPersistentActivitiesOnRootTask to"
- + " add activities");
- return;
- }
- if (mRootTask == null) {
- Log.wtf(TAG,
- "RootTask is null, cannot call setPersistentActivitiesOnRootTask to"
- + " add activities");
- return;
- }
- List<ComponentName> activitiesToAdd = new ArrayList<>();
- for (ComponentName activity : activities) {
- if (!mAllowListedActivities.contains(activity)) {
- activitiesToAdd.add(activity);
- } else {
- if (DBG) {
- Log.d(TAG, "Activity " + activity
- + " is already designated to this SemiControlledCarTaskView");
- }
- }
- }
- mAllowListedActivities.addAll(activitiesToAdd);
- carAm.setPersistentActivitiesOnRootTask(activitiesToAdd, mRootTask.token.asBinder());
- }
-
- /**
- * Remove the designation of the given {@code activities} to SemiControlledCarTaskView.
- * <p>Note: If an activity is already associated with another SemiControlledCarTaskView, it's
- * designates will be overridden.
- *
- * @param activities list of {@link ComponentName} of activities to be designated on the
- * SemiControlledCarTaskView
- */
- public void removeAllowListedActivities(List<ComponentName> activities) {
- CarActivityManager carAm = mCarActivityManagerRef.get();
- if (carAm == null) {
- Log.wtf(TAG,
- "CarActivityManager is null, cannot call setPersistentActivitiesOnRootTask to"
- + " remove activities");
- return;
- }
- if (mRootTask == null) {
- Log.wtf(TAG,
- "RootTask is null, cannot call setPersistentActivitiesOnRootTask to"
- + " add activities");
- return;
- }
- List<ComponentName> activitiesToRemove = new ArrayList<>();
- for (ComponentName activity : activities) {
- if (mAllowListedActivities.contains(activity)) {
- activitiesToRemove.add(activity);
- } else {
- if (DBG) {
- Log.d(TAG, "Activity " + activity
- + " was not designated to this SemiControlledCarTaskView");
- }
- }
- }
- mAllowListedActivities.removeAll(activitiesToRemove);
- carAm.setPersistentActivitiesOnRootTask(activitiesToRemove, /* rootTaskToken= */ null);
- }
-
- /**
- * Sets the designations for SemiControlledCarTaskView. Adds the designation of the given
- * {@code activities} to SemiControlledCarTaskView and removes the designations of activities
- * that are not in {@code activities}.
- *
- * <p>Note:
- * If an activity is already associated with another SemiControlledCarTaskView, it's
- * designates will be overridden.
- *
- * @param activities list of {@link ComponentName} of activities to be designated on the
- * SemiControlledCarTaskView
- */
- public void setAllowListedActivities(List<ComponentName> activities) {
- CarActivityManager carAm = mCarActivityManagerRef.get();
- if (carAm == null) {
- Log.wtf(TAG,
- "CarActivityManager is null, cannot call setPersistentActivitiesOnRootTask to"
- + " remove activities");
- return;
- }
- if (mRootTask == null) {
- Log.wtf(TAG,
- "RootTask is null, cannot call setPersistentActivitiesOnRootTask to"
- + " add activities");
- return;
- }
- List<ComponentName> activitiesToRemove = new ArrayList<>(mAllowListedActivities);
- carAm.setPersistentActivitiesOnRootTask(activitiesToRemove, /* rootTaskToken= */ null);
- carAm.setPersistentActivitiesOnRootTask(activities, mRootTask.token.asBinder());
- mAllowListedActivities.clear();
- mAllowListedActivities.addAll(activities);
- }
-
- @VisibleForTesting
- List<ComponentName> getPersistentActivities() {
- return mAllowListedActivities;
- }
-}
diff --git a/app/src/com/android/car/carlauncher/SemiControlledCarTaskViewCallbacks.java b/app/src/com/android/car/carlauncher/SemiControlledCarTaskViewCallbacks.java
deleted file mode 100644
index 040356e..0000000
--- a/app/src/com/android/car/carlauncher/SemiControlledCarTaskViewCallbacks.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (C) 2022 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.carlauncher;
-
-/**
- * A callbacks interface for {@link SemiControlledCarTaskView}.
- */
-public interface SemiControlledCarTaskViewCallbacks extends
- CarTaskViewCallbacks {
-}
diff --git a/app/src/com/android/car/carlauncher/TaskViewInputInterceptor.java b/app/src/com/android/car/carlauncher/TaskViewInputInterceptor.java
deleted file mode 100644
index 12e771d..0000000
--- a/app/src/com/android/car/carlauncher/TaskViewInputInterceptor.java
+++ /dev/null
@@ -1,292 +0,0 @@
-/*
- * Copyright (C) 2022 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.carlauncher;
-
-import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
-import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
-import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY;
-import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
-
-import android.annotation.MainThread;
-import android.app.Activity;
-import android.app.Application;
-import android.content.Context;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.hardware.input.InputManager;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.wm.shell.taskview.TaskView;
-
-import java.util.List;
-
-/**
- * This class is responsible to intercept the swipe gestures & long press over {@link
- * ControlledCarTaskView}.
- *
- * <ul>
- * <li>The gesture interception will only occur when the corresponding {@link
- * ControlledCarTaskViewConfig#mCaptureGestures} is set.
- * <li>The long press interception will only occur when the corresponding {@link
- * ControlledCarTaskViewConfig#mCaptureLongPress} is set.
- * </ul>
- */
-public final class TaskViewInputInterceptor {
-
- private static final String TAG = TaskViewInputInterceptor.class.getSimpleName();
- private static final boolean DBG = Log.isLoggable(CarLauncher.TAG, Log.DEBUG);
-
- private static final Rect sTmpBounds = new Rect();
-
- private final Activity mHostActivity;
- private final InputManager mInputManager;
- private final TaskViewManager mTaskViewManager;
- private final WindowManager mWm;
- private final GestureDetector mGestureDetector =
- new GestureDetector(new TaskViewGestureListener());
- private final Application.ActivityLifecycleCallbacks mActivityLifecycleCallbacks =
- new ActivityLifecycleHandler();
-
- private View mSpyWindow;
- private boolean mInitialized = false;
-
- TaskViewInputInterceptor(Activity hostActivity, TaskViewManager taskViewManager) {
- mHostActivity = hostActivity;
- mInputManager = hostActivity.getSystemService(InputManager.class);
- mTaskViewManager = taskViewManager;
- mWm = mHostActivity.getSystemService(WindowManager.class);
- }
-
- private static boolean isIn(MotionEvent event, TaskView taskView) {
- taskView.getBoundsOnScreen(sTmpBounds);
- return sTmpBounds.contains((int) event.getX(), (int) event.getY());
- }
-
- /** Initializes & starts intercepting gestures. Does nothing if already initialized. */
- @MainThread
- void init() {
- if (mInitialized) {
- Log.e(TAG, "Already initialized");
- return;
- }
- mInitialized = true;
- mHostActivity.registerActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
- startInterceptingGestures();
- }
-
- /**
- * Releases the held resources and stops intercepting gestures. Does nothing if already
- * released.
- */
- @MainThread
- void release() {
- if (!mInitialized) {
- Log.e(TAG, "Failed to release as it is not initialized");
- return;
- }
- mInitialized = false;
- mHostActivity.unregisterActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
- stopInterceptingGestures();
- }
-
- private void startInterceptingGestures() {
- if (DBG) {
- Log.d(TAG, "Start intercepting gestures");
- }
- if (mSpyWindow != null) {
- Log.d(TAG, "Already intercepting gestures");
- return;
- }
- createAndAddSpyWindow();
- }
-
- private void stopInterceptingGestures() {
- if (DBG) {
- Log.d(TAG, "Stop intercepting gestures");
- }
- if (mSpyWindow == null) {
- Log.d(TAG, "Already not intercepting gestures");
- return;
- }
- removeSpyWindow();
- }
-
- private void createAndAddSpyWindow() {
- mSpyWindow = new GestureSpyView(mHostActivity);
- WindowManager.LayoutParams p =
- new WindowManager.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT,
- TYPE_APPLICATION_OVERLAY,
- WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
- | FLAG_NOT_FOCUSABLE
- | FLAG_LAYOUT_IN_SCREEN,
- // LAYOUT_IN_SCREEN required so that event coordinate system matches the
- // taskview.getBoundsOnScreen coordinate system
- PixelFormat.TRANSLUCENT);
- p.inputFeatures = INPUT_FEATURE_SPY;
- p.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
- mWm.addView(mSpyWindow, p);
- }
-
- private void removeSpyWindow() {
- if (mSpyWindow == null) {
- Log.e(TAG, "Spy window is not present");
- return;
- }
- mWm.removeView(mSpyWindow);
- mSpyWindow = null;
- }
-
- private final class GestureSpyView extends View {
- private boolean mConsumingCurrentEventStream = false;
- private boolean mActionDownInsideTaskView = false;
- private float mTouchDownX;
- private float mTouchDownY;
-
- GestureSpyView(Context context) {
- super(context);
- }
-
- @Override
- public boolean dispatchTouchEvent(MotionEvent event) {
- boolean justToggled = false;
- mGestureDetector.onTouchEvent(event);
-
- if (event.getAction() == MotionEvent.ACTION_DOWN) {
- mActionDownInsideTaskView = false;
-
- List<ControlledCarTaskView> taskViewList =
- mTaskViewManager.getControlledTaskViews();
- for (ControlledCarTaskView tv : taskViewList) {
- if (tv.getConfig().mCaptureGestures && isIn(event, tv)) {
- mTouchDownX = event.getX();
- mTouchDownY = event.getY();
- mActionDownInsideTaskView = true;
- break;
- }
- }
-
- // Stop consuming immediately on ACTION_DOWN
- mConsumingCurrentEventStream = false;
- }
-
- if (event.getAction() == MotionEvent.ACTION_MOVE) {
- if (!mConsumingCurrentEventStream && mActionDownInsideTaskView
- && Float.compare(mTouchDownX, event.getX()) != 0
- && Float.compare(mTouchDownY, event.getY()) != 0) {
- // Start consuming on ACTION_MOVE when ACTION_DOWN happened inside TaskView
- mConsumingCurrentEventStream = true;
- justToggled = true;
- }
-
- // Handling the events
- if (mConsumingCurrentEventStream) {
- // Disable the propagation when consuming events.
- mInputManager.pilferPointers(getViewRootImpl().getInputToken());
-
- if (justToggled) {
- // When just toggled from DOWN to MOVE, dispatch a DOWN event as DOWN event
- // is meant to be the first event in an event stream.
- MotionEvent cloneEvent = MotionEvent.obtain(event);
- cloneEvent.setAction(MotionEvent.ACTION_DOWN);
- TaskViewInputInterceptor.this.mHostActivity.dispatchTouchEvent(cloneEvent);
- cloneEvent.recycle();
- }
- TaskViewInputInterceptor.this.mHostActivity.dispatchTouchEvent(event);
- }
- }
-
- if (event.getAction() == MotionEvent.ACTION_UP) {
- // Handling the events
- if (mConsumingCurrentEventStream) {
- // Disable the propagation when handling manually.
- mInputManager.pilferPointers(getViewRootImpl().getInputToken());
- TaskViewInputInterceptor.this.mHostActivity.dispatchTouchEvent(event);
- }
- mConsumingCurrentEventStream = false;
- }
- return false;
- }
- }
-
- private final class TaskViewGestureListener extends GestureDetector.SimpleOnGestureListener {
- @Override
- public void onLongPress(@NonNull MotionEvent e) {
- List<ControlledCarTaskView> taskViewList = mTaskViewManager.getControlledTaskViews();
- for (ControlledCarTaskView tv : taskViewList) {
- if (tv.getConfig().mCaptureLongPress && isIn(e, tv)) {
- if (DBG) {
- Log.d(TAG, "Long press captured for taskView: " + tv);
- }
- mInputManager.pilferPointers(mSpyWindow.getViewRootImpl().getInputToken());
- if (tv.getOnLongClickListener() != null) {
- tv.getOnLongClickListener().onLongClick(tv);
- }
- return;
- }
- }
- if (DBG) {
- Log.d(TAG, "Long press not captured");
- }
- }
- }
-
- private final class ActivityLifecycleHandler implements Application.ActivityLifecycleCallbacks {
- @Override
- public void onActivityCreated(
- @NonNull Activity activity, @Nullable Bundle savedInstanceState) {}
-
- @Override
- public void onActivityStarted(@NonNull Activity activity) {
- if (!mInitialized) {
- return;
- }
- startInterceptingGestures();
- }
-
- @Override
- public void onActivityResumed(@NonNull Activity activity) {}
-
- @Override
- public void onActivityPaused(@NonNull Activity activity) {}
-
- @Override
- public void onActivityStopped(@NonNull Activity activity) {
- if (!mInitialized) {
- return;
- }
- stopInterceptingGestures();
- }
-
- @Override
- public void onActivitySaveInstanceState(
- @NonNull Activity activity, @NonNull Bundle outState) {}
-
- @Override
- public void onActivityDestroyed(@NonNull Activity activity) {}
- }
-}
diff --git a/app/src/com/android/car/carlauncher/TaskViewManager.java b/app/src/com/android/car/carlauncher/TaskViewManager.java
deleted file mode 100644
index 9d29856..0000000
--- a/app/src/com/android/car/carlauncher/TaskViewManager.java
+++ /dev/null
@@ -1,610 +0,0 @@
-/*
- * 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.carlauncher;
-
-import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
-import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING;
-import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED;
-
-import static com.android.car.carlauncher.CarLauncher.TAG;
-import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.Activity;
-import android.app.ActivityManager;
-import android.app.ActivityTaskManager;
-import android.app.Application.ActivityLifecycleCallbacks;
-import android.app.TaskInfo;
-import android.app.TaskStackListener;
-import android.car.Car;
-import android.car.app.CarActivityManager;
-import android.car.user.CarUserManager;
-import android.car.user.UserLifecycleEventFilter;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.UserManager;
-import android.util.Log;
-import android.util.Slog;
-import android.view.WindowManagerGlobal;
-import android.window.TaskAppearedInfo;
-import android.window.WindowContainerToken;
-import android.window.WindowContainerTransaction;
-
-import com.android.car.carlauncher.taskstack.TaskStackChangeListeners;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.launcher3.icons.IconProvider;
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.common.HandlerExecutor;
-import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.common.annotations.ShellMainThread;
-import com.android.wm.shell.fullscreen.FullscreenTaskListener;
-import com.android.wm.shell.startingsurface.StartingWindowController;
-import com.android.wm.shell.startingsurface.phone.PhoneStartingWindowTypeAlgorithm;
-import com.android.wm.shell.sysui.ShellCommandHandler;
-import com.android.wm.shell.sysui.ShellController;
-import com.android.wm.shell.sysui.ShellInit;
-import com.android.wm.shell.taskview.TaskViewTransitions;
-import com.android.wm.shell.transition.HomeTransitionObserver;
-import com.android.wm.shell.transition.Transitions;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicReference;
-
-
-/**
- * A manager for creating {@link ControlledCarTaskView}, {@link LaunchRootCarTaskView} &
- * {@link SemiControlledCarTaskView}.
- */
-public final class TaskViewManager {
- static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
- private static final String SCHEME_PACKAGE = "package";
-
- private final AtomicReference<CarActivityManager> mCarActivityManagerRef =
- new AtomicReference<>();
- @ShellMainThread
- private final HandlerExecutor mShellExecutor;
- private final SyncTransactionQueue mSyncQueue;
- private final Transitions mTransitions;
- private final TaskViewTransitions mTaskViewTransitions;
- private final ShellTaskOrganizer mTaskOrganizer;
- private final int mHostTaskId;
-
- // All TaskView are bound to the Host Activity if it exists.
- @ShellMainThread
- private final List<ControlledCarTaskView> mControlledTaskViews = new ArrayList<>();
- @ShellMainThread
- private final List<SemiControlledCarTaskView> mSemiControlledTaskViews = new ArrayList<>();
- @ShellMainThread
- private LaunchRootCarTaskView mLaunchRootCarTaskView = null;
-
- private TaskViewInputInterceptor mTaskViewInputInterceptor;
- private CarUserManager mCarUserManager;
- private Activity mContext;
- private Car mCar;
- private boolean mReleased = false;
-
- private final TaskStackListener mTaskStackListener = new TaskStackListener() {
- @Override
- public void onTaskFocusChanged(int taskId, boolean focused) {
- boolean hostFocused = taskId == mHostTaskId && focused;
- if (DBG) {
- Log.d(TAG, "onTaskFocusChanged: taskId=" + taskId
- + ", hostFocused=" + hostFocused);
- }
- if (!hostFocused) {
- return;
- }
-
- for (int i = mControlledTaskViews.size() - 1; i >= 0; --i) {
- ControlledCarTaskView taskView = mControlledTaskViews.get(i);
- if (taskView.getTaskId() == INVALID_TASK_ID) {
- // If the task in TaskView is crashed when host is in background,
- // We'd like to restart it when host becomes foreground and focused.
- taskView.startActivity();
- }
- }
- }
-
- @Override
- public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
- boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
- if (DBG) {
- Log.d(TAG, "onActivityRestartAttempt: taskId=" + task.taskId
- + ", homeTaskVisible=" + homeTaskVisible + ", wasVisible=" + wasVisible);
- }
- if (mHostTaskId != task.taskId) {
- return;
- }
- showEmbeddedTasks();
- }
- };
-
- private final CarUserManager.UserLifecycleListener mUserLifecycleListener = event -> {
- if (DBG) {
- Log.d(TAG, "UserLifecycleListener.onEvent: For User "
- + mContext.getUserId()
- + ", received an event " + event);
- }
-
- // When user-unlocked, if task isn't launched yet, then try to start it.
- if (event.getEventType() == USER_LIFECYCLE_EVENT_TYPE_UNLOCKED
- && mContext.getUserId() == event.getUserId()) {
- for (int i = mControlledTaskViews.size() - 1; i >= 0; --i) {
- ControlledCarTaskView taskView = mControlledTaskViews.get(i);
- if (taskView.getTaskId() == INVALID_TASK_ID) {
- taskView.startActivity();
- }
- }
- }
-
- // When user-switching, onDestroy in the previous user's Host app isn't called.
- // So try to release the resource explicitly.
- if (event.getEventType() == USER_LIFECYCLE_EVENT_TYPE_SWITCHING
- && mContext.getUserId() == event.getPreviousUserId()) {
- release();
- }
- };
-
- private final BroadcastReceiver mPackageBroadcastReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (DBG) Log.d(TAG, "onReceive: intent=" + intent);
-
- if (!isHostVisible()) {
- return;
- }
-
- String packageName = intent.getData().getSchemeSpecificPart();
- for (int i = mControlledTaskViews.size() - 1; i >= 0; --i) {
- ControlledCarTaskView taskView = mControlledTaskViews.get(i);
- if (taskView.getTaskId() == INVALID_TASK_ID
- && taskView.getDependingPackageNames().contains(packageName)) {
- taskView.startActivity();
- }
- }
- }
- };
-
- public TaskViewManager(Activity context, Handler mainHandler) {
- this(context, mainHandler, new HandlerExecutor(mainHandler));
- }
-
- private TaskViewManager(Activity context, Handler mainHandler,
- HandlerExecutor handlerExecutor) {
- this(context, mainHandler, handlerExecutor, new ShellTaskOrganizer(handlerExecutor),
- new TransactionPool(), new ShellCommandHandler(), new ShellInit(handlerExecutor));
- }
-
- private TaskViewManager(Activity context, Handler mainHandler, HandlerExecutor handlerExecutor,
- ShellTaskOrganizer taskOrganizer, TransactionPool transactionPool,
- ShellCommandHandler shellCommandHandler, ShellInit shellinit) {
- this(context, mainHandler, handlerExecutor, taskOrganizer,
- transactionPool,
- shellinit,
- new ShellController(context, shellinit, shellCommandHandler, handlerExecutor),
- new DisplayController(context,
- WindowManagerGlobal.getWindowManagerService(), shellinit, handlerExecutor)
- );
- }
-
- private TaskViewManager(Activity context, Handler mainHandler, HandlerExecutor handlerExecutor,
- ShellTaskOrganizer taskOrganizer, TransactionPool transactionPool, ShellInit shellinit,
- ShellController shellController, DisplayController dc) {
- this(context, handlerExecutor, taskOrganizer,
- new SyncTransactionQueue(transactionPool, handlerExecutor),
- new Transitions(context, shellinit, shellController, taskOrganizer,
- transactionPool, dc, handlerExecutor, mainHandler, handlerExecutor,
- new HomeTransitionObserver(context, handlerExecutor)),
- shellinit,
- shellController,
- new StartingWindowController(context, shellinit,
- shellController,
- taskOrganizer,
- handlerExecutor,
- new PhoneStartingWindowTypeAlgorithm(),
- new IconProvider(context),
- transactionPool));
- }
-
- @VisibleForTesting
- TaskViewManager(Activity context, HandlerExecutor handlerExecutor,
- ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue,
- Transitions transitions, ShellInit shellInit, ShellController shellController,
- StartingWindowController startingWindowController) {
- if (DBG) Slog.d(TAG, "TaskViewManager(), u=" + context.getUserId());
- mContext = context;
- mShellExecutor = handlerExecutor;
- mTaskOrganizer = shellTaskOrganizer;
- mHostTaskId = mContext.getTaskId();
- mSyncQueue = syncQueue;
- mTransitions = transitions;
- mTaskViewTransitions = new TaskViewTransitions(mTransitions);
- mTaskViewInputInterceptor = new TaskViewInputInterceptor(context, this);
-
- initCar();
- shellInit.init();
- initTaskOrganizer(mCarActivityManagerRef);
- mContext.registerActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
- }
-
- private void initCar() {
- mCar = Car.createCar(/* context= */ mContext, /* handler= */ null,
- Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
- (car, ready) -> {
- if (!ready) {
- Log.w(TAG, "CarService looks crashed");
- mCarActivityManagerRef.set(null);
- return;
- }
- setCarUserManager((CarUserManager) car.getCarManager(Car.CAR_USER_SERVICE));
- UserLifecycleEventFilter filter = new UserLifecycleEventFilter.Builder()
- .addEventType(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED)
- .addEventType(USER_LIFECYCLE_EVENT_TYPE_SWITCHING).build();
- mCarUserManager.addListener(mContext.getMainExecutor(), filter,
- mUserLifecycleListener);
- CarActivityManager carAM = (CarActivityManager) car.getCarManager(
- Car.CAR_ACTIVITY_SERVICE);
- mCarActivityManagerRef.set(carAM);
-
- carAM.registerTaskMonitor();
- });
-
- TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
-
- IntentFilter packageIntentFilter = new IntentFilter(Intent.ACTION_PACKAGE_REPLACED);
- packageIntentFilter.addDataScheme(SCHEME_PACKAGE);
- mContext.registerReceiver(mPackageBroadcastReceiver, packageIntentFilter);
- }
-
- // TODO(b/239958124A): Remove this method when unit tests for TaskViewManager have been added.
- /**
- * This method only exists for the container activity to set mock car user manager in tests.
- */
- void setCarUserManager(CarUserManager carUserManager) {
- mCarUserManager = carUserManager;
- }
-
- private Transitions initTransitions(ShellInit shellInit, TransactionPool txPool,
- ShellController shellController, Handler mainHandler) {
- DisplayController dc = new DisplayController(mContext,
- WindowManagerGlobal.getWindowManagerService(), shellInit, mShellExecutor);
- return new Transitions(mContext, shellInit, shellController, mTaskOrganizer,
- txPool, dc, mShellExecutor, mainHandler, mShellExecutor,
- new HomeTransitionObserver(mContext, mShellExecutor));
- }
-
- private void initTaskOrganizer(AtomicReference<CarActivityManager> carActivityManagerRef) {
- FullscreenTaskListener fullscreenTaskListener = new CarFullscreenTaskMonitorListener(
- carActivityManagerRef, mSyncQueue);
- mTaskOrganizer.addListenerForType(fullscreenTaskListener, TASK_LISTENER_TYPE_FULLSCREEN);
- List<TaskAppearedInfo> taskAppearedInfos = mTaskOrganizer.registerOrganizer();
- cleanUpExistingTaskViewTasks(taskAppearedInfos);
- }
-
- /**
- * Creates a {@link ControlledCarTaskView}.
- *
- * @param callbackExecutor the executor which the {@link ControlledCarTaskViewCallbacks} will
- * be executed on.
- * @param controlledCarTaskViewConfig the configuration for the underlying
- * {@link ControlledCarTaskView}.
- * @param taskViewCallbacks the callbacks for the underlying TaskView.
- */
- public void createControlledCarTaskView(
- Executor callbackExecutor,
- ControlledCarTaskViewConfig controlledCarTaskViewConfig,
- ControlledCarTaskViewCallbacks taskViewCallbacks) {
- mShellExecutor.execute(() -> {
- ControlledCarTaskView taskView = new ControlledCarTaskView(mContext, mTaskOrganizer,
- mTaskViewTransitions, mSyncQueue, callbackExecutor, controlledCarTaskViewConfig,
- taskViewCallbacks, mContext.getSystemService(UserManager.class), this);
- mControlledTaskViews.add(taskView);
-
- if (controlledCarTaskViewConfig.mCaptureGestures
- || controlledCarTaskViewConfig.mCaptureLongPress) {
- mTaskViewInputInterceptor.init();
- }
- });
-
- }
-
- /**
- * Creates a {@link LaunchRootCarTaskView}.
- *
- * @param callbackExecutor the executor which the {@link LaunchRootCarTaskViewCallbacks} will be
- * executed on.
- * @param taskViewCallbacks the callbacks for the underlying TaskView.
- */
- public void createLaunchRootTaskView(Executor callbackExecutor,
- LaunchRootCarTaskViewCallbacks taskViewCallbacks) {
- mShellExecutor.execute(() -> {
- if (mLaunchRootCarTaskView != null) {
- throw new IllegalStateException("Cannot create more than one launch root task");
- }
- mLaunchRootCarTaskView = new LaunchRootCarTaskView(mContext, mTaskOrganizer,
- mTaskViewTransitions, mSyncQueue,
- callbackExecutor, taskViewCallbacks, mCarActivityManagerRef);
- });
- }
-
- /**
- * Updates the window visibility associated with {@link WindowContainerToken}.
- *
- * @param token {@link WindowContainerToken} of the window that needs to be hidden
- * @param visibility {true} if window needs to be displayed {false} otherwise
- */
- public void updateTaskVisibility(WindowContainerToken token, boolean visibility) {
- WindowContainerTransaction wct = new WindowContainerTransaction();
- wct.setHidden(token, !visibility);
- mSyncQueue.queue(wct);
- }
-
- /**
- * Updates the window visibility associated with the launch root task of
- * {@link mLaunchRootCarTaskView}.
- *
- * @param visibility {true} if window needs to be displayed {false} otherwise
- */
- public void updateLaunchRootCarTaskVisibility(boolean visibility) {
- if (mLaunchRootCarTaskView == null) {
- return;
- }
- mLaunchRootCarTaskView.updateRootTaskVisibility(visibility);
- }
-
- /**
- * Creates a {@link SemiControlledCarTaskView}.
- *
- * @param callbackExecutor the executor which the {@link SemiControlledCarTaskViewCallbacks}
- * will be executed on.
- * @param allowListedActivities the list of activities that will always be started in this
- * taskview.
- * @param taskViewCallbacks the callbacks for the underlying TaskView.
- */
- public void createSemiControlledTaskView(Executor callbackExecutor,
- List<ComponentName> allowListedActivities,
- SemiControlledCarTaskViewCallbacks taskViewCallbacks) {
- mShellExecutor.execute(() -> {
- SemiControlledCarTaskView taskView = new SemiControlledCarTaskView(mContext,
- mTaskOrganizer, mTaskViewTransitions, mSyncQueue,
- callbackExecutor, allowListedActivities, taskViewCallbacks,
- mCarActivityManagerRef);
- mSemiControlledTaskViews.add(taskView);
- });
- }
-
- /**
- * Releases {@link TaskViewManager} and unregisters the underlying {@link ShellTaskOrganizer}.
- * It also removes all TaskViews which are created by this {@link TaskViewManager}.
- */
- void release() {
- mShellExecutor.execute(() -> {
- if (DBG) Slog.d(TAG, "TaskViewManager.release, u=" + mContext.getUser());
-
- if (mCarUserManager != null) {
- mCarUserManager.removeListener(mUserLifecycleListener);
- }
- TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
- mContext.unregisterReceiver(mPackageBroadcastReceiver);
-
- CarActivityManager carAM = mCarActivityManagerRef.get();
- if (carAM != null) {
- carAM.unregisterTaskMonitor();
- mCarActivityManagerRef.set(null);
- }
-
- for (int i = mControlledTaskViews.size() - 1; i >= 0; --i) {
- mControlledTaskViews.get(i).release();
- }
- mControlledTaskViews.clear();
-
- for (int i = mSemiControlledTaskViews.size() - 1; i >= 0; --i) {
- mSemiControlledTaskViews.get(i).release();
- }
- mSemiControlledTaskViews.clear();
-
- if (mLaunchRootCarTaskView != null) {
- mLaunchRootCarTaskView.release();
- mLaunchRootCarTaskView = null;
- }
-
- mContext.unregisterActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
- mTaskOrganizer.unregisterOrganizer();
- mTaskViewInputInterceptor.release();
-
- if (mCar != null) {
- mCar.disconnect();
- }
- mReleased = true;
- });
- }
-
- /**
- * Shows all the embedded tasks. If the tasks are
- */
- public void showEmbeddedTasks() {
- WindowContainerTransaction wct = new WindowContainerTransaction();
- for (int i = mControlledTaskViews.size() - 1; i >= 0; --i) {
- // showEmbeddedTasks() will restart the crashed tasks too.
- mControlledTaskViews.get(i).showEmbeddedTask(wct);
- }
- if (mLaunchRootCarTaskView != null) {
- mLaunchRootCarTaskView.showEmbeddedTask(wct);
- }
- for (int i = mSemiControlledTaskViews.size() - 1; i >= 0; --i) {
- mSemiControlledTaskViews.get(i).showEmbeddedTask(wct);
- }
- mSyncQueue.queue(wct);
- }
-
- /**
- * @return {@code true} if the host activity is in resumed or started state, {@code false}
- * otherwise.
- */
- boolean isHostVisible() {
- // This code relies on Activity#isVisibleForAutofill() instead of maintaining a custom
- // activity state.
- return mContext.isVisibleForAutofill();
- }
-
- private final ActivityLifecycleCallbacks mActivityLifecycleCallbacks =
- new ActivityLifecycleCallbacks() {
- @Override
- public void onActivityCreated(@NonNull Activity activity,
- @Nullable Bundle savedInstanceState) {}
-
- @Override
- public void onActivityStarted(@NonNull Activity activity) {}
-
- @Override
- public void onActivityResumed(@NonNull Activity activity) {}
-
- @Override
- public void onActivityPaused(@NonNull Activity activity) {}
-
- @Override
- public void onActivityStopped(@NonNull Activity activity) {}
-
- @Override
- public void onActivitySaveInstanceState(@NonNull Activity activity,
- @NonNull Bundle outState) {}
-
- @Override
- public void onActivityDestroyed(@NonNull Activity activity) {
- release();
- }
- };
-
- private static void cleanUpExistingTaskViewTasks(List<TaskAppearedInfo> taskAppearedInfos) {
- ActivityTaskManager atm = ActivityTaskManager.getInstance();
- for (TaskAppearedInfo taskAppearedInfo : taskAppearedInfos) {
- TaskInfo taskInfo = taskAppearedInfo.getTaskInfo();
- // Only TaskView tasks have WINDOWING_MODE_MULTI_WINDOW.
- if (taskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) {
- if (DBG) Slog.d(TAG, "Found the dangling task, removing: " + taskInfo.taskId);
- atm.removeTask(taskInfo.taskId);
- }
- }
- }
-
- @VisibleForTesting
- List<ControlledCarTaskView> getControlledTaskViews() {
- return mControlledTaskViews;
- }
-
- @VisibleForTesting
- LaunchRootCarTaskView getLaunchRootCarTaskView() {
- return mLaunchRootCarTaskView;
- }
-
- @VisibleForTesting
- List<SemiControlledCarTaskView> getSemiControlledTaskViews() {
- return mSemiControlledTaskViews;
- }
-
- @VisibleForTesting
- BroadcastReceiver getPackageBroadcastReceiver() {
- return mPackageBroadcastReceiver;
- }
-
- @VisibleForTesting
- /** Only meant for testing, should not be used by real code. */
- void setTaskViewInputInterceptor(TaskViewInputInterceptor taskViewInputInterceptor) {
- mTaskViewInputInterceptor = taskViewInputInterceptor;
- }
-
- public int getRootTaskCount() {
- return mLaunchRootCarTaskView != null ? mLaunchRootCarTaskView.getRootTaskCount() : 0;
- }
-
- /**
- * Returns the {@link android.app.ActivityManager.RunningTaskInfo} of the top task inside the
- * launch root car task view.
- */
- public ActivityManager.RunningTaskInfo getTopTaskInLaunchRootTask() {
- return mLaunchRootCarTaskView != null
- ? mLaunchRootCarTaskView.getTopTaskInLaunchRootTask() : null;
- }
-
- boolean isReleased() {
- return mReleased;
- }
-
- /**
- * Adds {@code activities} to allowed list of {@code carTaskView} if this car task view is a
- * known {@link SemiControlledCarTaskView}.
- */
- public void addAllowListedActivities(@NonNull CarTaskView carTaskView,
- List<ComponentName> activities) {
- if (activities.size() == 0) {
- if (DBG) {
- Log.d(TAG, "No activity to add to allowlist");
- }
- return;
- }
- for (SemiControlledCarTaskView semiControlledCarTaskView: mSemiControlledTaskViews) {
- if (semiControlledCarTaskView.equals(carTaskView)) {
- semiControlledCarTaskView.addAllowListedActivities(activities);
- return;
- }
- }
- }
-
- /**
- * Removes {@code activities} from allowed list of {@code carTaskView} if this CarTaskView is a
- * known SemiControlledCarTaskView.
- */
- public void removeAllowListedActivities(@NonNull CarTaskView carTaskView,
- List<ComponentName> activities) {
- if (activities.size() == 0) {
- if (DBG) {
- Log.d(TAG, "No activity to remove from allowlist");
- }
- return;
- }
- for (SemiControlledCarTaskView semiControlledCarTaskView: mSemiControlledTaskViews) {
- if (semiControlledCarTaskView.equals(carTaskView)) {
- semiControlledCarTaskView.removeAllowListedActivities(activities);
- return;
- }
- }
- }
-
- /**
- * Sets {@code activities} to be the allowed list of {@code carTaskView} if this CarTaskView
- * is a known SemiControlledCarTaskView.
- */
- public void setAllowListedActivities(CarTaskView carTaskView, List<ComponentName> activities) {
- for (SemiControlledCarTaskView semiControlledCarTaskView: mSemiControlledTaskViews) {
- if (semiControlledCarTaskView.equals(carTaskView)) {
- semiControlledCarTaskView.setAllowListedActivities(activities);
- return;
- }
- }
- }
-}
diff --git a/app/src/com/android/car/carlauncher/calmmode/CalmModeActivity.java b/app/src/com/android/car/carlauncher/calmmode/CalmModeActivity.java
index ce0f7d6..69a15ad 100644
--- a/app/src/com/android/car/carlauncher/calmmode/CalmModeActivity.java
+++ b/app/src/com/android/car/carlauncher/calmmode/CalmModeActivity.java
@@ -17,7 +17,10 @@
package com.android.car.carlauncher.calmmode;
import android.os.Bundle;
+import android.view.View;
+import android.view.WindowInsets;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
@@ -29,5 +32,15 @@
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.calm_mode_activity);
+
+ getWindow().getDecorView().setOnApplyWindowInsetsListener(
+ new View.OnApplyWindowInsetsListener() {
+ @NonNull
+ @Override
+ public WindowInsets onApplyWindowInsets(
+ @NonNull View v, @NonNull WindowInsets insets) {
+ return insets;
+ }
+ });
}
}
diff --git a/app/src/com/android/car/carlauncher/calmmode/CalmModeFragment.java b/app/src/com/android/car/carlauncher/calmmode/CalmModeFragment.java
index 7af6fdd..124cffa 100644
--- a/app/src/com/android/car/carlauncher/calmmode/CalmModeFragment.java
+++ b/app/src/com/android/car/carlauncher/calmmode/CalmModeFragment.java
@@ -20,43 +20,53 @@
import android.os.Build;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.WindowInsets;
+import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import androidx.constraintlayout.widget.Group;
+import androidx.core.animation.Animator;
+import androidx.core.animation.AnimatorInflater;
+import androidx.core.animation.AnimatorSet;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.LiveData;
-import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModelProvider;
import com.android.car.carlauncher.R;
+import com.android.car.media.common.MediaItemMetadata;
import com.android.car.media.common.playback.PlaybackViewModel;
+import java.util.ArrayList;
+import java.util.List;
+
public final class CalmModeFragment extends Fragment {
private static final String TAG = CalmModeFragment.class.getSimpleName();
private static final boolean DEBUG = Build.isDebuggable();
private View mContainerView;
- private Group mMediaGroup;
- private Group mNavGroup;
- private Group mTemperatureGroup;
private TextView mMediaTitleView;
private TextView mNavStateView;
private TextView mTemperatureView;
private TextView mClockView;
private TextView mDateView;
+ private ImageView mNavStateIconView;
+ private TextView mTemperatureIconView;
private ViewModelProvider mViewModelProvider;
private TemperatureViewModel mTemperatureViewModel;
private LiveData<TemperatureData> mTemperatureData;
private PlaybackViewModel mPlaybackViewModel;
-
@Nullable
private NavigationStateViewModel mNavigationStateViewModel;
+ private boolean mShowEntryAnimations;
+ private final List<View> mViewsPendingAnimation = new ArrayList<>();
+ private final AnimatorSet mAnimatorSet = new AnimatorSet();
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@@ -72,23 +82,35 @@
mContainerView = rootView.findViewById(R.id.calm_mode_container);
mClockView = rootView.findViewById(R.id.clock);
mDateView = rootView.findViewById(R.id.date);
- mNavGroup = rootView.findViewById(R.id.nav_group);
mNavStateView = rootView.findViewById(R.id.nav_state);
- mTemperatureGroup = rootView.findViewById(R.id.temperature_group);
+ mNavStateIconView = rootView.findViewById(R.id.nav_state_icon);
+ mTemperatureIconView = rootView.findViewById(R.id.temperature_icon);
mTemperatureView = rootView.findViewById(R.id.temperature);
- mMediaGroup = rootView.findViewById(R.id.media_group);
mMediaTitleView = rootView.findViewById(R.id.media_title);
+ mShowEntryAnimations = true;
+ hideStatusBar();
initExitOnClick();
- initClock();
initDate();
initNavState();
initTemperature();
initMediaTitle();
+ initClockAndPlayEntryAnimations();
return rootView;
}
+ private void hideStatusBar() {
+ final Handler handler = new Handler(Looper.getMainLooper());
+ handler.postDelayed(
+ () -> {
+ if (getActivity() == null) return;
+ getActivity().getWindow().getDecorView().getWindowInsetsController()
+ .hide(WindowInsets.Type.statusBars());
+ },
+ getResources().getInteger(R.integer.calm_mode_activity_fade_duration));
+ }
+
private void initMediaTitle() {
if (DEBUG) {
Log.v(TAG, "initMediaTitle()");
@@ -96,12 +118,7 @@
if (shouldShowMedia()) {
mPlaybackViewModel = PlaybackViewModel.get(requireActivity().getApplication(),
MEDIA_SOURCE_MODE_PLAYBACK);
- // Transform LiveData from MediaItemMetadata into a media title CharSequence
- // If MediaItemMetadata is null, media title value will be null
- LiveData<CharSequence> mediaTitleLiveData = Transformations.map(
- mPlaybackViewModel.getMetadata(), mediaItemMetadata ->
- mediaItemMetadata == null ? null : mediaItemMetadata.getTitle());
- mediaTitleLiveData.observe(this, this::updateMediaTitle);
+ mPlaybackViewModel.getMetadata().observe(this, this::updateMediaTitle);
}
}
@@ -120,17 +137,96 @@
if (DEBUG) {
Log.v(TAG, "initDate()");
}
- if (shouldShowDate()) {
+ if (!shouldShowDate()) {
+ return;
+ }
+ if (mShowEntryAnimations) {
+ mDateView.setVisibility(View.INVISIBLE);
+ mViewsPendingAnimation.add(mDateView);
+ } else {
mDateView.setVisibility(View.VISIBLE);
}
+
}
- private void initClock() {
+ private void playEntryAnimations() {
+ mShowEntryAnimations = false;
+ List<Animator> animList = new ArrayList<>();
+
+ for (View view : mViewsPendingAnimation) {
+ if (getContext() == null) {
+ return;
+ }
+ Animator animator = AnimatorInflater.loadAnimator(getContext(),
+ R.animator.calm_mode_enter);
+ animator.setTarget(view);
+ Animator.AnimatorListener animatorListener = new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(@NonNull Animator animation) {
+ view.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationEnd(@NonNull Animator animation) {
+
+ }
+
+ @Override
+ public void onAnimationCancel(@NonNull Animator animation) {
+
+ }
+
+ @Override
+ public void onAnimationRepeat(@NonNull Animator animation) {
+
+ }
+ };
+ animator.addListener(animatorListener);
+ animList.add(animator);
+ }
+ mViewsPendingAnimation.clear();
+ mAnimatorSet.setDuration(getResources().getInteger(
+ R.integer.calm_mode_content_fade_duration));
+ mAnimatorSet.playTogether(animList);
+ mAnimatorSet.start();
+ }
+ private void initClockAndPlayEntryAnimations() {
if (DEBUG) {
Log.v(TAG, "initClock()");
}
if (shouldShowClock()) {
- mClockView.setVisibility(View.VISIBLE);
+ mClockView.setVisibility(View.INVISIBLE);
+ if (getContext() == null) {
+ return;
+ }
+ Animator animator = AnimatorInflater.loadAnimator(getContext(),
+ R.animator.calm_mode_enter);
+ animator.setStartDelay(getResources().getInteger(
+ R.integer.calm_mode_activity_fade_duration));
+ animator.setTarget(mClockView);
+
+ animator.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(@NonNull Animator animation) {
+ mClockView.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationEnd(@NonNull Animator animation) {
+ playEntryAnimations();
+ }
+
+ @Override
+ public void onAnimationCancel(@NonNull Animator animation) {
+
+ }
+
+ @Override
+ public void onAnimationRepeat(@NonNull Animator animation) {
+
+ }
+ });
+ animator.start();
}
}
@@ -183,14 +279,24 @@
private void updateTemperatureData(@Nullable TemperatureData temperatureData) {
if (temperatureData == null) {
- mTemperatureGroup.setVisibility(View.GONE);
+ mTemperatureView.setVisibility(View.GONE);
+ mTemperatureIconView.setVisibility(View.GONE);
mTemperatureView.setText("");
return;
}
- mTemperatureGroup.setVisibility(View.VISIBLE);
+ if (mShowEntryAnimations) {
+ mTemperatureView.setVisibility(View.INVISIBLE);
+ mTemperatureIconView.setVisibility(View.INVISIBLE);
+ mViewsPendingAnimation.add(mTemperatureView);
+ mViewsPendingAnimation.add(mTemperatureIconView);
+ } else {
+ mTemperatureView.setVisibility(View.VISIBLE);
+ mTemperatureIconView.setVisibility(View.VISIBLE);
+ }
mTemperatureView.setText(
TemperatureData.buildTemperatureString(
- temperatureData, getResources().getConfiguration().getLocales().get(0)));
+ temperatureData, getResources().getConfiguration().getLocales().get(0),
+ /* showUnit = */ false));
}
private void updateNavigationState(NavigationStateData navState) {
@@ -199,29 +305,74 @@
}
if (navState == null) {
- mNavGroup.setVisibility(View.GONE);
- mNavStateView.setText(null);
+ mNavStateView.setVisibility(View.GONE);
+ mNavStateIconView.setVisibility(View.GONE);
+ mNavStateView.setText("");
return;
}
- mNavGroup.setVisibility(View.VISIBLE);
+ if (mShowEntryAnimations) {
+ mNavStateView.setVisibility(View.INVISIBLE);
+ mNavStateIconView.setVisibility(View.INVISIBLE);
+ mViewsPendingAnimation.add(mNavStateView);
+ mViewsPendingAnimation.add(mNavStateIconView);
+ } else {
+ mNavStateView.setVisibility(View.VISIBLE);
+ mNavStateIconView.setVisibility(View.VISIBLE);
+ }
mNavStateView.setText(
NavigationStateData.buildTripStatusString(navState,
- getResources().getConfiguration().getLocales().get(0)));
+ getResources().getConfiguration().getLocales().get(0),
+ getResources().getString(R.string.calm_mode_separator)));
}
@VisibleForTesting
- void updateMediaTitle(CharSequence mediaTitle) {
+ void updateMediaTitle(MediaItemMetadata mediaItemMetadata) {
if (DEBUG) {
- Log.v(TAG, "updateMediaTitle mediaTitle = " + mediaTitle);
+ Log.v(TAG, "updateMediaTitle mediaItemMetadata = " + mediaItemMetadata);
}
-
- if (mediaTitle == null || mediaTitle.length() == 0) {
- mMediaGroup.setVisibility(View.GONE);
+ if (mediaItemMetadata == null || mediaItemMetadata.getTitle() == null
+ || mediaItemMetadata.getTitle().length() == 0) {
+ mMediaTitleView.setVisibility(View.GONE);
+ mClockView.setTranslationY(0f);
mMediaTitleView.setText("");
return;
}
- mMediaGroup.setVisibility(View.VISIBLE);
- mMediaTitleView.setText(mediaTitle);
+
+ StringBuilder medaTitleBuilder = new StringBuilder();
+ medaTitleBuilder.append(mediaItemMetadata.getTitle());
+
+ if (mediaItemMetadata.getSubtitle() != null
+ && mediaItemMetadata.getSubtitle().length() > 0) {
+ medaTitleBuilder.append(getResources().getString(R.string.calm_mode_separator));
+ medaTitleBuilder.append(mediaItemMetadata.getSubtitle());
+ }
+
+ mClockView.setTranslationY(
+ -getResources().getDimension(R.dimen.calm_mode_clock_translationY));
+ mContainerView.requestLayout();
+ if (mShowEntryAnimations) {
+ mMediaTitleView.setVisibility(View.INVISIBLE);
+ mViewsPendingAnimation.add(mMediaTitleView);
+ } else {
+ mMediaTitleView.setVisibility(View.VISIBLE);
+ }
+ mMediaTitleView.setText(medaTitleBuilder.toString());
}
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ int launchType = requireActivity().getIntent().getIntExtra(
+ CalmModeStatsLogHelper.INTENT_EXTRA_CALM_MODE_LAUNCH_TYPE,
+ CalmModeStatsLogHelper.CalmModeLaunchType.UNSPECIFIED_LAUNCH_TYPE);
+ CalmModeStatsLogHelper.getInstance().logSessionStarted(launchType);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ CalmModeStatsLogHelper.getInstance().logSessionFinished();
+ }
+
}
diff --git a/app/src/com/android/car/carlauncher/calmmode/CalmModeQCProvider.java b/app/src/com/android/car/carlauncher/calmmode/CalmModeQCProvider.java
index a4be84a..00306be 100644
--- a/app/src/com/android/car/carlauncher/calmmode/CalmModeQCProvider.java
+++ b/app/src/com/android/car/carlauncher/calmmode/CalmModeQCProvider.java
@@ -16,7 +16,9 @@
package com.android.car.carlauncher.calmmode;
+import android.app.ActivityOptions;
import android.app.PendingIntent;
+import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
@@ -35,6 +37,7 @@
import com.android.car.qc.provider.BaseQCProvider;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
/**
* Remote Quick Control provider for Calm mode in CarLauncher.
@@ -53,6 +56,7 @@
.build();
private Set<String> mAllowListedPackages;
private Context mContext;
+ private AtomicInteger mPendingIntentRequestCode = new AtomicInteger(0);
@VisibleForTesting
QCItem mQCItem;
@@ -103,17 +107,22 @@
@VisibleForTesting
QCItem getQCItem() {
Resources resources = mContext.getResources();
- String packageName = resources.getString(R.string.config_calmMode_packageName);
- String activityName = resources.getString(R.string.config_calmMode_activityName);
-
+ ComponentName componentName = ComponentName.unflattenFromString(
+ resources.getString(R.string.config_calmMode_componentName));
Intent intent = new Intent();
- intent.setClassName(packageName, activityName);
- PendingIntent ambientModeIntent = PendingIntent.getActivity(mContext, 0, intent,
- PendingIntent.FLAG_IMMUTABLE);
+ intent.setComponent(componentName);
+ intent.putExtra(CalmModeStatsLogHelper.INTENT_EXTRA_CALM_MODE_LAUNCH_TYPE,
+ CalmModeStatsLogHelper.CalmModeLaunchType.QUICK_CONTROLS);
+ ActivityOptions activityOptions = ActivityOptions.makeBasic()
+ .setPendingIntentCreatorBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
+ PendingIntent calmModeIntent = PendingIntent.getActivity(mContext,
+ mPendingIntentRequestCode.getAndAdd(1), intent,
+ PendingIntent.FLAG_IMMUTABLE, activityOptions.toBundle());
QCRow calmModeRow = new QCRow.Builder()
.setTitle(mContext.getString(R.string.calm_mode_title))
- .setPrimaryAction(ambientModeIntent)
+ .setPrimaryAction(calmModeIntent)
.build();
return new QCList.Builder().addRow(calmModeRow).build();
diff --git a/app/src/com/android/car/carlauncher/calmmode/CalmModeStatsLogHelper.java b/app/src/com/android/car/carlauncher/calmmode/CalmModeStatsLogHelper.java
new file mode 100644
index 0000000..5a437fd
--- /dev/null
+++ b/app/src/com/android/car/carlauncher/calmmode/CalmModeStatsLogHelper.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2023 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.carlauncher.calmmode;
+
+import android.annotation.IntDef;
+import android.os.Build;
+import android.util.Log;
+
+import com.android.car.carlauncher.CarLauncherStatsLog;
+
+import java.util.UUID;
+
+/**
+ * Helper class that directly interacts with {@link CarLauncherStatsLog}, a generated class that
+ * contains logging methods for CalmModeActivity.
+ * logging methods for CalmModeActivity.
+ */
+public class CalmModeStatsLogHelper {
+ private static final String TAG = CalmModeStatsLogHelper.class.getSimpleName();
+ public static final String INTENT_EXTRA_CALM_MODE_LAUNCH_TYPE =
+ CalmModeLaunchType.class.getSimpleName();
+ private static CalmModeStatsLogHelper sInstance;
+ private long mSessionId;
+
+ /**
+ * IntDef representing enum values of CarCalmModeEventReported.event_type.
+ */
+ @IntDef({
+ CalmModeEventType.UNSPECIFIED_EVENT_TYPE,
+ CalmModeEventType.SESSION_STARTED,
+ CalmModeEventType.SESSION_FINISHED,
+ })
+ public @interface CalmModeEventType {
+ int UNSPECIFIED_EVENT_TYPE =
+ CarLauncherStatsLog
+ .CAR_CALM_MODE_EVENT_REPORTED__EVENT_TYPE__UNSPECIFIED_EVENT_TYPE;
+ int SESSION_STARTED =
+ CarLauncherStatsLog.CAR_CALM_MODE_EVENT_REPORTED__EVENT_TYPE__SESSION_STARTED;
+ int SESSION_FINISHED =
+ CarLauncherStatsLog.CAR_CALM_MODE_EVENT_REPORTED__EVENT_TYPE__SESSION_FINISHED;
+ }
+
+ /**
+ * IntDef representing enum values of CarCalmModeEventReported.launch_type.
+ */
+ @IntDef({
+ CalmModeLaunchType.UNSPECIFIED_LAUNCH_TYPE,
+ CalmModeLaunchType.SETTINGS,
+ CalmModeLaunchType.QUICK_CONTROLS,
+ })
+ public @interface CalmModeLaunchType {
+ int UNSPECIFIED_LAUNCH_TYPE =
+ CarLauncherStatsLog
+ .CAR_CALM_MODE_EVENT_REPORTED__LAUNCH_TYPE__UNSPECIFIED_LAUNCH_TYPE;
+ int SETTINGS =
+ CarLauncherStatsLog.CAR_CALM_MODE_EVENT_REPORTED__LAUNCH_TYPE__SETTINGS;
+ int QUICK_CONTROLS =
+ CarLauncherStatsLog.CAR_CALM_MODE_EVENT_REPORTED__LAUNCH_TYPE__QUICK_CONTROLS;
+ }
+
+ /**
+ * Returns the current logging instance of CalmModeStatsLogHelper to write this devices'
+ * CarLauncherStatsModule.
+ *
+ * @return the logging instance of CalmModeStatsLogHelper.
+ */
+ public static CalmModeStatsLogHelper getInstance() {
+ if (sInstance == null) {
+ sInstance = new CalmModeStatsLogHelper();
+ }
+ return sInstance;
+ }
+
+ /**
+ * Logs that a new Calm mode session has started. Additionally, resets measurements and IDs such
+ * as session ID and start time.
+ */
+ public void logSessionStarted(@CalmModeLaunchType int launchType) {
+ mSessionId = UUID.randomUUID().getMostSignificantBits();
+ writeCarCalmModeEventReported(CalmModeEventType.SESSION_STARTED, launchType);
+ }
+
+ /**
+ * Logs that the current Calm mode session has finished.
+ */
+ public void logSessionFinished() {
+ writeCarCalmModeEventReported(CalmModeEventType.SESSION_FINISHED);
+ }
+
+ /**
+ * Writes to CarCalmModeEvent atom with {@code eventType} as the only field, and log all other
+ * fields as unspecified.
+ *
+ * @param eventType one of {@link CalmModeEventType}
+ */
+ private void writeCarCalmModeEventReported(int eventType) {
+ writeCarCalmModeEventReported(
+ eventType, /* launchType */ CalmModeLaunchType.UNSPECIFIED_LAUNCH_TYPE);
+ }
+
+ /**
+ * Writes to CarCalmModeEvent atom with all the optional fields filled.
+ *
+ * @param eventType one of {@link CalmModeEventType}
+ * @param launchType one of {@link CalmModeLaunchType}
+ */
+ private void writeCarCalmModeEventReported(int eventType, int launchType) {
+ long eventId = UUID.randomUUID().getMostSignificantBits();
+ if (Build.isDebuggable()) {
+ Log.v(TAG, "writing CAR_CALM_MODE_EVENT_REPORTED. sessionId=" + mSessionId
+ + ", eventId=" + eventId + "eventType= " + eventType
+ + ", launchType=" + launchType);
+ }
+ CarLauncherStatsLog.write(
+ /* atomId */ CarLauncherStatsLog.CAR_CALM_MODE_EVENT_REPORTED,
+ /* sessionId */ mSessionId,
+ /* eventId */ eventId,
+ /* eventType */ eventType,
+ /* launchType */ launchType);
+ }
+}
diff --git a/app/src/com/android/car/carlauncher/calmmode/NavigationStateData.java b/app/src/com/android/car/carlauncher/calmmode/NavigationStateData.java
index 632a112..87cfafb 100644
--- a/app/src/com/android/car/carlauncher/calmmode/NavigationStateData.java
+++ b/app/src/com/android/car/carlauncher/calmmode/NavigationStateData.java
@@ -32,7 +32,6 @@
public class NavigationStateData {
private static final boolean DEBUG = Build.isDebuggable();
private static final String TAG = NavigationStateData.class.getSimpleName();
- private static final String SEPARATOR = " ";
@NonNull
private final String mTimeToDestination;
@@ -49,7 +48,7 @@
}
/**
- * @return new Builder instance for creating NavigationStateData
+ * @return new {@link Builder} instance for creating {@link NavigationStateData}
*/
@NonNull
public static Builder newBuilder() {
@@ -57,14 +56,18 @@
}
/**
- * @param navigationState NavigationStateData to use for building Trip Status string
- * @param locale Locale to build Trip Status string
- * @return String representing trip status with time and distance to next destination,
- * returns null if arguments are invalid, uses default locale if input Locale is null
+ * Trip Status is a string containing time and distance remaining to reach next destination
+ * Example: "1hr 52 min | 100mi"
+ * @param navigationState {@link NavigationStateData} to use for building Trip Status string
+ * @param locale {@link Locale} to build Trip Status string
+ * @param separator Separator used to separate time and distance
+ * @return String representing trip status with time and distance to next destination, returns
+ * null if arguments are invalid, uses {@link Locale#getDefault()} if {@code locale} is null
*/
@Nullable
public static String buildTripStatusString(
- @NonNull NavigationStateData navigationState, @NonNull Locale locale) {
+ @NonNull NavigationStateData navigationState, @NonNull Locale locale,
+ @NonNull String separator) {
if (navigationState == null
|| navigationState.getTimeToDestination() == null
@@ -86,7 +89,7 @@
}
StringBuilder navStateTextBuilder = new StringBuilder();
navStateTextBuilder.append(navigationState.getTimeToDestination());
- navStateTextBuilder.append(SEPARATOR);
+ navStateTextBuilder.append(separator);
navStateTextBuilder.append(NumberFormatter.withLocale(locale)
.notation(Notation.compactShort())
.precision(Precision.integer())
diff --git a/app/src/com/android/car/carlauncher/calmmode/NavigationStateViewModel.java b/app/src/com/android/car/carlauncher/calmmode/NavigationStateViewModel.java
index 225207e..622703e 100644
--- a/app/src/com/android/car/carlauncher/calmmode/NavigationStateViewModel.java
+++ b/app/src/com/android/car/carlauncher/calmmode/NavigationStateViewModel.java
@@ -65,7 +65,7 @@
@Override
- public void onNavigationState(byte[] navigationStateByteArr) {
+ public void onNavigationStateChanged(byte[] navigationStateByteArr) {
if (DEBUG) {
Log.v(TAG, "ClusterNavigationStateListener onNavigationState");
}
diff --git a/app/src/com/android/car/carlauncher/calmmode/TemperatureData.java b/app/src/com/android/car/carlauncher/calmmode/TemperatureData.java
index a26ba0a..08d8e94 100644
--- a/app/src/com/android/car/carlauncher/calmmode/TemperatureData.java
+++ b/app/src/com/android/car/carlauncher/calmmode/TemperatureData.java
@@ -52,11 +52,11 @@
*/
@NonNull
public static String buildTemperatureString(
- @NonNull TemperatureData temperatureData, @NonNull Locale locale) {
+ @NonNull TemperatureData temperatureData, @NonNull Locale locale, boolean showUnit) {
return NumberFormatter.withLocale(locale)
.notation(Notation.compactShort())
.precision(Precision.integer())
- .unit(temperatureData.getUnit())
+ .unit(showUnit ? temperatureData.getUnit() : MeasureUnit.GENERIC_TEMPERATURE)
.format(temperatureData.getValue())
.toString();
}
diff --git a/app/src/com/android/car/carlauncher/homescreen/HomeCardFragment.java b/app/src/com/android/car/carlauncher/homescreen/HomeCardFragment.java
index 51a1eb3..fc01e07 100644
--- a/app/src/com/android/car/carlauncher/homescreen/HomeCardFragment.java
+++ b/app/src/com/android/car/carlauncher/homescreen/HomeCardFragment.java
@@ -123,6 +123,7 @@
mRootView = inflater.inflate(R.layout.card_fragment, container, false);
mCardTitle = mRootView.findViewById(R.id.card_name);
mCardIcon = mRootView.findViewById(R.id.card_icon);
+ mCardBackgroundImage = mRootView.findViewById(R.id.card_background_image);
return mRootView;
}
@@ -174,16 +175,6 @@
}
/**
- * Returns the size of the card or null if the view hasn't yet been laid out
- */
- protected Size getCardSize() {
- if (mSize == null && mRootView.isLaidOut()) {
- mSize = new Size(mRootView.getWidth(), mRootView.getHeight());
- }
- return mSize;
- }
-
- /**
* Removes the audio card from view
*/
@Override
diff --git a/app/src/com/android/car/carlauncher/homescreen/audio/AudioCardFragment.java b/app/src/com/android/car/carlauncher/homescreen/audio/AudioCardFragment.java
new file mode 100644
index 0000000..25f0ad8
--- /dev/null
+++ b/app/src/com/android/car/carlauncher/homescreen/audio/AudioCardFragment.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.homescreen.audio;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+
+import com.android.car.carlauncher.R;
+import com.android.car.carlauncher.homescreen.HomeCardFragment;
+import com.android.car.carlauncher.homescreen.HomeCardInterface;
+import com.android.car.carlauncher.homescreen.audio.dialer.DialerCardFragment;
+import com.android.car.carlauncher.homescreen.audio.media.MediaCardFragment;
+
+/**
+ * Fragment used to display the audio related controls.
+ */
+public class AudioCardFragment extends Fragment implements HomeCardInterface.View {
+ private View mRootView;
+
+ private MediaCardFragment mMediaFragment;
+ private DialerCardFragment mInCallFragment;
+ private boolean mViewCreated;
+
+ private HomeCardFragment.OnViewLifecycleChangeListener mOnViewLifecycleChangeListener;
+
+ /**
+ * Register a callback to be invoked when the fragment lifecycle changes.
+ *
+ * @param onViewLifecycleChangeListener The callback that will run
+ */
+ public void setOnViewLifecycleChangeListener(
+ HomeCardFragment.OnViewLifecycleChangeListener onViewLifecycleChangeListener) {
+ mOnViewLifecycleChangeListener = onViewLifecycleChangeListener;
+ if (mViewCreated) {
+ mOnViewLifecycleChangeListener.onViewCreated();
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mRootView = inflater.inflate(R.layout.card_fragment_audio_card, container, false);
+ return mRootView;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ mViewCreated = true;
+ mMediaFragment = createMediaFragment();
+ mInCallFragment = new DialerCardFragment();
+
+ FragmentTransaction ft = getChildFragmentManager().beginTransaction();
+ ft.replace(R.id.media_fragment_container, mMediaFragment, mMediaFragment.getTag());
+ ft.replace(R.id.in_call_fragment_container, mInCallFragment);
+ ft.commitNow();
+
+ FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
+ transaction.hide(mInCallFragment);
+ transaction.commit();
+
+ if (mOnViewLifecycleChangeListener != null) {
+ mOnViewLifecycleChangeListener.onViewCreated();
+ }
+ }
+
+ protected MediaCardFragment createMediaFragment() {
+ return new MediaCardFragment();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mViewCreated = false;
+ if (mOnViewLifecycleChangeListener != null) {
+ mOnViewLifecycleChangeListener.onViewDestroyed();
+ }
+ }
+
+ @Override
+ public Fragment getFragment() {
+ return this;
+ }
+
+ @Override
+ public void hideCard() {
+ mRootView.setVisibility(View.GONE);
+ }
+
+ public MediaCardFragment getMediaFragment() {
+ return mMediaFragment;
+ }
+
+ public DialerCardFragment getInCallFragment() {
+ return mInCallFragment;
+ }
+
+ /** Does a fragment transaction to show the media card and hide the dialer card */
+ public void showMediaCard() {
+ FragmentManager fragmentManager = getChildFragmentManager();
+ FragmentTransaction transaction = fragmentManager.beginTransaction();
+ transaction.show(mMediaFragment);
+ transaction.hide(mInCallFragment);
+ transaction.commitAllowingStateLoss();
+ }
+
+ /** Does a fragment transaction to show the dialer card and hide the media card */
+ public void showInCallCard() {
+ FragmentManager fragmentManager = getChildFragmentManager();
+ FragmentTransaction transaction = fragmentManager.beginTransaction();
+ transaction.hide(mMediaFragment);
+ transaction.show(mInCallFragment);
+ transaction.commitAllowingStateLoss();
+ }
+}
diff --git a/app/src/com/android/car/carlauncher/homescreen/audio/AudioCardModel.java b/app/src/com/android/car/carlauncher/homescreen/audio/AudioCardModel.java
new file mode 100644
index 0000000..f24281c
--- /dev/null
+++ b/app/src/com/android/car/carlauncher/homescreen/audio/AudioCardModel.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.homescreen.audio;
+
+import android.os.SystemClock;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.car.carlauncher.homescreen.HomeCardInterface;
+import com.android.car.carlauncher.homescreen.audio.dialer.DialerCardModel;
+
+/** A wrapper around {@code MediaViewModel} and {@code InCallModel}. */
+public class AudioCardModel implements HomeCardInterface.Model {
+
+ private final MediaViewModel mMediaViewModel;
+ private final InCallModel mInCallViewModel;
+
+ public AudioCardModel(@NonNull ViewModelProvider viewModelProvider) {
+ mMediaViewModel = viewModelProvider.get(MediaViewModel.class);
+ mInCallViewModel = new DialerCardModel(SystemClock.elapsedRealtimeClock());
+ }
+
+ MediaViewModel getMediaViewModel() {
+ return mMediaViewModel;
+ }
+
+ InCallModel getInCallViewModel() {
+ return mInCallViewModel;
+ }
+
+ @Override
+ public void setOnModelUpdateListener(OnModelUpdateListener onModelUpdateListener) {
+ // No-op
+ }
+}
diff --git a/app/src/com/android/car/carlauncher/homescreen/audio/AudioCardModule.java b/app/src/com/android/car/carlauncher/homescreen/audio/AudioCardModule.java
index 6ffb692..fec78a8 100644
--- a/app/src/com/android/car/carlauncher/homescreen/audio/AudioCardModule.java
+++ b/app/src/com/android/car/carlauncher/homescreen/audio/AudioCardModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -16,37 +16,34 @@
package com.android.car.carlauncher.homescreen.audio;
-import android.os.SystemClock;
-import android.util.Log;
-
import androidx.lifecycle.ViewModelProvider;
import com.android.car.carlauncher.R;
-import com.android.car.carlauncher.homescreen.CardPresenter;
-import com.android.car.carlauncher.homescreen.HomeCardFragment;
+import com.android.car.carlauncher.homescreen.HomeCardInterface;
import com.android.car.carlauncher.homescreen.HomeCardModule;
-
-import java.util.Arrays;
-import java.util.Collections;
+import com.android.car.carlauncher.homescreen.audio.dialer.DialerCardPresenter;
+import com.android.car.carlauncher.homescreen.audio.media.MediaCardPresenter;
/**
- * Home screen card that displays audio related content
+ * The Audio card module. This class initializes the necessary components to present an audio card
+ * as a home module.
*/
public class AudioCardModule implements HomeCardModule {
-
- private static final String TAG = "HomeScreenAudioCard";
-
- private ViewModelProvider mViewModelProvider;
- private HomeAudioCardPresenter mAudioCardPresenter;
- private AudioFragment mAudioCardView;
-
+ protected AudioCardPresenter mAudioCardPresenter;
+ protected HomeCardInterface.View mAudioCardView;
+ protected ViewModelProvider mViewModelProvider;
@Override
public void setViewModelProvider(ViewModelProvider viewModelProvider) {
+ if (mViewModelProvider != null) {
+ throw new IllegalStateException("Cannot reset the view model provider");
+ }
mViewModelProvider = viewModelProvider;
- }
- protected ViewModelProvider getViewModelProvider() {
- return mViewModelProvider;
+ mAudioCardPresenter = new AudioCardPresenter(
+ new DialerCardPresenter(), new MediaCardPresenter());
+ mAudioCardPresenter.setModel(new AudioCardModel(mViewModelProvider));
+ mAudioCardView = new AudioCardFragment();
+ mAudioCardPresenter.setView(mAudioCardView);
}
@Override
@@ -55,29 +52,12 @@
}
@Override
- public CardPresenter getCardPresenter() {
- if (mAudioCardPresenter == null) {
- mAudioCardPresenter = new HomeAudioCardPresenter();
- if (mViewModelProvider == null) {
- Log.w(TAG, "No ViewModelProvider set. Cannot get MediaViewModel");
- mAudioCardPresenter.setModels(
- Collections.unmodifiableList(Collections.singletonList(
- new InCallModel(SystemClock.elapsedRealtimeClock()))));
- } else {
- mAudioCardPresenter.setModels(Collections.unmodifiableList(Arrays.asList(
- mViewModelProvider.get(MediaViewModel.class),
- new InCallModel(SystemClock.elapsedRealtimeClock()))));
- }
- }
+ public HomeCardInterface.Presenter getCardPresenter() {
return mAudioCardPresenter;
}
@Override
- public HomeCardFragment getCardView() {
- if (mAudioCardView == null) {
- mAudioCardView = new AudioFragment();
- getCardPresenter().setView(mAudioCardView);
- }
+ public HomeCardInterface.View getCardView() {
return mAudioCardView;
}
}
diff --git a/app/src/com/android/car/carlauncher/homescreen/audio/AudioCardPresenter.java b/app/src/com/android/car/carlauncher/homescreen/audio/AudioCardPresenter.java
new file mode 100644
index 0000000..a03522c
--- /dev/null
+++ b/app/src/com/android/car/carlauncher/homescreen/audio/AudioCardPresenter.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.homescreen.audio;
+
+import com.android.car.carlauncher.Flags;
+import com.android.car.carlauncher.homescreen.CardPresenter;
+import com.android.car.carlauncher.homescreen.HomeCardFragment;
+import com.android.car.carlauncher.homescreen.HomeCardInterface;
+import com.android.car.carlauncher.homescreen.audio.dialer.DialerCardPresenter;
+import com.android.car.carlauncher.homescreen.audio.media.MediaCardPresenter;
+
+import java.util.List;
+
+/**
+ * Presenter used to coordinate the binding between the audio card model and presentation
+ */
+public class AudioCardPresenter extends CardPresenter {
+
+ // Presenter for the dialer card
+ private final DialerCardPresenter mDialerPresenter;
+
+ // Presenter for the media card
+ private final MediaCardPresenter mMediaPresenter;
+
+ // The fragment controlled by this presenter.
+ private AudioCardFragment mFragment;
+
+ private final HomeCardFragment.OnViewLifecycleChangeListener mOnViewLifecycleChangeListener =
+ new HomeCardFragment.OnViewLifecycleChangeListener() {
+ @Override
+ public void onViewCreated() {
+ mDialerPresenter.setView(mFragment.getInCallFragment());
+ if (!Flags.mediaCardFullscreen()) {
+ mMediaPresenter.setView(mFragment.getMediaFragment());
+ }
+ }
+
+ @Override
+ public void onViewDestroyed() {
+ }
+ };
+
+ public AudioCardPresenter(DialerCardPresenter dialerPresenter,
+ MediaCardPresenter mediaPresenter) {
+ mDialerPresenter = dialerPresenter;
+ mMediaPresenter = mediaPresenter;
+
+ mDialerPresenter.setOnInCallStateChangeListener(hasActiveCall -> {
+ if (hasActiveCall) {
+ if (!Flags.mediaCardFullscreen()) {
+ mMediaPresenter.setShowMedia(false);
+ }
+ mFragment.showInCallCard();
+ } else {
+ if (!Flags.mediaCardFullscreen()) {
+ mMediaPresenter.setShowMedia(true);
+ }
+ mFragment.showMediaCard();
+ }
+ });
+ }
+
+ // Deprecated. Use setModel instead.
+ @Override
+ public void setModels(List<HomeCardInterface.Model> models) {
+ // No-op
+ }
+
+ /** Sets the model for this presenter. */
+ public void setModel(AudioCardModel viewModel) {
+ mDialerPresenter.setModel(viewModel.getInCallViewModel());
+ if (!Flags.mediaCardFullscreen()) {
+ mMediaPresenter.setModel(viewModel.getMediaViewModel());
+ }
+ }
+
+ @Override
+ public void setView(HomeCardInterface.View view) {
+ super.setView(view);
+ mFragment = (AudioCardFragment) view;
+ mFragment.setOnViewLifecycleChangeListener(mOnViewLifecycleChangeListener);
+ }
+}
diff --git a/app/src/com/android/car/carlauncher/homescreen/audio/HomeAudioCardPresenter.java b/app/src/com/android/car/carlauncher/homescreen/audio/HomeAudioCardPresenter.java
deleted file mode 100644
index 9579975..0000000
--- a/app/src/com/android/car/carlauncher/homescreen/audio/HomeAudioCardPresenter.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * Copyright (C) 2020 Google Inc.
- *
- * 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.carlauncher.homescreen.audio;
-
-import android.app.ActivityOptions;
-import android.content.Intent;
-import android.view.Display;
-
-import com.android.car.carlauncher.homescreen.CardPresenter;
-import com.android.car.carlauncher.homescreen.HomeCardFragment;
-import com.android.car.carlauncher.homescreen.HomeCardInterface;
-import com.android.car.carlauncher.homescreen.ui.DescriptiveTextWithControlsView;
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.util.List;
-
-/**
- * The {@link CardPresenter} for an audio card.
- *
- * For the audio card, the {@link AudioFragment} implements the View and displays information on
- * media from a {@link MediaViewModel}.
- */
-public class HomeAudioCardPresenter extends CardPresenter {
-
- private AudioFragment mAudioFragment;
-
- private AudioModel mCurrentModel;
- private List<HomeCardInterface.Model> mModelList;
- private MediaViewModel mMediaViewModel;
-
- private HomeCardFragment.OnViewClickListener mOnViewClickListener =
- new HomeCardFragment.OnViewClickListener() {
- @Override
- public void onViewClicked() {
- Intent intent = mCurrentModel.getIntent();
- if (intent != null) {
- ActivityOptions options = ActivityOptions.makeBasic();
- options.setLaunchDisplayId(Display.DEFAULT_DISPLAY);
- mAudioFragment.getContext().startActivity(intent, options.toBundle());
- }
- }
- };
-
- private HomeCardFragment.OnViewLifecycleChangeListener mOnViewLifecycleChangeListener =
- new HomeCardFragment.OnViewLifecycleChangeListener() {
- @Override
- public void onViewCreated() {
- for (HomeCardInterface.Model model : mModelList) {
- if (model.getClass() == MediaViewModel.class) {
- mMediaViewModel = (MediaViewModel) model;
- mMediaViewModel.setOnProgressUpdateListener(
- mOnProgressUpdateListener);
- }
- model.setOnModelUpdateListener(mOnModelUpdateListener);
- model.onCreate(getFragment().requireContext());
- }
- }
-
- @Override
- public void onViewDestroyed() {
- if (mModelList != null) {
- for (HomeCardInterface.Model model : mModelList) {
- model.onDestroy(getFragment().requireContext());
- }
- }
- }
- };
-
- @VisibleForTesting
- HomeCardInterface.Model.OnModelUpdateListener mOnModelUpdateListener =
- new HomeCardInterface.Model.OnModelUpdateListener() {
- @Override
- public void onModelUpdate(HomeCardInterface.Model model) {
- AudioModel audioModel = (AudioModel) model;
- // Null card header indicates the model has no content to display
- if (audioModel.getCardHeader() == null) {
- if (mCurrentModel != null
- && audioModel.getClass() == mCurrentModel.getClass()) {
- // If the model currently on display is updating to empty content,
- // check if there
- // is media content to display. If there is no media content the
- // super method is
- // called with empty content, which hides the card.
- if (mMediaViewModel != null
- && mMediaViewModel.getCardHeader() != null) {
- mCurrentModel = mMediaViewModel;
- updateCurrentModelInFragment();
- return;
- }
- } else {
- // Otherwise, another model is already on display, so don't update
- // with this
- // empty content since that would hide the card.
- return;
- }
- } else if (mCurrentModel != null
- && mCurrentModel.getClass() == InCallModel.class
- && audioModel.getClass() != InCallModel.class) {
- // If the Model has content, check if currentModel on display is an
- // ongoing phone call.
- // If there is any ongoing phone call, do not update the View
- // if the model trying to update View is NOT a phone call.
- return;
- }
- mCurrentModel = audioModel;
- updateCurrentModelInFragment();
- }
- };
-
- @VisibleForTesting
- AudioModel.OnProgressUpdateListener mOnProgressUpdateListener =
- new AudioModel.OnProgressUpdateListener() {
- @Override
- public void onProgressUpdate(AudioModel model, boolean updateProgress) {
- if (model == null || model.getCardContent() == null
- || model.getCardHeader() == null) {
- return;
- }
- DescriptiveTextWithControlsView descriptiveTextWithControlsContent =
- (DescriptiveTextWithControlsView) model.getCardContent();
- mAudioFragment.updateProgress(
- descriptiveTextWithControlsContent.getSeekBarViewModel(),
- updateProgress);
- }
- };
-
- private AudioFragment.OnMediaViewInitializedListener mOnMediaViewInitializedListener =
- new AudioFragment.OnMediaViewInitializedListener() {
- @Override
- public void onMediaViewInitialized() {
- // set playbackviewmodel on playback control actions view
- mAudioFragment.getPlaybackControlsActionBar().setModel(
- mMediaViewModel.getPlaybackViewModel(),
- mAudioFragment.getViewLifecycleOwner());
- }
- };
-
- @Override
- public void setView(HomeCardInterface.View view) {
- super.setView(view);
- mAudioFragment = (AudioFragment) view;
- mAudioFragment.setOnViewLifecycleChangeListener(mOnViewLifecycleChangeListener);
- mAudioFragment.setOnViewClickListener(mOnViewClickListener);
- mAudioFragment.setOnMediaViewInitializedListener(mOnMediaViewInitializedListener);
- }
-
- @Override
- public void setModels(List<HomeCardInterface.Model> models) {
- mModelList = models;
- }
-
- protected List<HomeCardInterface.Model> getModels() {
- return mModelList;
- }
-
- protected AudioModel getCurrentModel() {
- return mCurrentModel;
- }
-
- private void updateCurrentModelInFragment() {
- if (mCurrentModel != null && mCurrentModel.getCardHeader() != null) {
- mAudioFragment.updateHeaderView(mCurrentModel.getCardHeader());
- if (mCurrentModel.getCardContent() != null) {
- mAudioFragment.updateContentView(mCurrentModel.getCardContent());
- }
- } else {
- mAudioFragment.hideCard();
- }
- }
-}
diff --git a/app/src/com/android/car/carlauncher/homescreen/audio/InCallModel.java b/app/src/com/android/car/carlauncher/homescreen/audio/InCallModel.java
index a36c040..9f49eca 100644
--- a/app/src/com/android/car/carlauncher/homescreen/audio/InCallModel.java
+++ b/app/src/com/android/car/carlauncher/homescreen/audio/InCallModel.java
@@ -26,7 +26,6 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
-import android.os.Bundle;
import android.telecom.Call;
import android.telecom.CallAudioState;
import android.telecom.PhoneAccountHandle;
@@ -169,24 +168,16 @@
public Intent getIntent() {
Intent intent = null;
if (isSelfManagedCall()) {
- Bundle extras = mCurrentCall.getDetails().getExtras();
- ComponentName componentName = extras == null ? null : extras.getParcelable(
- Intent.EXTRA_COMPONENT_NAME, ComponentName.class);
- if (componentName != null) {
- intent = new Intent();
- intent.setComponent(componentName);
- } else {
- String callingAppPackageName = getCallingAppPackageName();
- if (!TextUtils.isEmpty(callingAppPackageName)) {
- if (isCarAppCallingService(callingAppPackageName)) {
- intent = new Intent();
- intent.setComponent(
- new ComponentName(
- callingAppPackageName, CAR_APP_ACTIVITY_INTERFACE));
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- } else {
- intent = mPackageManager.getLaunchIntentForPackage(callingAppPackageName);
- }
+ String callingAppPackageName = getCallingAppPackageName();
+ if (!TextUtils.isEmpty(callingAppPackageName)) {
+ if (isCarAppCallingService(callingAppPackageName)) {
+ intent = new Intent();
+ intent.setComponent(
+ new ComponentName(
+ callingAppPackageName, CAR_APP_ACTIVITY_INTERFACE));
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ } else {
+ intent = mPackageManager.getLaunchIntentForPackage(callingAppPackageName);
}
}
} else {
@@ -477,10 +468,21 @@
}
private boolean isCarAppCallingService(String packageName) {
- Intent intent =
+ // Check that app is integrated with CAL and handles calls
+ Intent serviceIntent =
new Intent(CAR_APP_SERVICE_INTERFACE)
.setPackage(packageName)
.addCategory(CAR_APP_CATEGORY_CALLING);
- return !mPackageManager.queryIntentServices(intent, GET_RESOLVED_FILTER).isEmpty();
+
+ if (mPackageManager.queryIntentServices(serviceIntent, GET_RESOLVED_FILTER).isEmpty()) {
+ return false;
+ }
+
+ // Check that app has CAl activity
+ Intent activityIntent = new Intent();
+ activityIntent.setComponent(new ComponentName(packageName, CAR_APP_ACTIVITY_INTERFACE));
+
+ return mPackageManager
+ .resolveActivity(activityIntent, PackageManager.MATCH_DEFAULT_ONLY) != null;
}
}
diff --git a/app/src/com/android/car/carlauncher/LaunchRootCarTaskViewCallbacks.java b/app/src/com/android/car/carlauncher/homescreen/audio/IntentHandler.java
similarity index 66%
copy from app/src/com/android/car/carlauncher/LaunchRootCarTaskViewCallbacks.java
copy to app/src/com/android/car/carlauncher/homescreen/audio/IntentHandler.java
index e30ffe5..753dec3 100644
--- a/app/src/com/android/car/carlauncher/LaunchRootCarTaskViewCallbacks.java
+++ b/app/src/com/android/car/carlauncher/homescreen/audio/IntentHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -14,10 +14,16 @@
* limitations under the License.
*/
-package com.android.car.carlauncher;
+package com.android.car.carlauncher.homescreen.audio;
+
+import android.content.Intent;
/**
- * A callbacks interface for {@link LaunchRootCarTaskView}.
+ * Handles an {@link Intent}
*/
-public interface LaunchRootCarTaskViewCallbacks extends
- CarTaskViewCallbacks {}
+public interface IntentHandler {
+ /**
+ * Handle {@link Intent}.
+ */
+ void handleIntent(Intent intent);
+}
diff --git a/app/src/com/android/car/carlauncher/homescreen/audio/MediaViewModel.java b/app/src/com/android/car/carlauncher/homescreen/audio/MediaViewModel.java
index 5becb9f..e320c1c 100644
--- a/app/src/com/android/car/carlauncher/homescreen/audio/MediaViewModel.java
+++ b/app/src/com/android/car/carlauncher/homescreen/audio/MediaViewModel.java
@@ -16,11 +16,9 @@
package com.android.car.carlauncher.homescreen.audio;
-import static android.car.media.CarMediaIntents.EXTRA_MEDIA_COMPONENT;
import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK;
import android.app.Application;
-import android.car.media.CarMediaIntents;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
@@ -33,6 +31,8 @@
import androidx.lifecycle.Observer;
import com.android.car.apps.common.imaging.ImageBinder;
+import com.android.car.carlauncher.Flags;
+import com.android.car.carlauncher.MediaSessionUtils;
import com.android.car.carlauncher.homescreen.HomeCardInterface;
import com.android.car.carlauncher.homescreen.ui.CardContent;
import com.android.car.carlauncher.homescreen.ui.CardHeader;
@@ -42,12 +42,12 @@
import com.android.car.media.common.R;
import com.android.car.media.common.playback.PlaybackProgress;
import com.android.car.media.common.playback.PlaybackViewModel;
+import com.android.car.media.common.source.MediaModels;
import com.android.car.media.common.source.MediaSource;
import com.android.car.media.common.source.MediaSourceColors;
import com.android.car.media.common.source.MediaSourceViewModel;
import com.android.internal.annotations.VisibleForTesting;
-
/**
* ViewModel for media. Uses both a {@link MediaSourceViewModel} and a {@link PlaybackViewModel}
* for data on the audio source and audio metadata (such as song title), respectively.
@@ -136,13 +136,24 @@
@Override
public void onCreate(@NonNull Context context) {
- if (mSourceViewModel == null) {
- mSourceViewModel = MediaSourceViewModel.get(getApplication(),
- MEDIA_SOURCE_MODE_PLAYBACK);
- }
- if (mPlaybackViewModel == null) {
- mPlaybackViewModel = PlaybackViewModel.get(getApplication(),
- MEDIA_SOURCE_MODE_PLAYBACK);
+ // Initialize media data with media session sources or mbt sources
+ if (Flags.mediaSessionCard()) {
+ MediaModels mediaModels = MediaSessionUtils.getMediaModels(context);
+ if (mSourceViewModel == null) {
+ mSourceViewModel = mediaModels.getMediaSourceViewModel();
+ }
+ if (mPlaybackViewModel == null) {
+ mPlaybackViewModel = mediaModels.getPlaybackViewModel();
+ }
+ } else {
+ if (mSourceViewModel == null) {
+ mSourceViewModel = MediaSourceViewModel.get(getApplication(),
+ MEDIA_SOURCE_MODE_PLAYBACK);
+ }
+ if (mPlaybackViewModel == null) {
+ mPlaybackViewModel = PlaybackViewModel.get(getApplication(),
+ MEDIA_SOURCE_MODE_PLAYBACK);
+ }
}
mContext = context;
@@ -177,20 +188,21 @@
@Override
protected void onCleared() {
super.onCleared();
- mSourceViewModel.getPrimaryMediaSource().removeObserver(mMediaSourceObserver);
- mPlaybackViewModel.getMetadata().removeObserver(mMetadataObserver);
- mPlaybackViewModel.getPlaybackStateWrapper().removeObserver(mPlaybackStateWrapperObserver);
+ if (mSourceViewModel != null) {
+ mSourceViewModel.getPrimaryMediaSource().removeObserver(mMediaSourceObserver);
+ }
+ if (mPlaybackViewModel != null) {
+ mPlaybackViewModel.getMetadata().removeObserver(mMetadataObserver);
+ mPlaybackViewModel.getPlaybackStateWrapper().removeObserver(
+ mPlaybackStateWrapperObserver);
+ }
}
@Override
public Intent getIntent() {
MediaSource mediaSource = getMediaSourceViewModel().getPrimaryMediaSource().getValue();
- Intent intent = new Intent(CarMediaIntents.ACTION_MEDIA_TEMPLATE);
- if (mediaSource != null) {
- intent.putExtra(EXTRA_MEDIA_COMPONENT,
- mediaSource.getBrowseServiceComponentName().flattenToString());
- }
- return intent;
+
+ return mediaSource != null ? mediaSource.getIntent() : null;
}
@Override
diff --git a/app/src/com/android/car/carlauncher/homescreen/audio/dialer/DialerCardFragment.java b/app/src/com/android/car/carlauncher/homescreen/audio/dialer/DialerCardFragment.java
new file mode 100644
index 0000000..e0c4108
--- /dev/null
+++ b/app/src/com/android/car/carlauncher/homescreen/audio/dialer/DialerCardFragment.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2023 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.carlauncher.homescreen.audio.dialer;
+
+import static android.graphics.Shader.TileMode.MIRROR;
+
+import android.graphics.Bitmap;
+import android.graphics.RenderEffect;
+import android.os.Bundle;
+import android.util.Size;
+import android.view.View;
+import android.widget.Chronometer;
+
+import com.android.car.apps.common.BitmapUtils;
+import com.android.car.carlauncher.R;
+import com.android.car.carlauncher.homescreen.HomeCardFragment;
+import com.android.car.carlauncher.homescreen.ui.CardContent;
+import com.android.car.carlauncher.homescreen.ui.DescriptiveTextWithControlsView;
+
+/** A fragment for in-call controls. Displays and controls an ongoing phone call */
+public class DialerCardFragment extends HomeCardFragment {
+ private Chronometer mChronometer;
+ private View mChronometerSeparator;
+ private float mBlurRadius;
+ private CardContent.CardBackgroundImage mDefaultCardBackgroundImage;
+ private CardContent.CardBackgroundImage mCardBackgroundImage;
+ private Size mDialerSize;
+ private View.OnLayoutChangeListener mOnRootLayoutChangeListener =
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+ boolean isWidthChanged = left - right != oldLeft - oldRight;
+ boolean isHeightChanged = top - bottom != oldTop - oldBottom;
+ boolean isSizeChanged = isWidthChanged || isHeightChanged;
+ if (isSizeChanged) {
+ mDialerSize = new Size(right - left, bottom - top);
+ resizeCardBackgroundImage(mDialerSize);
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mBlurRadius = getResources().getFloat(R.dimen.card_background_image_blur_radius);
+ mDefaultCardBackgroundImage = new CardContent.CardBackgroundImage(
+ getContext().getDrawable(R.drawable.default_audio_background),
+ getContext().getDrawable(R.drawable.control_bar_image_background));
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ getRootView().addOnLayoutChangeListener(mOnRootLayoutChangeListener);
+ getDescriptiveTextWithControlsLayoutView();
+ getRootView().setVisibility(View.VISIBLE);
+ getRootView().findViewById(R.id.optional_seek_bar_with_times_container).setVisibility(
+ View.GONE);
+
+ }
+
+ @Override
+ public void updateContentViewInternal(CardContent content) {
+ if (content.getType() == CardContent.HomeCardContentType.DESCRIPTIVE_TEXT_WITH_CONTROLS) {
+ DescriptiveTextWithControlsView audioContent =
+ (DescriptiveTextWithControlsView) content;
+ updateBackgroundImage(audioContent.getImage());
+ updateDescriptiveTextWithControlsView(audioContent.getTitle(),
+ audioContent.getSubtitle(),
+ /* optionalImage= */ null, audioContent.getLeftControl(),
+ audioContent.getCenterControl(), audioContent.getRightControl());
+ updateAudioDuration(audioContent);
+ } else {
+ super.updateContentViewInternal(content);
+ }
+ }
+
+ @Override
+ protected void hideAllViews() {
+ super.hideAllViews();
+ getCardBackground().setVisibility(View.GONE);
+ }
+
+ private Chronometer getChronometer() {
+ if (mChronometer == null) {
+ mChronometer = getDescriptiveTextWithControlsLayoutView().findViewById(
+ R.id.optional_timer);
+ mChronometerSeparator = getDescriptiveTextWithControlsLayoutView().findViewById(
+ R.id.optional_timer_separator);
+ }
+ return mChronometer;
+ }
+
+ private void resizeCardBackgroundImage(Size cardSize) {
+ if (mCardBackgroundImage == null || mCardBackgroundImage.getForeground() == null) {
+ mCardBackgroundImage = mDefaultCardBackgroundImage;
+ }
+ int maxDimen = Math.max(getCardBackgroundImage().getWidth(),
+ getCardBackgroundImage().getHeight());
+ // Prioritize size of background image view. Otherwise, use size of whole card
+ if (maxDimen == 0) {
+ // This function may be called before a non-null cardSize is ready. Instead of waiting
+ // for the next CardContent update to trigger resizeCardBackgroundImage(), resize the
+ // card as soon as mOnRootChangeLayoutListener sets the mDialerSize.
+ if (cardSize == null) {
+ return;
+ }
+ maxDimen = Math.max(cardSize.getWidth(), cardSize.getHeight());
+ }
+
+ if (maxDimen == 0) {
+ return;
+ }
+ Size scaledSize = new Size(maxDimen, maxDimen);
+ Bitmap imageBitmap = BitmapUtils.fromDrawable(mCardBackgroundImage.getForeground(),
+ scaledSize);
+ RenderEffect blur = RenderEffect.createBlurEffect(mBlurRadius, mBlurRadius, MIRROR);
+ getCardBackgroundImage().setRenderEffect(blur);
+
+ if (mCardBackgroundImage.getBackground() != null) {
+ getCardBackgroundImage().setBackground(mCardBackgroundImage.getBackground());
+ getCardBackgroundImage().setClipToOutline(true);
+ }
+ getCardBackgroundImage().setImageBitmap(imageBitmap, /* showAnimation= */ true);
+ getCardBackground().setVisibility(View.VISIBLE);
+ }
+
+ private void updateBackgroundImage(CardContent.CardBackgroundImage cardBackgroundImage) {
+ mCardBackgroundImage = cardBackgroundImage;
+ resizeCardBackgroundImage(mDialerSize);
+ }
+
+ private void updateAudioDuration(DescriptiveTextWithControlsView content) {
+ if (content.getStartTime() > 0) {
+ getChronometer().setVisibility(View.VISIBLE);
+ getChronometer().setBase(content.getStartTime());
+ getChronometer().start();
+ mChronometerSeparator.setVisibility(View.VISIBLE);
+ } else {
+ getChronometer().setVisibility(View.GONE);
+ mChronometerSeparator.setVisibility(View.GONE);
+ }
+ }
+}
+
diff --git a/app/src/com/android/car/carlauncher/homescreen/audio/dialer/DialerCardModel.java b/app/src/com/android/car/carlauncher/homescreen/audio/dialer/DialerCardModel.java
new file mode 100644
index 0000000..8f1372e
--- /dev/null
+++ b/app/src/com/android/car/carlauncher/homescreen/audio/dialer/DialerCardModel.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2023 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.carlauncher.homescreen.audio.dialer;
+
+import android.telecom.Call;
+
+import com.android.car.carlauncher.homescreen.audio.InCallModel;
+import com.android.car.telephony.common.CallDetail;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.time.Clock;
+import java.util.List;
+
+/** A wrapper around InCallModel to track when an active call is in progress. */
+public class DialerCardModel extends InCallModel {
+
+ private boolean mHasActiveCall;
+ private List<Integer> mAvailableRoutes;
+ private int mActiveRoute;
+
+ public DialerCardModel(Clock elapsedTimeClock) {
+ super(elapsedTimeClock);
+ }
+
+ /** Indicates whether there is an active call or not. */
+ public boolean hasActiveCall() {
+ return mHasActiveCall;
+ }
+
+ @Override
+ public void onCallAdded(Call call) {
+ mHasActiveCall = call != null;
+ super.onCallAdded(call);
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ mHasActiveCall = false;
+ super.onCallRemoved(call);
+ }
+
+ @Override
+ protected void handleActiveCall(@NotNull Call call) {
+ CallDetail callDetails = CallDetail.fromTelecomCallDetail(call.getDetails());
+ mAvailableRoutes = sInCallServiceManager.getSupportedAudioRoute(callDetails);
+ mActiveRoute = sInCallServiceManager.getAudioRoute(
+ CallDetail.fromTelecomCallDetail(call.getDetails()).getScoState());
+ super.handleActiveCall(call);
+ }
+
+ /**
+ * Returns audio routes supported by current call.
+ */
+ public List<Integer> getAvailableAudioRoutes() {
+ return mAvailableRoutes;
+ }
+
+ /**
+ * Returns current call audio state.
+ */
+ public int getActiveAudioRoute() {
+ return mActiveRoute;
+ }
+
+ /**
+ * Sets current call audio route.
+ */
+ public void setActiveAudioRoute(int audioRoute) {
+ if (getCurrentCall() == null) {
+ // AudioRouteButton is disabled if it is null. Simply ignore it.
+ return;
+ }
+ sInCallServiceManager.setAudioRoute(audioRoute, getCurrentCall());
+ mActiveRoute = audioRoute;
+ }
+}
diff --git a/app/src/com/android/car/carlauncher/homescreen/audio/dialer/DialerCardPresenter.java b/app/src/com/android/car/carlauncher/homescreen/audio/dialer/DialerCardPresenter.java
new file mode 100644
index 0000000..aaaf5cd
--- /dev/null
+++ b/app/src/com/android/car/carlauncher/homescreen/audio/dialer/DialerCardPresenter.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2023 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.carlauncher.homescreen.audio.dialer;
+
+import android.app.ActivityOptions;
+import android.content.Context;
+import android.content.Intent;
+import android.view.Display;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.car.carlauncher.homescreen.CardPresenter;
+import com.android.car.carlauncher.homescreen.HomeCardFragment.OnViewClickListener;
+import com.android.car.carlauncher.homescreen.HomeCardFragment.OnViewLifecycleChangeListener;
+import com.android.car.carlauncher.homescreen.HomeCardInterface;
+import com.android.car.carlauncher.homescreen.audio.InCallModel;
+
+import java.util.List;
+
+/**
+ * A presenter for the in-call controls.
+ */
+public class DialerCardPresenter extends CardPresenter {
+
+ /** A listener to notify when an in-call state changes. */
+ public interface OnInCallStateChangeListener {
+
+ /** Notifies when an in-call state changes. */
+ void onInCallStateChanged(boolean hasActiveCall);
+ }
+
+ private InCallModel mViewModel;
+ private DialerCardFragment mFragment;
+
+ @VisibleForTesting
+ boolean mHasActiveCall;
+
+ public void setOnInCallStateChangeListener(
+ OnInCallStateChangeListener onInCallStateChangeListener) {
+ mOnInCallStateChangeListener = onInCallStateChangeListener;
+ }
+
+ OnInCallStateChangeListener mOnInCallStateChangeListener;
+
+ private final OnViewClickListener mOnViewClickListener =
+ new OnViewClickListener() {
+ @Override
+ public void onViewClicked() {
+ ActivityOptions options = ActivityOptions.makeBasic();
+ options.setLaunchDisplayId(Display.DEFAULT_DISPLAY);
+ Intent intent = mViewModel.getIntent();
+ Context context = mFragment.getContext();
+ if (context != null) {
+ context.startActivity(intent, options.toBundle());
+ }
+ }
+ };
+ @VisibleForTesting
+ final HomeCardInterface.Model.OnModelUpdateListener mOnInCallModelUpdateListener =
+ new HomeCardInterface.Model.OnModelUpdateListener() {
+ @Override
+ public void onModelUpdate(HomeCardInterface.Model model) {
+ DialerCardModel dialerCardModel = (DialerCardModel) model;
+ if (dialerCardModel.getCardHeader() != null) {
+ mFragment.updateHeaderView(dialerCardModel.getCardHeader());
+ }
+ if (dialerCardModel.getCardContent() != null) {
+ mFragment.updateContentView(dialerCardModel.getCardContent());
+ }
+ boolean hasActiveCall = dialerCardModel.hasActiveCall();
+ if (mHasActiveCall != hasActiveCall) {
+ mHasActiveCall = hasActiveCall;
+ mOnInCallStateChangeListener.onInCallStateChanged(hasActiveCall);
+ }
+ }
+ };
+
+ private final OnViewLifecycleChangeListener mOnInCallViewLifecycleChangeListener =
+ new OnViewLifecycleChangeListener() {
+ @Override
+ public void onViewCreated() {
+ mViewModel.setOnModelUpdateListener(mOnInCallModelUpdateListener);
+ mViewModel.onCreate(mFragment.requireContext());
+ }
+
+ @Override
+ public void onViewDestroyed() {
+ mViewModel.onDestroy(getFragment().requireContext());
+ }
+ };
+
+ // Deprecated. Use setModel instead.
+ @Override
+ public void setModels(List<HomeCardInterface.Model> models) {
+ // No-op
+ }
+
+ public void setModel(InCallModel viewModel) {
+ mViewModel = viewModel;
+ }
+
+ @Override
+ public void setView(HomeCardInterface.View view) {
+ super.setView(view);
+ mFragment = (DialerCardFragment) view;
+ mFragment.setOnViewLifecycleChangeListener(mOnInCallViewLifecycleChangeListener);
+ mFragment.setOnViewClickListener(mOnViewClickListener);
+ }
+}
diff --git a/app/src/com/android/car/carlauncher/homescreen/audio/media/MediaCardController.java b/app/src/com/android/car/carlauncher/homescreen/audio/media/MediaCardController.java
new file mode 100644
index 0000000..655428a
--- /dev/null
+++ b/app/src/com/android/car/carlauncher/homescreen/audio/media/MediaCardController.java
@@ -0,0 +1,627 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.homescreen.audio.media;
+
+import static com.android.car.media.common.ui.PlaybackCardControllerUtilities.getFirstCustomActionInSet;
+import static com.android.car.media.common.ui.PlaybackCardControllerUtilities.skipBackStandardActions;
+import static com.android.car.media.common.ui.PlaybackCardControllerUtilities.skipForwardStandardActions;
+import static com.android.car.media.common.ui.PlaybackCardControllerUtilities.updatePlayButtonWithPlaybackState;
+
+import static java.lang.Integer.max;
+
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+
+import androidx.constraintlayout.motion.widget.MotionLayout;
+import androidx.viewpager2.widget.ViewPager2;
+
+import com.android.car.apps.common.RoundedDrawable;
+import com.android.car.apps.common.util.ViewUtils;
+import com.android.car.carlauncher.R;
+import com.android.car.media.common.CustomPlaybackAction;
+import com.android.car.media.common.MediaItemMetadata;
+import com.android.car.media.common.playback.PlaybackProgress;
+import com.android.car.media.common.playback.PlaybackViewModel;
+import com.android.car.media.common.playback.PlaybackViewModel.PlaybackController;
+import com.android.car.media.common.playback.PlaybackViewModel.PlaybackStateWrapper;
+import com.android.car.media.common.source.MediaSource;
+import com.android.car.media.common.ui.PlaybackCardController;
+import com.android.car.media.common.ui.PlaybackHistoryController;
+import com.android.car.media.common.ui.PlaybackQueueController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MediaCardController extends PlaybackCardController implements
+ MediaCardPanelViewPagerAdapter.ViewPagerQueueCreator,
+ MediaCardPanelViewPagerAdapter.ViewPagerHistoryCreator {
+
+ private static final int SWIPE_MAX_OFF_PATH = 75;
+ private static final int SWIPE_THRESHOLD_VELOCITY = 200;
+
+ private final MediaIntentRouter mMediaIntentRouter = MediaIntentRouter.getInstance();
+ private Resources mViewResources;
+ private View mPanelHandlebar;
+ private LinearLayout mPanel;
+ private MotionLayout mMotionLayout;
+ private MediaCardFragment.MediaCardViewModel mCardViewModel;
+ private ImageButton mSkipPrevButton;
+ private ImageButton mSkipNextButton;
+ private int mSkipPrevVisibility;
+ private int mSkipNextVisibility;
+ private int mAlbumCoverVisibility;
+ private int mSubtitleVisibility;
+ private int mLogoVisibility;
+
+ private PlaybackQueueController mPlaybackQueueController;
+ private PlaybackHistoryController mPlaybackHistoryController;
+
+ private ViewPager2 mPager;
+ private MediaCardPanelViewPagerAdapter mPagerAdapter;
+ private Handler mHandler;
+
+ @Override
+ public void createQueueController(ViewGroup queueContainer) {
+ mPlaybackQueueController = new PlaybackQueueController(
+ queueContainer, /* queueResource */ Resources.ID_NULL,
+ R.layout.media_card_queue_item, R.layout.media_card_queue_header_item,
+ getViewLifecycleOwner(), mDataModel, mCardViewModel.getMediaItemsRepository(),
+ /* uxrContentLimiter */ null, /* uxrConfigurationId */ 0);
+ mPlaybackQueueController.setShowTimeForActiveQueueItem(false);
+ mPlaybackQueueController.setShowIconForActiveQueueItem(false);
+ mPlaybackQueueController.setShowThumbnailForQueueItem(true);
+ mPlaybackQueueController.setShowSubtitleForQueueItem(true);
+ }
+
+ @Override
+ public void createHistoryController(ViewGroup historyContainer) {
+ mPlaybackHistoryController = new PlaybackHistoryController(getViewLifecycleOwner(),
+ mCardViewModel, historyContainer, R.layout.media_card_history_item,
+ R.layout.media_card_history_header_item, /* uxrConfigurationId */ 0);
+ mPlaybackHistoryController.setupView();
+ }
+
+ /** Builder for {@link MediaCardController}. Overrides build() method to return
+ * NowPlayingController rather than base {@link PlaybackCardController}
+ */
+ public static class Builder extends PlaybackCardController.Builder {
+
+ @Override
+ public MediaCardController build() {
+ MediaCardController controller = new MediaCardController(this);
+ controller.setupController();
+ return controller;
+ }
+ }
+
+ public MediaCardController(Builder builder) {
+ super(builder);
+
+ mCardViewModel = (MediaCardFragment.MediaCardViewModel) mViewModel;
+ mViewResources = mView.getContext().getResources();
+
+ mView.setOnClickListener(view -> {
+ launchMediaAppOrClosePanel();
+ });
+ mView.findViewById(R.id.empty_panel).setOnClickListener(view -> {
+ launchMediaAppOrClosePanel();
+ });
+
+ mPager = mView.findViewById(R.id.view_pager);
+ mPagerAdapter = new MediaCardPanelViewPagerAdapter(mView.getContext());
+ mPager.setAdapter(mPagerAdapter);
+ mPagerAdapter.setQueueControllerProvider(this);
+ mPagerAdapter.setHistoryControllerProvider(this);
+ mPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
+
+ @Override
+ public void onPageSelected(int position) {
+ super.onPageSelected(position);
+ if (!mCardViewModel.getPanelExpanded()) {
+ return;
+ }
+ selectOverflow(position == getOverflowTabIndex());
+ selectQueue(position == getQueueTabIndex());
+ selectHistory(position == getHistoryTabIndex());
+ }
+ });
+
+ mMotionLayout = mView.findViewById(R.id.motion_layout);
+
+ mPanel = mView.findViewById(R.id.button_panel_background);
+ mPanelHandlebar = mView.findViewById(R.id.media_card_panel_handlebar);
+
+ mSkipPrevButton = mView.findViewById(R.id.playback_action_id1);
+ mSkipNextButton = mView.findViewById(R.id.playback_action_id2);
+
+ mMotionLayout.addTransitionListener(new MotionLayout.TransitionListener() {
+ @Override
+ public void onTransitionStarted(MotionLayout motionLayout, int i, int i1) {
+ }
+
+ @Override
+ public void onTransitionChange(MotionLayout motionLayout, int i, int i1, float v) {
+ }
+
+ @Override
+ public void onTransitionCompleted(MotionLayout motionLayout, int i) {
+ if (mCardViewModel.getPanelExpanded()) {
+ mSkipPrevButton.setVisibility(View.GONE);
+ mSkipNextButton.setVisibility(View.GONE);
+ mLogo.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void onTransitionTrigger(MotionLayout motionLayout, int i, boolean b, float v) {
+ }
+ });
+
+ GestureDetector mCloseGestureDetector = new GestureDetector(mView.getContext(),
+ new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onFling(MotionEvent event1, MotionEvent event2,
+ float velocityX, float velocityY) {
+ if (Math.abs(event1.getX() - event2.getX()) > SWIPE_MAX_OFF_PATH
+ || Math.abs(velocityY) < SWIPE_THRESHOLD_VELOCITY) {
+ // swipe was not vertical or was not fast enough
+ return false;
+ }
+ boolean isInClosingDirection = velocityY > 0;
+ if (isInClosingDirection) {
+ animateClosePanel();
+ return true;
+ }
+ return false;
+ }
+ }
+ );
+ mPanelHandlebar.setOnClickListener((view) -> animateClosePanel());
+ mPanelHandlebar.setOnTouchListener((view, event) ->
+ mCloseGestureDetector.onTouchEvent(event));
+
+ mHandler = new Handler(Looper.getMainLooper());
+ }
+
+ @Override
+ protected void setupController() {
+ super.setupController();
+
+ mSkipPrevVisibility = mSkipPrevButton.getVisibility();
+ mSkipNextVisibility = mSkipNextButton.getVisibility();
+ mAlbumCoverVisibility = mAlbumCover.getVisibility();
+ mSubtitleVisibility = mSubtitle.getVisibility();
+ mLogoVisibility = mLogo.getVisibility();
+ }
+
+ @Override
+ protected void updateMetadata(MediaItemMetadata metadata) {
+ super.updateMetadata(metadata);
+ if (mCardViewModel.getPanelExpanded()) {
+ mSubtitleVisibility = mSubtitle.getVisibility();
+ mSubtitle.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ protected void updateAlbumCoverWithDrawable(Drawable drawable) {
+ RoundedDrawable roundedDrawable = new RoundedDrawable(drawable, mView.getResources()
+ .getFloat(R.dimen.media_card_album_art_drawable_corner_ratio));
+ super.updateAlbumCoverWithDrawable(roundedDrawable);
+
+ if (mCardViewModel.getPanelExpanded()) {
+ mAlbumCoverVisibility = mAlbumCover.getVisibility();
+ mAlbumCover.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ @Override
+ protected void updateLogoWithDrawable(Drawable drawable) {
+ super.updateLogoWithDrawable(drawable);
+ if (mCardViewModel.getPanelExpanded()) {
+ mLogoVisibility = mLogo.getVisibility();
+ mLogo.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ protected void updateMediaSource(MediaSource mediaSource) {
+ super.updateMediaSource(mediaSource);
+ if (mCardViewModel.getPanelExpanded()) {
+ mAppIcon.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ @Override
+ protected void updateProgress(PlaybackProgress progress) {
+ super.updateProgress(progress);
+ ViewUtils.setVisible(mSeekBar, progress != null && progress.hasTime());
+ if (progress == null || !progress.hasTime()) {
+ mLogo.setVisibility(View.GONE);
+ } else if (mDataModel.getMetadata().getValue() != null) {
+ Uri logoUri = mLogo.prepareToDisplay(mDataModel.getMetadata().getValue());
+ if (logoUri != null && !mCardViewModel.getPanelExpanded()) {
+ mLogo.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ @Override
+ protected void updatePlaybackState(PlaybackViewModel.PlaybackStateWrapper playbackState) {
+ PlaybackController playbackController = mDataModel.getPlaybackController().getValue();
+ if (playbackState != null) {
+ updatePlayButtonWithPlaybackState(mPlayPauseButton, playbackState, playbackController);
+ List<PlaybackViewModel.RawCustomPlaybackAction> usedCustomActions =
+ updateSkipButtonsAndReturnUsedStandardCustomActions(
+ playbackState, playbackController);
+
+ boolean hasCustomActions = playbackState.getCustomActions().size() != 0;
+ boolean isPreviouslyVisible = ViewUtils.isVisible(mActionOverflowButton);
+ ViewUtils.setVisible(mActionOverflowButton, hasCustomActions);
+ mPagerAdapter.setHasOverflow(hasCustomActions);
+ if (mCardViewModel.getPanelExpanded() && isPreviouslyVisible != hasCustomActions) {
+ animateClosePanel();
+ }
+ mPagerAdapter.notifyPlaybackStateChanged(playbackState,
+ playbackController, usedCustomActions);
+ } else {
+ mSkipPrevButton.setVisibility(View.GONE);
+ mSkipNextButton.setVisibility(View.GONE);
+ }
+
+ if (mCardViewModel.getPanelExpanded()) {
+ mSkipPrevVisibility = mSkipPrevButton.getVisibility();
+ mSkipNextVisibility = mSkipNextButton.getVisibility();
+ mSkipPrevButton.setVisibility(View.GONE);
+ mSkipNextButton.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ protected void setUpActionsOverflowButton() {
+ super.setUpActionsOverflowButton();
+ setOverflowState(mCardViewModel.getOverflowExpanded(), false);
+ }
+
+ @Override
+ protected void handleCustomActionsOverflowButtonClicked(View overflow) {
+ super.handleCustomActionsOverflowButtonClicked(overflow);
+ setOverflowState(mCardViewModel.getOverflowExpanded(), true);
+ }
+
+ @Override
+ protected void setUpQueueButton() {
+ super.setUpQueueButton();
+ setQueueState(mCardViewModel.getQueueVisible(), false);
+ }
+
+ @Override
+ protected void updateQueueState(boolean hasQueue, boolean isQueueVisible) {
+ super.updateQueueState(hasQueue, isQueueVisible);
+ mPagerAdapter.setHasQueue(hasQueue);
+ ViewUtils.setVisible(mQueueButton, hasQueue);
+ if (mCardViewModel.getPanelExpanded() && !hasQueue) {
+ animateClosePanel();
+ }
+ }
+
+ @Override
+ protected void handleQueueButtonClicked(View queue) {
+ super.handleQueueButtonClicked(queue);
+ setQueueState(mCardViewModel.getQueueVisible(), true);
+ }
+
+ @Override
+ protected void setUpHistoryButton() {
+ super.setUpHistoryButton();
+ setHistoryState(mCardViewModel.getHistoryVisible(), false);
+ }
+
+ @Override
+ protected void handleHistoryButtonClicked(View history) {
+ super.handleHistoryButtonClicked(history);
+ setHistoryState(mCardViewModel.getHistoryVisible(), true);
+ }
+
+ private void setOverflowState(boolean isExpanded, boolean stateSetThroughClick) {
+ if (mActionOverflowButton == null) {
+ return;
+ }
+ if (!mCardViewModel.getPanelExpanded()) {
+ if (stateSetThroughClick) {
+ saveViewVisibilityBeforeAnimation();
+ mCardViewModel.setPanelExpanded(true);
+
+ mPager.setCurrentItem(getOverflowTabIndex());
+
+ mHandler.post(() -> mMotionLayout.transitionToEnd());
+
+ selectOverflow(true);
+ } else {
+ unselectPanel();
+ }
+ } else {
+ // If the panel is already open and overflow is clicked again,
+ // always switch to overflow tab
+ mPager.setCurrentItem(getOverflowTabIndex(), true);
+ mPanel.setEnabled(true);
+
+ selectOverflow(true);
+
+ selectQueue(false);
+ selectHistory(false);
+ }
+ }
+
+ private void setQueueState(boolean isVisible, boolean stateSetThroughClick) {
+ if (mQueueButton == null) {
+ return;
+ }
+ if (!mCardViewModel.getPanelExpanded()) {
+ if (stateSetThroughClick) {
+ saveViewVisibilityBeforeAnimation();
+ mCardViewModel.setPanelExpanded(true);
+ mPager.setCurrentItem(getQueueTabIndex());
+
+ mHandler.post(() -> mMotionLayout.transitionToEnd());
+
+ selectQueue(true);
+ } else {
+ unselectPanel();
+ }
+ } else {
+ // If the panel is already open and queue is clicked again,
+ // always switch to queue tab
+ mPager.setCurrentItem(getQueueTabIndex(), true);
+
+ mPanel.setEnabled(true);
+
+ selectQueue(true);
+
+ selectOverflow(false);
+ selectHistory(false);
+ }
+ }
+
+ private void setHistoryState(boolean isVisible, boolean stateSetThroughClick) {
+ if (mHistoryButton == null) {
+ return;
+ }
+ int historyPos = getHistoryTabIndex();
+ if (!mCardViewModel.getPanelExpanded()) {
+ if (stateSetThroughClick) {
+ saveViewVisibilityBeforeAnimation();
+ mCardViewModel.setPanelExpanded(true);
+ mPager.setCurrentItem(historyPos);
+
+ mHandler.post(() -> mMotionLayout.transitionToEnd());
+
+ selectHistory(true);
+ } else {
+ unselectPanel();
+ }
+ } else {
+ // If the panel is already open and history is clicked again,
+ // always switch to history tab
+ mPager.setCurrentItem(historyPos, true);
+
+ mPanel.setEnabled(true);
+
+ selectHistory(true);
+
+ selectOverflow(false);
+ selectQueue(false);
+ }
+ }
+
+ private void launchMediaAppOrClosePanel() {
+ if (mCardViewModel.getPanelExpanded()) {
+ animateClosePanel();
+ } else {
+ MediaSource mediaSource = mDataModel.getMediaSource().getValue();
+ Intent intent = mediaSource != null ? mediaSource.getIntent() : null;
+ mMediaIntentRouter.handleMediaIntent(intent);
+ }
+ }
+
+ private void animateClosePanel() {
+ mCardViewModel.setPanelExpanded(false);
+ mMotionLayout.transitionToStart();
+ restoreExtraViewsWhenPanelClosed();
+ unselectAllPanelButtons();
+ }
+
+ private void unselectPanel() {
+ mPanel.setEnabled(false);
+ unselectAllPanelButtons();
+ }
+
+ private void selectQueue(boolean shouldSelect) {
+ mCardViewModel.setQueueVisible(shouldSelect);
+ mQueueButton.setSelected(shouldSelect);
+ }
+
+ private void selectOverflow(boolean shouldSelect) {
+ mCardViewModel.setOverflowExpanded(shouldSelect);
+ mActionOverflowButton.setSelected(shouldSelect);
+ }
+
+ private void selectHistory(boolean shouldSelect) {
+ mCardViewModel.setHistoryVisible(shouldSelect);
+ mHistoryButton.setSelected(shouldSelect);
+ }
+
+ private void unselectAllPanelButtons() {
+ selectOverflow(false);
+ selectQueue(false);
+ selectHistory(false);
+ }
+
+ private void saveViewVisibilityBeforeAnimation() {
+ mSubtitleVisibility = mSubtitle.getVisibility();
+ mLogoVisibility = mLogo.getVisibility();
+ mSkipPrevVisibility = mSkipPrevButton.getVisibility();
+ mSkipNextVisibility = mSkipNextButton.getVisibility();
+ mAlbumCoverVisibility = mAlbumCover.getVisibility();
+ }
+
+ private void restoreExtraViewsWhenPanelClosed() {
+ mAlbumCover.setVisibility(mAlbumCoverVisibility);
+ mAppIcon.setVisibility(View.VISIBLE);
+ mSkipPrevButton.setVisibility(mSkipPrevVisibility);
+ mSkipNextButton.setVisibility(mSkipNextVisibility);
+ mSubtitle.setVisibility(mSubtitleVisibility);
+ mLogo.setVisibility(mLogoVisibility);
+ }
+
+ /**
+ * Set the mSkipNextButton and mSkipPrevButton with a skip action Drawable if sent by the
+ * playbackState, otherwise with a skipForwardStandardAction and skipBackStandardAction
+ * respectively. If none exist, hide the button.
+ */
+ private List<PlaybackViewModel.RawCustomPlaybackAction>
+ updateSkipButtonsAndReturnUsedStandardCustomActions(PlaybackStateWrapper playbackState,
+ PlaybackController playbackController) {
+ List<PlaybackViewModel.RawCustomPlaybackAction> usedCustomActions =
+ new ArrayList<PlaybackViewModel.RawCustomPlaybackAction>();
+ updateSkipNextButtonWithSkipOrStandardAction(playbackState, playbackController,
+ usedCustomActions);
+ updateSkipPrevButtonWithSkipOrStandardAction(playbackState, playbackController,
+ usedCustomActions);
+ return usedCustomActions;
+ }
+
+ private void updateSkipNextButtonWithSkipOrStandardAction(
+ PlaybackStateWrapper playbackState, PlaybackController playbackController,
+ List<PlaybackViewModel.RawCustomPlaybackAction> usedCustomActions) {
+ boolean isSkipNextEnabled = playbackState.isSkipNextEnabled();
+ boolean isSkipNextReserved = playbackState.isSkipNextReserved();
+ if ((isSkipNextEnabled || isSkipNextReserved)) {
+ updateButton(mSkipNextButton, mView.getContext().getDrawable(
+ com.android.car.media.common.R.drawable.ic_skip_next),
+ mView.getContext().getDrawable(R.drawable.circle_button_background),
+ true, isSkipNextEnabled, (v) -> {
+ if (playbackController != null) {
+ playbackController.skipToNext();
+ }
+ });
+ } else {
+ PlaybackViewModel.RawCustomPlaybackAction skipForwardCustomAction =
+ getFirstCustomActionInSet(playbackState.getCustomActions(),
+ skipForwardStandardActions);
+ if (skipForwardCustomAction != null) {
+ boolean isCustomActionUsed =
+ updateButtonWithCustomAction(mSkipNextButton, skipForwardCustomAction,
+ playbackController);
+ if (isCustomActionUsed) {
+ usedCustomActions.add(skipForwardCustomAction);
+ }
+ } else {
+ updateButton(mSkipNextButton, null, null, false, false, null);
+ }
+ }
+ }
+
+ private void updateSkipPrevButtonWithSkipOrStandardAction(
+ PlaybackStateWrapper playbackState, PlaybackController playbackController,
+ List<PlaybackViewModel.RawCustomPlaybackAction> usedCustomActions) {
+ boolean isSkipPrevEnabled = playbackState.isSkipPreviousEnabled();
+ boolean isSkipPrevReserved = playbackState.iSkipPreviousReserved();
+ if ((isSkipPrevEnabled || isSkipPrevReserved)) {
+ updateButton(mSkipPrevButton, mView.getContext().getDrawable(
+ com.android.car.media.common.R.drawable.ic_skip_previous),
+ mView.getContext().getDrawable(R.drawable.circle_button_background),
+ true, isSkipPrevEnabled, (v) -> {
+ if (playbackController != null) {
+ playbackController.skipToPrevious();
+ }
+ });
+ } else {
+ PlaybackViewModel.RawCustomPlaybackAction skipBackCustomAction =
+ getFirstCustomActionInSet(playbackState.getCustomActions(),
+ skipBackStandardActions);
+ if (skipBackCustomAction != null) {
+ boolean isCustomActionUsed =
+ updateButtonWithCustomAction(mSkipPrevButton, skipBackCustomAction,
+ playbackController);
+ if (isCustomActionUsed) {
+ usedCustomActions.add(skipBackCustomAction);
+ }
+ } else {
+ updateButton(mSkipPrevButton, null, null, false, false, null);
+ }
+ }
+ }
+
+ private void updateButton(ImageButton button, Drawable imageDrawable,
+ Drawable backgroundDrawable, boolean isVisible, boolean isEnabled,
+ View.OnClickListener listener) {
+ button.setImageDrawable(imageDrawable);
+ button.setBackground(backgroundDrawable);
+ ViewUtils.setVisible(button, isVisible);
+ button.setEnabled(isEnabled);
+ button.setOnClickListener(listener);
+ }
+
+ private boolean updateButtonWithCustomAction(ImageButton button,
+ PlaybackViewModel.RawCustomPlaybackAction rawCustomAction,
+ PlaybackController playbackController) {
+ CustomPlaybackAction customAction = rawCustomAction
+ .fetchDrawable(mView.getContext());
+ if (customAction != null) {
+ updateButton(button, customAction.mIcon, mView.getContext().getDrawable(
+ R.drawable.circle_button_background), true, true, (v) -> {
+ if (playbackController != null) {
+ playbackController.doCustomAction(
+ customAction.mAction, customAction.mExtras);
+ }
+ });
+ return true;
+ } else {
+ updateButton(button, null, null, false, false, null);
+ return false;
+ }
+ }
+
+ private int getOverflowTabIndex() {
+ return hasOverflow() ? 0 : -1;
+ }
+
+ private int getQueueTabIndex() {
+ if (!getMediaHasQueue()) return -1;
+ return getOverflowTabIndex() + 1;
+ }
+
+ private int getHistoryTabIndex() {
+ return max(getOverflowTabIndex(), getQueueTabIndex()) + 1;
+ }
+
+ private boolean hasOverflow() {
+ PlaybackStateWrapper playbackState = mDataModel.getPlaybackStateWrapper().getValue();
+ return playbackState != null && playbackState.getCustomActions().size() != 0;
+ }
+}
diff --git a/app/src/com/android/car/carlauncher/homescreen/audio/AudioFragment.java b/app/src/com/android/car/carlauncher/homescreen/audio/media/MediaCardFragment.java
similarity index 75%
rename from app/src/com/android/car/carlauncher/homescreen/audio/AudioFragment.java
rename to app/src/com/android/car/carlauncher/homescreen/audio/media/MediaCardFragment.java
index af9db40..e5e3ebe 100644
--- a/app/src/com/android/car/carlauncher/homescreen/audio/AudioFragment.java
+++ b/app/src/com/android/car/carlauncher/homescreen/audio/media/MediaCardFragment.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 Google Inc.
+ * Copyright (C) 2023 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.
@@ -14,14 +14,19 @@
* limitations under the License.
*/
-package com.android.car.carlauncher.homescreen.audio;
+package com.android.car.carlauncher.homescreen.audio.media;
+import static android.graphics.Shader.TileMode.MIRROR;
+
+import android.app.Application;
import android.content.res.ColorStateList;
import android.graphics.Bitmap;
+import android.graphics.RenderEffect;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.util.Size;
+import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
@@ -30,23 +35,29 @@
import android.widget.SeekBar;
import android.widget.TextView;
+import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProvider;
import com.android.car.apps.common.BitmapUtils;
-import com.android.car.apps.common.ImageUtils;
+import com.android.car.carlauncher.Flags;
+import com.android.car.carlauncher.MediaSessionUtils;
import com.android.car.carlauncher.R;
import com.android.car.carlauncher.homescreen.HomeCardFragment;
+import com.android.car.carlauncher.homescreen.HomeCardInterface;
+import com.android.car.carlauncher.homescreen.audio.MediaViewModel;
import com.android.car.carlauncher.homescreen.ui.CardContent;
import com.android.car.carlauncher.homescreen.ui.DescriptiveTextWithControlsView;
import com.android.car.carlauncher.homescreen.ui.SeekBarViewModel;
import com.android.car.media.common.PlaybackControlsActionBar;
-
+import com.android.car.media.common.source.MediaModels;
+import com.android.car.media.common.ui.PlaybackCardViewModel;
/**
- * {@link HomeCardInterface.View} for the audio card. Displays and controls the current audio source
- * such as the currently playing (or last played) media item or an ongoing phone call.
+ * {@link HomeCardInterface.View} for the media audio card. Displays and controls the current
+ * audio source such as the currently playing (or last played) media item.
*/
-public class AudioFragment extends HomeCardFragment {
+public class MediaCardFragment extends HomeCardFragment {
/**
* Interface definition for a callback to be invoked when a media layout is inflated.
@@ -59,7 +70,7 @@
void onMediaViewInitialized();
}
- private static final String TAG = AudioFragment.class.getSimpleName();
+ private static final String TAG = MediaCardFragment.class.getSimpleName();
private Chronometer mChronometer;
private View mChronometerSeparator;
@@ -104,30 +115,62 @@
};
private CardContent.CardBackgroundImage mCardBackgroundImage;
+ private Size mMediaSize;
private View.OnLayoutChangeListener mOnRootLayoutChangeListener =
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
boolean isWidthChanged = left - right != oldLeft - oldRight;
boolean isHeightChanged = top - bottom != oldTop - oldBottom;
boolean isSizeChanged = isWidthChanged || isHeightChanged;
if (isSizeChanged) {
- resizeCardBackgroundImage();
+ mMediaSize = new Size(right - left, bottom - top);
+ resizeCardBackgroundImage(mMediaSize);
}
};
+ private MediaCardController mMediaCardController;
+ protected MediaCardViewModel mViewModel;
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- mBlurRadius = getResources().getFloat(R.dimen.card_background_image_blur_radius);
- mDefaultCardBackgroundImage = new CardContent.CardBackgroundImage(
- getContext().getDrawable(R.drawable.default_audio_background),
- getContext().getDrawable(R.drawable.control_bar_image_background));
- mShowSeekBar = getResources().getBoolean(R.bool.show_seek_bar);
+ if (!Flags.mediaCardFullscreen()) {
+ mBlurRadius = getResources().getFloat(R.dimen.card_background_image_blur_radius);
+ mDefaultCardBackgroundImage = new CardContent.CardBackgroundImage(
+ getContext().getDrawable(R.drawable.default_audio_background),
+ getContext().getDrawable(R.drawable.control_bar_image_background));
+ mShowSeekBar = getResources().getBoolean(R.bool.show_seek_bar);
+ } else {
+ mViewModel = new ViewModelProvider(requireActivity()).get(MediaCardViewModel.class);
+ if (mViewModel.needsInitialization()) {
+ MediaModels models = MediaSessionUtils.getMediaModels(getContext());
+ mViewModel.init(models);
+ }
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ if (!Flags.mediaCardFullscreen()) {
+ return super.onCreateView(inflater, container, savedInstanceState);
+ } else {
+ return inflater.inflate(R.layout.media_card_fullscreen, container, false);
+ }
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- getRootView().addOnLayoutChangeListener(mOnRootLayoutChangeListener);
+ if (!Flags.mediaCardFullscreen()) {
+ super.onViewCreated(view, savedInstanceState);
+ getRootView().addOnLayoutChangeListener(mOnRootLayoutChangeListener);
+ } else {
+ mMediaCardController = (MediaCardController) new MediaCardController.Builder()
+ .setModels(mViewModel.getPlaybackViewModel(),
+ mViewModel,
+ mViewModel.getMediaItemsRepository())
+ .setViewGroup((ViewGroup) view)
+ .build();
+ }
}
@Override
@@ -186,7 +229,7 @@
return (PlaybackControlsActionBar) mMediaControlBarView;
}
- private void resizeCardBackgroundImage() {
+ private void resizeCardBackgroundImage(Size cardSize) {
if (mCardBackgroundImage == null || mCardBackgroundImage.getForeground() == null) {
mCardBackgroundImage = mDefaultCardBackgroundImage;
}
@@ -194,8 +237,9 @@
getCardBackgroundImage().getHeight());
// Prioritize size of background image view. Otherwise, use size of whole card
if (maxDimen == 0) {
- Size cardSize = getCardSize();
-
+ // This function may be called before a non-null cardSize is ready. Instead of waiting
+ // for the next CardContent update to trigger resizeCardBackgroundImage(), resize the
+ // card as soon as mOnRootChangeLayoutListener sets the mMediaSize.
if (cardSize == null) {
return;
}
@@ -206,22 +250,23 @@
return;
}
Size scaledSize = new Size(maxDimen, maxDimen);
+
Bitmap imageBitmap = BitmapUtils.fromDrawable(mCardBackgroundImage.getForeground(),
scaledSize);
- Bitmap blurredBackground = ImageUtils.blur(getContext(), imageBitmap, scaledSize,
- mBlurRadius);
+ RenderEffect blur = RenderEffect.createBlurEffect(mBlurRadius, mBlurRadius, MIRROR);
+ getCardBackgroundImage().setRenderEffect(blur);
if (mCardBackgroundImage.getBackground() != null) {
getCardBackgroundImage().setBackground(mCardBackgroundImage.getBackground());
getCardBackgroundImage().setClipToOutline(true);
}
- getCardBackgroundImage().setImageBitmap(blurredBackground, /* showAnimation= */ true);
+ getCardBackgroundImage().setImageBitmap(imageBitmap, /* showAnimation= */ true);
getCardBackground().setVisibility(View.VISIBLE);
}
private void updateBackgroundImage(CardContent.CardBackgroundImage cardBackgroundImage) {
mCardBackgroundImage = cardBackgroundImage;
- resizeCardBackgroundImage();
+ resizeCardBackgroundImage(mMediaSize);
}
private void updateMediaView(CharSequence title, CharSequence subtitle) {
@@ -351,4 +396,21 @@
}
return mSeekBarWithTimesContainer;
}
+
+ public static class MediaCardViewModel extends PlaybackCardViewModel {
+
+ private boolean mPanelExpanded = false;
+
+ public MediaCardViewModel(@NonNull Application application) {
+ super(application);
+ }
+
+ public void setPanelExpanded(boolean expanded) {
+ mPanelExpanded = expanded;
+ }
+
+ public boolean getPanelExpanded() {
+ return mPanelExpanded;
+ }
+ }
}
diff --git a/app/src/com/android/car/carlauncher/homescreen/audio/media/MediaCardPanelViewPagerAdapter.java b/app/src/com/android/car/carlauncher/homescreen/audio/media/MediaCardPanelViewPagerAdapter.java
new file mode 100644
index 0000000..007bbf2
--- /dev/null
+++ b/app/src/com/android/car/carlauncher/homescreen/audio/media/MediaCardPanelViewPagerAdapter.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.homescreen.audio.media;
+
+import static com.android.car.carlauncher.homescreen.audio.media.MediaCardPanelViewPagerAdapter.Tab.HistoryTab;
+import static com.android.car.carlauncher.homescreen.audio.media.MediaCardPanelViewPagerAdapter.Tab.OverflowTab;
+import static com.android.car.carlauncher.homescreen.audio.media.MediaCardPanelViewPagerAdapter.Tab.QueueTab;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.TableLayout;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.apps.common.util.ViewUtils;
+import com.android.car.carlauncher.R;
+import com.android.car.media.common.CustomPlaybackAction;
+import com.android.car.media.common.playback.PlaybackViewModel;
+import com.android.car.media.common.playback.PlaybackViewModel.PlaybackController;
+import com.android.car.media.common.playback.PlaybackViewModel.PlaybackStateWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MediaCardPanelViewPagerAdapter extends
+ RecyclerView.Adapter<MediaCardPanelViewPagerAdapter.PanelViewHolder> {
+
+ private final Context mContext;
+ private boolean mHasQueue;
+ private ViewPagerQueueCreator mQueueCreator;
+ private ViewPagerHistoryCreator mHistoryCreator;
+
+ private boolean mHasOverflow;
+ private PlaybackStateWrapper mPlaybackState;
+ private PlaybackController mPlaybackController;
+ private List<PlaybackViewModel.RawCustomPlaybackAction> mCustomActionsToExclude =
+ new ArrayList<PlaybackViewModel.RawCustomPlaybackAction>();
+
+ enum Tab { OverflowTab, QueueTab, HistoryTab };
+
+ public MediaCardPanelViewPagerAdapter(Context context) {
+ this.mContext = context;
+ }
+
+ @NonNull
+ @Override
+ public PanelViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new PanelViewHolder(LayoutInflater.from(mContext).inflate(
+ R.layout.media_card_panel_content_item, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull PanelViewHolder holder, int position) {
+ TableLayout overflowGrid = holder.itemView.findViewById(R.id.overflow_grid);
+ FrameLayout queue = holder.itemView.findViewById(R.id.queue_list_container);
+ FrameLayout history = holder.itemView.findViewById(R.id.history_list_container);
+
+ Tab tab = getTab(position);
+ switch (tab) {
+ case OverflowTab: {
+ updateCustomActionsWithPlaybackState(holder.itemView);
+ break;
+ }
+ case QueueTab: {
+ mQueueCreator.createQueueController(queue);
+ break;
+ }
+ case HistoryTab: {
+ mHistoryCreator.createHistoryController(history);
+ break;
+ }
+ }
+ overflowGrid.setVisibility(tab == OverflowTab ? View.VISIBLE : View.GONE);
+ queue.setVisibility(tab == QueueTab ? View.VISIBLE : View.GONE);
+ history.setVisibility(tab == HistoryTab ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mHasQueue && mHasOverflow ? 3 : (!mHasQueue && !mHasOverflow ? 1 : 2);
+ }
+
+ /** Notify ViewHolder to rebind when a media source queue status changes */
+ public void setHasQueue(boolean hasQueue) {
+ mHasQueue = hasQueue;
+ notifyDataSetChanged();
+ }
+
+ public void setQueueControllerProvider(ViewPagerQueueCreator queueCreator) {
+ mQueueCreator = queueCreator;
+ }
+
+ public void setHistoryControllerProvider(ViewPagerHistoryCreator historyCreator) {
+ mHistoryCreator = historyCreator;
+ }
+
+ /** Notify ViewHolder to rebind when a media source overflow status changes */
+ public void setHasOverflow(boolean hasOverflow) {
+ if (mHasOverflow != hasOverflow) {
+ mHasOverflow = hasOverflow;
+ notifyDataSetChanged();
+ }
+ }
+
+ /** Notify a change in playback state so ViewHolder binds with latest update */
+ public void notifyPlaybackStateChanged(PlaybackStateWrapper playbackState,
+ PlaybackController playbackController,
+ List<PlaybackViewModel.RawCustomPlaybackAction> customActionsToExclude) {
+ mPlaybackState = playbackState;
+ mPlaybackController = playbackController;
+ mCustomActionsToExclude.clear();
+ mCustomActionsToExclude.addAll(customActionsToExclude);
+ if (mHasOverflow) {
+ notifyItemChanged(0);
+ }
+ }
+
+ private void updateCustomActionsWithPlaybackState(View itemView) {
+ List<ImageButton> actions = ViewUtils.getViewsById(itemView,
+ mContext.getResources(), R.array.playback_action_slot_ids, null);
+ Drawable defaultDrawable = mContext.getDrawable(
+ R.drawable.empty_action_drawable);
+ List<PlaybackViewModel.RawCustomPlaybackAction> customActions = mPlaybackState == null
+ ? new ArrayList<PlaybackViewModel.RawCustomPlaybackAction>()
+ : mPlaybackState.getCustomActions();
+ customActions.removeAll(mCustomActionsToExclude);
+ List<ImageButton> actionsToFill = new ArrayList<>();
+ for (int i = 0; i < actions.size(); i++) {
+ ImageButton button = actions.get(i);
+ if (button != null) {
+ actionsToFill.add(button);
+ button.setBackground(null);
+ button.setImageDrawable(defaultDrawable);
+ button.setImageTintList(ColorStateList.valueOf(
+ mContext.getResources().getColor(
+ R.color.car_surface_variant, /* theme */ null)));
+ ViewUtils.setVisible(button, true);
+ }
+ }
+
+ int i = 0;
+ for (PlaybackViewModel.RawCustomPlaybackAction a : customActions) {
+ if (i < actionsToFill.size()) {
+ CustomPlaybackAction customAction = a.fetchDrawable(mContext);
+ if (customAction != null) {
+ actionsToFill.get(i).setImageDrawable(customAction.mIcon);
+ actionsToFill.get(i).setBackgroundColor(Color.TRANSPARENT);
+ actionsToFill.get(i).setImageTintList(ColorStateList.valueOf(
+ mContext.getResources().getColor(
+ R.color.car_on_surface, /* theme */ null)));
+ ViewUtils.setVisible(actionsToFill.get(i), true);
+ actionsToFill.get(i).setOnClickListener(v -> {
+ if (mPlaybackController != null) {
+ mPlaybackController.doCustomAction(
+ customAction.mAction, customAction.mExtras);
+ }
+ });
+ }
+ i++;
+ } else {
+ break;
+ }
+ }
+ }
+
+ private Tab getTab(int index) {
+ if (index == getQueueTabIndex()) {
+ return QueueTab;
+ } else if (index == getOverflowTabIndex()) {
+ return OverflowTab;
+ } else {
+ return HistoryTab;
+ }
+ }
+
+ private int getQueueTabIndex() {
+ if (!mHasQueue) return -1;
+ return getOverflowTabIndex() + 1;
+ }
+
+ private int getOverflowTabIndex() {
+ return mHasOverflow ? 0 : -1;
+ }
+
+ static class PanelViewHolder extends RecyclerView.ViewHolder {
+
+ PanelViewHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+ }
+
+ interface ViewPagerQueueCreator {
+ void createQueueController(ViewGroup queueContainer);
+ }
+
+ interface ViewPagerHistoryCreator {
+ void createHistoryController(ViewGroup historyContainer);
+ }
+}
diff --git a/app/src/com/android/car/carlauncher/homescreen/audio/media/MediaCardPresenter.java b/app/src/com/android/car/carlauncher/homescreen/audio/media/MediaCardPresenter.java
new file mode 100644
index 0000000..4ba575b
--- /dev/null
+++ b/app/src/com/android/car/carlauncher/homescreen/audio/media/MediaCardPresenter.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2023 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.carlauncher.homescreen.audio.media;
+
+import android.content.Intent;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.car.carlauncher.homescreen.CardPresenter;
+import com.android.car.carlauncher.homescreen.HomeCardFragment;
+import com.android.car.carlauncher.homescreen.HomeCardFragment.OnViewLifecycleChangeListener;
+import com.android.car.carlauncher.homescreen.HomeCardInterface;
+import com.android.car.carlauncher.homescreen.audio.AudioModel;
+import com.android.car.carlauncher.homescreen.audio.MediaViewModel;
+import com.android.car.carlauncher.homescreen.ui.CardContent;
+import com.android.car.carlauncher.homescreen.ui.CardHeader;
+import com.android.car.carlauncher.homescreen.ui.DescriptiveTextWithControlsView;
+
+import java.util.List;
+
+/**
+ * A portrait UI version of {@link MediaCardPresenter}
+ */
+public class MediaCardPresenter extends CardPresenter {
+
+ public final MediaIntentRouter mMediaIntentRouter = MediaIntentRouter.getInstance();
+
+ private MediaViewModel mViewModel;
+ private MediaCardFragment mFragment;
+ private boolean mShowMedia = true;
+ private CardContent mCachedCardContent;
+ private CardHeader mCachedCardHeader;
+
+ private final HomeCardFragment.OnViewClickListener mOnViewClickListener =
+ new HomeCardFragment.OnViewClickListener() {
+ @Override
+ public void onViewClicked() {
+ Intent intent = mViewModel.getIntent();
+ mMediaIntentRouter.handleMediaIntent(intent);
+ }
+ };
+
+ @VisibleForTesting
+ final HomeCardInterface.Model.OnModelUpdateListener mOnMediaModelUpdateListener =
+ new HomeCardInterface.Model.OnModelUpdateListener() {
+ @Override
+ public void onModelUpdate(HomeCardInterface.Model model) {
+ MediaViewModel mediaViewModel = (MediaViewModel) model;
+ if (mShowMedia) {
+ if (mediaViewModel.getCardHeader() != null) {
+ mFragment.updateHeaderView(mViewModel.getCardHeader());
+ }
+ if (mediaViewModel.getCardContent() != null) {
+ mFragment.updateContentView(mViewModel.getCardContent());
+ }
+ } else {
+ if (mediaViewModel.getCardHeader() != null) {
+ mCachedCardHeader = mViewModel.getCardHeader();
+ }
+ if (mediaViewModel.getCardContent() != null) {
+ mCachedCardContent = mViewModel.getCardContent();
+ }
+ }
+ }
+ };
+
+ private final HomeCardFragment.OnViewLifecycleChangeListener
+ mOnMediaViewLifecycleChangeListener =
+ new OnViewLifecycleChangeListener() {
+ @Override
+ public void onViewCreated() {
+ mViewModel.setOnProgressUpdateListener(mOnMediaProgressUpdateListener);
+ mViewModel.setOnModelUpdateListener(mOnMediaModelUpdateListener);
+ mViewModel.onCreate(getFragment().requireContext());
+ }
+
+ @Override
+ public void onViewDestroyed() {
+ mViewModel.onDestroy(getFragment().requireContext());
+ }
+ };
+
+ private final MediaCardFragment.OnMediaViewInitializedListener mOnMediaViewInitializedListener =
+ new MediaCardFragment.OnMediaViewInitializedListener() {
+ @Override
+ public void onMediaViewInitialized() {
+ mFragment.getPlaybackControlsActionBar().setModel(
+ mViewModel.getPlaybackViewModel(),
+ mFragment.getViewLifecycleOwner());
+ }
+ };
+
+ private final AudioModel.OnProgressUpdateListener mOnMediaProgressUpdateListener =
+ new AudioModel.OnProgressUpdateListener() {
+ @Override
+ public void onProgressUpdate(AudioModel model, boolean updateProgress) {
+ if (model == null || model.getCardContent() == null
+ || model.getCardHeader() == null) {
+ return;
+ }
+ DescriptiveTextWithControlsView descriptiveTextWithControlsContent =
+ (DescriptiveTextWithControlsView) model.getCardContent();
+ mFragment.updateProgress(
+ descriptiveTextWithControlsContent.getSeekBarViewModel(),
+ updateProgress);
+ }
+ };
+
+ /** Informs this presenter whether or not to process model updates */
+ public void setShowMedia(boolean shouldShowMedia) {
+ mShowMedia = shouldShowMedia;
+ if (shouldShowMedia) {
+ updateFragmentWithCachedContent();
+ }
+ }
+
+ // Deprecated. Use setModel instead.
+ @Override
+ public void setModels(List<HomeCardInterface.Model> models) {
+ // No-op
+ }
+
+ public void setModel(MediaViewModel viewModel) {
+ mViewModel = viewModel;
+ }
+
+ @Override
+ public void setView(HomeCardInterface.View view) {
+ super.setView(view);
+
+ mFragment = (MediaCardFragment) view;
+ mFragment.setOnViewLifecycleChangeListener(mOnMediaViewLifecycleChangeListener);
+ mFragment.setOnViewClickListener(mOnViewClickListener);
+ mFragment.setOnMediaViewInitializedListener(mOnMediaViewInitializedListener);
+ }
+
+ private void updateFragmentWithCachedContent() {
+ if (mCachedCardHeader != null) {
+ mFragment.updateHeaderView(mCachedCardHeader);
+ mCachedCardHeader = null;
+ }
+ if (mCachedCardContent != null) {
+ mFragment.updateContentView(mCachedCardContent);
+ mCachedCardContent = null;
+ }
+ }
+}
diff --git a/app/src/com/android/car/carlauncher/homescreen/audio/media/MediaIntentRouter.java b/app/src/com/android/car/carlauncher/homescreen/audio/media/MediaIntentRouter.java
new file mode 100644
index 0000000..191d4fa
--- /dev/null
+++ b/app/src/com/android/car/carlauncher/homescreen/audio/media/MediaIntentRouter.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2023 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.carlauncher.homescreen.audio.media;
+
+import android.content.Intent;
+
+import com.android.car.carlauncher.homescreen.audio.IntentHandler;
+
+/**
+ * Routes media {@link Intent} to {@link IntentHandler}.
+ */
+public class MediaIntentRouter {
+ private static MediaIntentRouter sInstance;
+ private IntentHandler mIntentHandler;
+
+ /**
+ * @return an instance of {@link MediaIntentRouter}.
+ */
+ public static MediaIntentRouter getInstance() {
+ if (sInstance == null) {
+ sInstance = new MediaIntentRouter();
+ }
+ return sInstance;
+ }
+
+ /**
+ * Register a {@link IntentHandler}.
+ */
+ public void registerMediaIntentHandler(IntentHandler intentHandler) {
+ mIntentHandler = intentHandler;
+ }
+
+ /**
+ * Dispatch a media intent to {@link IntentHandler}
+ */
+ public void handleMediaIntent(Intent intent) {
+ if (intent != null) {
+ mIntentHandler.handleIntent(intent);
+ }
+ }
+}
diff --git a/app/src/com/android/car/carlauncher/recents/CarQuickStepService.java b/app/src/com/android/car/carlauncher/recents/CarQuickStepService.java
index a4c820e..e5e3c1b 100644
--- a/app/src/com/android/car/carlauncher/recents/CarQuickStepService.java
+++ b/app/src/com/android/car/carlauncher/recents/CarQuickStepService.java
@@ -17,7 +17,7 @@
package com.android.car.carlauncher.recents;
import static com.android.car.carlauncher.recents.CarRecentsActivity.OPEN_RECENT_TASK_ACTION;
-import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS;
+import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS;
import android.app.ActivityManager;
import android.app.Service;
@@ -27,11 +27,12 @@
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
-import android.view.SurfaceControl;
import androidx.annotation.Nullable;
import com.android.systemui.shared.recents.IOverviewProxy;
+import com.android.systemui.shared.statusbar.phone.BarTransitions;
+import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
import com.android.wm.shell.recents.IRecentTasks;
import java.util.List;
@@ -141,7 +142,7 @@
}
@Override
- public void onSystemUiStateChanged(int stateFlags) {
+ public void onSystemUiStateChanged(@SystemUiStateFlags long stateFlags) {
// no-op
}
@@ -161,22 +162,58 @@
}
@Override
+ public void onTransitionModeUpdated(int barMode, boolean checkBarModes) {
+ // no-op
+ }
+
+ @Override
public void onNavButtonsDarkIntensityChanged(float darkIntensity) {
// no-op
}
@Override
+ public void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {
+ // no-op
+ }
+
+ @Override
public void enterStageSplitFromRunningApp(boolean leftOrTop) {
// no-op
}
@Override
- public void onNavigationBarSurface(SurfaceControl surface) {
+ public void onTaskbarToggled() {
// no-op
}
@Override
- public void onTaskbarToggled() {
+ public void updateWallpaperVisibility(int displayId, boolean visible) {
+ // no-op
+ }
+
+ @Override
+ public void checkNavBarModes() {
+ // no-op
+ }
+
+ @Override
+ public void finishBarAnimations() {
+ // no-op
+ }
+
+ @Override
+ public void touchAutoDim(boolean reset) {
+ // no-op
+ }
+
+ @Override
+ public void transitionTo(@BarTransitions.TransitionMode int barMode,
+ boolean animate) {
+ // no-op
+ }
+
+ @Override
+ public void appTransitionPending(boolean pending) {
// no-op
}
}
diff --git a/app/src/com/android/car/carlauncher/recents/CarRecentsActivity.java b/app/src/com/android/car/carlauncher/recents/CarRecentsActivity.java
index 9c69491..0731007 100644
--- a/app/src/com/android/car/carlauncher/recents/CarRecentsActivity.java
+++ b/app/src/com/android/car/carlauncher/recents/CarRecentsActivity.java
@@ -62,6 +62,7 @@
private Animator mClearAllAnimator;
private NonDODisabledTaskProvider mNonDODisabledTaskProvider;
private Set<String> mPackagesToHideFromRecents;
+ private boolean mLaunchMostRecentTaskOnDismiss;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -80,6 +81,8 @@
mNonDODisabledTaskProvider = new NonDODisabledTaskProvider(this);
mRecentTasksViewModel.setDisabledTaskProvider(mNonDODisabledTaskProvider);
WindowMetrics windowMetrics = this.getWindowManager().getCurrentWindowMetrics();
+ mLaunchMostRecentTaskOnDismiss = getResources().getBoolean(
+ R.bool.config_launch_most_recent_task_on_recents_dismiss);
mRecentTasksViewModel.init(
/* displayId= */ getDisplay().getDisplayId(),
/* windowWidth= */ windowMetrics.getBounds().width(),
@@ -147,7 +150,9 @@
protected void onResume() {
super.onResume();
if (OPEN_RECENT_TASK_ACTION.equals(getIntent().getAction())) {
- mRecentTasksViewModel.openMostRecentTask();
+ if (mLaunchMostRecentTaskOnDismiss) {
+ mRecentTasksViewModel.openMostRecentTask();
+ }
return;
}
mRecentTasksViewModel.fetchRecentTaskList();
@@ -174,7 +179,9 @@
super.onDestroy();
mNonDODisabledTaskProvider.terminate();
mRecentTasksViewModel.terminate();
- mClearAllAnimator.end();
+ if (mClearAllAnimator.isRunning()) {
+ mClearAllAnimator.end();
+ }
mClearAllAnimator.removeAllListeners();
}
diff --git a/app/src/com/android/car/carlauncher/recents/NonDODisabledTaskProvider.java b/app/src/com/android/car/carlauncher/recents/NonDODisabledTaskProvider.java
index 0a47e26..167a146 100644
--- a/app/src/com/android/car/carlauncher/recents/NonDODisabledTaskProvider.java
+++ b/app/src/com/android/car/carlauncher/recents/NonDODisabledTaskProvider.java
@@ -27,9 +27,8 @@
import android.os.Build;
import android.util.Log;
import android.view.View;
-import android.widget.Toast;
-import com.android.car.carlauncher.R;
+import com.android.car.carlaunchercommon.toasts.NonDrivingOptimizedLaunchFailedToast;
import com.google.common.annotations.VisibleForTesting;
@@ -100,9 +99,8 @@
}
return;
}
- CharSequence appName = ai.loadLabel(mPackageManager);
- String warningText = v.getResources().getString(R.string.driving_toast_text, appName);
- Toast.makeText(v.getContext(), warningText, Toast.LENGTH_SHORT).show();
+ NonDrivingOptimizedLaunchFailedToast.Companion.showToast(
+ v.getContext(), ai.loadLabel(mPackageManager).toString());
};
}
diff --git a/app/src/com/android/car/carlauncher/recents/RecentTasksProvider.java b/app/src/com/android/car/carlauncher/recents/RecentTasksProvider.java
index 884763b..ec1b4af 100644
--- a/app/src/com/android/car/carlauncher/recents/RecentTasksProvider.java
+++ b/app/src/com/android/car/carlauncher/recents/RecentTasksProvider.java
@@ -18,9 +18,9 @@
import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE;
-import static com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_FREEFORM;
-import static com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_SINGLE;
-import static com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_SPLIT;
+import static com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_FREEFORM;
+import static com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_SINGLE;
+import static com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_SPLIT;
import android.app.Activity;
import android.app.ActivityManager;
@@ -49,7 +49,7 @@
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.wm.shell.recents.IRecentTasks;
-import com.android.wm.shell.util.GroupedRecentTaskInfo;
+import com.android.wm.shell.shared.GroupedRecentTaskInfo;
import com.google.common.annotations.VisibleForTesting;
@@ -237,7 +237,7 @@
@Override
public Bitmap getRecentTaskThumbnail(int taskId) {
ThumbnailData thumbnailData = getRecentTaskThumbnailData(taskId);
- return thumbnailData != null ? thumbnailData.thumbnail : null;
+ return thumbnailData != null ? thumbnailData.getThumbnail() : null;
}
@NonNull
@@ -320,11 +320,11 @@
private void getRecentTaskThumbnailAsync(int taskId) {
sRecentsModelExecutor.execute(() -> {
- ThumbnailData thumbnailData = mActivityManagerWrapper.getTaskThumbnail(
- taskId, /* isLowResolution= */ false);
if (!mRecentTaskIdToTaskMap.containsKey(taskId)) {
return;
}
+ ThumbnailData thumbnailData = mActivityManagerWrapper.getTaskThumbnail(
+ taskId, /* isLowResolution= */ false);
mRecentTaskIdToTaskMap.get(taskId).thumbnail = thumbnailData;
if (mRecentsDataChangeListener != null) {
sMainHandler.post(
@@ -336,6 +336,9 @@
@VisibleForTesting
void getRecentTaskIconAsync(int taskId) {
sRecentsModelExecutor.execute(() -> {
+ if (!mRecentTaskIdToTaskMap.containsKey(taskId)) {
+ return;
+ }
Task task = mRecentTaskIdToTaskMap.get(taskId);
Task.TaskKey key = task.key;
Drawable drawableIcon = getIconFromTaskDescription(task.taskDescription);
@@ -350,9 +353,6 @@
drawableIcon = mDefaultIcon;
}
}
- if (!mRecentTaskIdToTaskMap.containsKey(taskId)) {
- return;
- }
mRecentTaskIdToTaskMap.get(taskId).icon = drawableIcon;
if (mRecentsDataChangeListener != null) {
sMainHandler.post(
diff --git a/app/tests/Android.bp b/app/tests/Android.bp
index 322c2de..0eb8b29 100644
--- a/app/tests/Android.bp
+++ b/app/tests/Android.bp
@@ -16,6 +16,7 @@
package {
default_applicable_licenses: ["Android-Apache-2.0"],
+ default_team: "trendy_team_system_experience",
}
android_test {
@@ -27,7 +28,7 @@
libs: [
"android.car",
- "android.test.base",
+ "android.test.base.stubs.system",
"android.car-system-stubs",
],
@@ -54,6 +55,9 @@
"flag-junit",
],
+ // b/341652226: temporarily disable multi-dex until D8 is fixed
+ no_dex_container: true,
+
platform_apis: true,
certificate: "platform",
@@ -78,4 +82,6 @@
"automotive-tests",
"device-tests",
],
+ // TODO(b/319708040): re-enable use_resource_processor
+ use_resource_processor: false,
}
diff --git a/app/tests/AndroidManifest.xml b/app/tests/AndroidManifest.xml
index 8054b2d..1bb4f4e 100644
--- a/app/tests/AndroidManifest.xml
+++ b/app/tests/AndroidManifest.xml
@@ -23,9 +23,7 @@
android:label="@string/app_test_title"
tools:replace="android:label">
- <activity android:name="com.android.car.carlauncher.TaskViewManagerTest$TestActivity"/>
- <activity
- android:name="com.android.car.carlauncher.TaskViewInputInterceptorTest$TestActivity"/>
+ <activity android:name="com.android.car.carlauncher.CarLauncherViewModelTest$TestActivity"/>
<uses-library android:name="android.test.runner"/>
<provider android:name="com.android.car.carlauncher.calmmode.CalmModeQCProvider"
tools:node="remove"/>
diff --git a/docklib-util/res/values/strings.xml b/app/tests/res/values-en-rCA/strings.xml
similarity index 70%
rename from docklib-util/res/values/strings.xml
rename to app/tests/res/values-en-rCA/strings.xml
index 2c24df7..42f3ece 100644
--- a/docklib-util/res/values/strings.xml
+++ b/app/tests/res/values-en-rCA/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,9 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_test_title" msgid="5099375056282404070">"CarLauncherTests"</string>
</resources>
diff --git a/app/tests/src/com/android/car/carlauncher/CarLauncherTest.java b/app/tests/src/com/android/car/carlauncher/CarLauncherTest.java
index 427f065..d62d0fe 100644
--- a/app/tests/src/com/android/car/carlauncher/CarLauncherTest.java
+++ b/app/tests/src/com/android/car/carlauncher/CarLauncherTest.java
@@ -16,47 +16,52 @@
package com.android.car.carlauncher;
+import static android.car.settings.CarSettings.Secure.KEY_UNACCEPTED_TOS_DISABLED_APPS;
import static android.car.settings.CarSettings.Secure.KEY_USER_TOS_ACCEPTED;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.RootMatchers.hasWindowLayoutParams;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.verify;
+import android.car.app.RemoteCarTaskView;
import android.car.test.mocks.AbstractExtendedMockitoTestCase;
-import android.car.user.CarUserManager;
-import android.car.user.CarUserManager.UserLifecycleListener;
import android.content.Intent;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.provider.Settings;
import android.testing.TestableContext;
import android.util.ArraySet;
+import android.view.WindowManager;
import androidx.lifecycle.Lifecycle;
import androidx.test.InstrumentationRegistry;
import androidx.test.core.app.ActivityScenario;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.filters.Suppress;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.Mock;
import java.net.URISyntaxException;
import java.util.Set;
-@Suppress // To be ignored until b/224978827 is fixed
@RunWith(AndroidJUnit4.class)
@SmallTest
public class CarLauncherTest extends AbstractExtendedMockitoTestCase {
@@ -65,8 +70,9 @@
public TestableContext mContext = new TestableContext(InstrumentationRegistry.getContext());
private ActivityScenario<CarLauncher> mActivityScenario;
- @Mock
- private CarUserManager mMockCarUserManager;
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule =
+ DeviceFlagsValueProvider.createCheckFlagsRule();
private static final String TOS_MAP_INTENT = "intent:#Intent;"
+ "component=com.android.car.carlauncher/"
@@ -79,6 +85,11 @@
private static final String CUSTOM_MAP_INTENT = "intent:#Intent;component=com.custom.car.maps/"
+ "com.custom.car.maps.MapActivity;"
+ "action=android.intent.action.MAIN;end";
+ // TOS disabled app list is non empty when TOS is not accepted.
+ private static final String NON_EMPTY_TOS_DISABLED_APPS =
+ "com.test.package1, com.test.package2";
+ // TOS disabled app list is empty when TOS has been accepted or uninitialized.
+ private static final String EMPTY_TOS_DISABLED_APPS = "";
@Override
protected void onSessionBuilder(CustomMockitoSessionBuilder session) {
@@ -95,36 +106,40 @@
@Test
public void onResume_mapsCard_isVisible() {
- mActivityScenario = ActivityScenario.launch(CarLauncher.class);
- mActivityScenario.moveToState(Lifecycle.State.RESUMED);
+ setUpActivityScenario();
- onView(withId(R.id.maps_card)).check(matches(isDisplayed()));
+ onView(withId(R.id.maps_card))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_MEDIA_CARD_FULLSCREEN)
public void onResume_assistiveCard_isVisible() {
- mActivityScenario = ActivityScenario.launch(CarLauncher.class);
- mActivityScenario.moveToState(Lifecycle.State.RESUMED);
+ setUpActivityScenario();
- onView(withId(R.id.top_card)).check(matches(isDisplayed()));
+ onView(withId(R.id.top_card))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_MEDIA_CARD_FULLSCREEN)
+ public void onResume_fullscreenMediaCard_assistiveCard_isGone() {
+ setUpActivityScenario();
+
+ onView(withId(R.id.top_card))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
}
@Test
public void onResume_audioCard_isVisible() {
- mActivityScenario = ActivityScenario.launch(CarLauncher.class);
- mActivityScenario.moveToState(Lifecycle.State.RESUMED);
+ setUpActivityScenario();
- onView(withId(R.id.bottom_card)).check(matches(isDisplayed()));
- }
-
- @Test
- public void onDestroy_unregistersUserLifecycleListener() {
- mActivityScenario = ActivityScenario.launch(CarLauncher.class);
- mActivityScenario.onActivity(activity -> activity.setCarUserManager(mMockCarUserManager));
-
- mActivityScenario.moveToState(Lifecycle.State.DESTROYED);
-
- verify(mMockCarUserManager).removeListener(any(UserLifecycleListener.class));
+ onView(withId(R.id.bottom_card))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
}
@Test
@@ -193,9 +208,11 @@
}
@Test
- public void onCreate_tosStateContentObserver_tosAccepted() {
+ public void onCreate_whenTosAccepted_tosContentObserverIsNull() {
TestableContext mContext = new TestableContext(InstrumentationRegistry.getContext());
Settings.Secure.putInt(mContext.getContentResolver(), KEY_USER_TOS_ACCEPTED, 2);
+ Settings.Secure.putString(mContext.getContentResolver(), KEY_UNACCEPTED_TOS_DISABLED_APPS,
+ EMPTY_TOS_DISABLED_APPS);
mActivityScenario = ActivityScenario.launch(new Intent(mContext, CarLauncher.class));
mActivityScenario.moveToState(Lifecycle.State.RESUMED);
@@ -207,9 +224,11 @@
}
@Test
- public void onCreate_registerTosStateContentObserver_tosNotAccepted() {
+ public void onCreate_whenTosNotAccepted_tosContentObserverIsNotNull() {
TestableContext mContext = new TestableContext(InstrumentationRegistry.getContext());
Settings.Secure.putInt(mContext.getContentResolver(), KEY_USER_TOS_ACCEPTED, 1);
+ Settings.Secure.putString(mContext.getContentResolver(), KEY_UNACCEPTED_TOS_DISABLED_APPS,
+ NON_EMPTY_TOS_DISABLED_APPS);
mActivityScenario = ActivityScenario.launch(new Intent(mContext, CarLauncher.class));
mActivityScenario.moveToState(Lifecycle.State.RESUMED);
@@ -221,9 +240,11 @@
}
@Test
- public void onCreate_registerTosStateContentObserver_tosNotInitialized() {
+ public void onCreate_whenTosNotInitialized_tosContentObserverIsNotNull() {
TestableContext mContext = new TestableContext(InstrumentationRegistry.getContext());
Settings.Secure.putInt(mContext.getContentResolver(), KEY_USER_TOS_ACCEPTED, 0);
+ Settings.Secure.putString(mContext.getContentResolver(), KEY_UNACCEPTED_TOS_DISABLED_APPS,
+ EMPTY_TOS_DISABLED_APPS);
mActivityScenario = ActivityScenario.launch(new Intent(mContext, CarLauncher.class));
mActivityScenario.moveToState(Lifecycle.State.RESUMED);
@@ -235,9 +256,11 @@
}
@Test
- public void recreate_tosStateContentObserver_tosNotAccepted() {
+ public void recreate_afterTosIsAccepted_tosStateContentObserverIsNull() {
TestableContext mContext = new TestableContext(InstrumentationRegistry.getContext());
- Settings.Secure.putInt(mContext.getContentResolver(), KEY_USER_TOS_ACCEPTED, 1);
+ Settings.Secure.putInt(mContext.getContentResolver(), KEY_USER_TOS_ACCEPTED, 0);
+ Settings.Secure.putString(mContext.getContentResolver(), KEY_UNACCEPTED_TOS_DISABLED_APPS,
+ NON_EMPTY_TOS_DISABLED_APPS);
mActivityScenario = ActivityScenario.launch(new Intent(mContext, CarLauncher.class));
@@ -246,30 +269,94 @@
// Accept TOS
Settings.Secure.putInt(mContext.getContentResolver(), KEY_USER_TOS_ACCEPTED, 2);
+ Settings.Secure.putString(mContext.getContentResolver(),
+ KEY_UNACCEPTED_TOS_DISABLED_APPS, EMPTY_TOS_DISABLED_APPS);
activity.mTosContentObserver.onChange(true);
});
+
// Content observer is null after recreate
mActivityScenario.onActivity(activity -> assertNull(activity.mTosContentObserver));
}
@Test
- public void recreate_tosStateContentObserver_tosNotInitialized() {
+ public void recreate_afterTosIsInitialized_tosStateContentObserverIsNotNull() {
TestableContext mContext = new TestableContext(InstrumentationRegistry.getContext());
Settings.Secure.putInt(mContext.getContentResolver(), KEY_USER_TOS_ACCEPTED, 0);
+ Settings.Secure.putString(mContext.getContentResolver(), KEY_UNACCEPTED_TOS_DISABLED_APPS,
+ EMPTY_TOS_DISABLED_APPS);
mActivityScenario = ActivityScenario.launch(new Intent(mContext, CarLauncher.class));
mActivityScenario.onActivity(activity -> {
assertNotNull(activity.mTosContentObserver); // Content observer is setup
- // TOS changed to unaccepted
+ // Initialize TOS
Settings.Secure.putInt(mContext.getContentResolver(), KEY_USER_TOS_ACCEPTED, 1);
+ Settings.Secure.putString(mContext.getContentResolver(),
+ KEY_UNACCEPTED_TOS_DISABLED_APPS, NON_EMPTY_TOS_DISABLED_APPS);
activity.mTosContentObserver.onChange(true);
});
+
// Content observer is not null after recreate
mActivityScenario.onActivity(activity -> assertNotNull(activity.mTosContentObserver));
}
+ @Test
+ public void recreate_afterTosIsInitialized_releaseTaskView() {
+ TestableContext mContext = new TestableContext(InstrumentationRegistry.getContext());
+ Settings.Secure.putInt(mContext.getContentResolver(), KEY_USER_TOS_ACCEPTED, 0);
+ Settings.Secure.putString(mContext.getContentResolver(), KEY_UNACCEPTED_TOS_DISABLED_APPS,
+ EMPTY_TOS_DISABLED_APPS);
+
+ mActivityScenario = ActivityScenario.launch(new Intent(mContext, CarLauncher.class));
+
+ mActivityScenario.onActivity(activity -> {
+ assertNotNull(activity.mCarLauncherViewModel); // CarLauncherViewModel is setup
+
+ RemoteCarTaskView oldRemoteCarTaskView =
+ activity.mCarLauncherViewModel.getRemoteCarTaskView().getValue();
+ assertNotNull(oldRemoteCarTaskView);
+
+ // Initialize TOS
+ Settings.Secure.putInt(mContext.getContentResolver(), KEY_USER_TOS_ACCEPTED, 1);
+ Settings.Secure.putString(mContext.getContentResolver(),
+ KEY_UNACCEPTED_TOS_DISABLED_APPS, NON_EMPTY_TOS_DISABLED_APPS);
+ activity.mTosContentObserver.onChange(true);
+
+ // Different instance of task view since TOS has gone from uninitialized to initialized
+ assertThat(oldRemoteCarTaskView).isNotSameInstanceAs(
+ activity.mCarLauncherViewModel.getRemoteCarTaskView().getValue());
+ });
+ }
+
+ @Test
+ public void recreate_afterTosIsAccepted_releaseTaskView() {
+ TestableContext mContext = new TestableContext(InstrumentationRegistry.getContext());
+ Settings.Secure.putInt(mContext.getContentResolver(), KEY_USER_TOS_ACCEPTED, 1);
+ Settings.Secure.putString(mContext.getContentResolver(), KEY_UNACCEPTED_TOS_DISABLED_APPS,
+ NON_EMPTY_TOS_DISABLED_APPS);
+
+ mActivityScenario = ActivityScenario.launch(new Intent(mContext, CarLauncher.class));
+
+ mActivityScenario.onActivity(activity -> {
+ assertNotNull(activity.mCarLauncherViewModel); // CarLauncherViewModel is setup
+
+ RemoteCarTaskView oldRemoteCarTaskView =
+ activity.mCarLauncherViewModel.getRemoteCarTaskView().getValue();
+ assertNotNull(oldRemoteCarTaskView);
+
+ // Accept TOS
+ Settings.Secure.putInt(mContext.getContentResolver(), KEY_USER_TOS_ACCEPTED, 2);
+ Settings.Secure.putString(mContext.getContentResolver(),
+ KEY_UNACCEPTED_TOS_DISABLED_APPS, EMPTY_TOS_DISABLED_APPS);
+ activity.mTosContentObserver.onChange(true);
+
+ // Different instance of task view since TOS has been accepted
+ assertThat(oldRemoteCarTaskView).isNotSameInstanceAs(
+ activity.mCarLauncherViewModel.getRemoteCarTaskView().getValue());
+ });
+ }
+
private Intent createIntentFromString(String intentString) {
try {
return Intent.parseUri(intentString, Intent.URI_ANDROID_APP_SCHEME);
@@ -284,4 +371,16 @@
packages.add("com.android.car.assistant");
return packages;
}
+
+ private void setUpActivityScenario() {
+ mActivityScenario = ActivityScenario.launch(CarLauncher.class);
+ mActivityScenario.moveToState(Lifecycle.State.RESUMED);
+ mActivityScenario.onActivity(activity -> {
+ activity.runOnUiThread(new Runnable() {
+ public void run() {
+ activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
+ }
+ });
+ });
+ }
}
diff --git a/app/tests/src/com/android/car/carlauncher/CarLauncherViewModelFactoryTest.java b/app/tests/src/com/android/car/carlauncher/CarLauncherViewModelFactoryTest.java
index 722a3ed..b11a40f 100644
--- a/app/tests/src/com/android/car/carlauncher/CarLauncherViewModelFactoryTest.java
+++ b/app/tests/src/com/android/car/carlauncher/CarLauncherViewModelFactoryTest.java
@@ -34,7 +34,7 @@
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.android.car.carlauncher.TaskViewManagerTest.TestActivity;
+import com.android.car.carlauncher.CarLauncherViewModelTest.TestActivity;
import org.junit.After;
import org.junit.Before;
@@ -64,7 +64,7 @@
.createWindowContext(TYPE_APPLICATION_STARTING, /* options */ null);
when(mContext.createWindowContext(eq(WindowManager.LayoutParams.TYPE_APPLICATION_STARTING),
any())).thenReturn(windowContext);
- mCarLauncherViewModelFactory = new CarLauncherViewModelFactory(mContext, mIntent);
+ mCarLauncherViewModelFactory = new CarLauncherViewModelFactory(mContext);
}
@After
diff --git a/app/tests/src/com/android/car/carlauncher/CarLauncherViewModelTest.java b/app/tests/src/com/android/car/carlauncher/CarLauncherViewModelTest.java
index 087d5a1..33d1611 100644
--- a/app/tests/src/com/android/car/carlauncher/CarLauncherViewModelTest.java
+++ b/app/tests/src/com/android/car/carlauncher/CarLauncherViewModelTest.java
@@ -18,6 +18,7 @@
import static com.google.common.truth.Truth.assertThat;
+import android.app.Activity;
import android.app.Instrumentation;
import android.car.app.RemoteCarTaskView;
import android.car.test.mocks.AbstractExtendedMockitoTestCase;
@@ -29,8 +30,6 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
-import com.android.car.carlauncher.TaskViewManagerTest.TestActivity;
-
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -38,6 +37,9 @@
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
@RunWith(AndroidJUnit4.class)
public final class CarLauncherViewModelTest extends AbstractExtendedMockitoTestCase {
private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
@@ -93,7 +95,8 @@
}
private CarLauncherViewModel createCarLauncherViewModel() {
- CarLauncherViewModel carLauncherViewModel = new CarLauncherViewModel(mActivity, mIntent);
+ CarLauncherViewModel carLauncherViewModel = new CarLauncherViewModel(mActivity);
+ carLauncherViewModel.initializeRemoteCarTaskView(mIntent);
runOnMain(() -> carLauncherViewModel.getRemoteCarTaskView().observeForever(
remoteCarTaskView -> mRemoteCarTaskView = remoteCarTaskView));
mInstrumentation.waitForIdleSync();
@@ -110,4 +113,21 @@
private void runOnMain(Runnable runnable) {
mContext.getMainExecutor().execute(runnable);
}
+
+
+ public static class TestActivity extends Activity {
+ private static final int FINISH_TIMEOUT_MS = 1000;
+ private final CountDownLatch mDestroyed = new CountDownLatch(1);
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mDestroyed.countDown();
+ }
+
+ void finishCompletely() throws InterruptedException {
+ finish();
+ mDestroyed.await(FINISH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ }
+ }
}
diff --git a/app/tests/src/com/android/car/carlauncher/TaskViewInputInterceptorTest.java b/app/tests/src/com/android/car/carlauncher/TaskViewInputInterceptorTest.java
deleted file mode 100644
index 7c90094..0000000
--- a/app/tests/src/com/android/car/carlauncher/TaskViewInputInterceptorTest.java
+++ /dev/null
@@ -1,507 +0,0 @@
-/*
- * Copyright (C) 2022 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.carlauncher;
-
-import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY;
-import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-
-import android.app.Activity;
-import android.app.UiAutomation;
-import android.car.test.mocks.AbstractExtendedMockitoTestCase;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Rect;
-import android.hardware.input.InputManager;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.SystemClock;
-import android.view.InputDevice;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.WindowManager;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.lifecycle.Lifecycle;
-import androidx.test.core.app.ActivityScenario;
-import androidx.test.ext.junit.rules.ActivityScenarioRule;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.Suppress;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-@Suppress // CarTaskView in Launcher are not supported anymore and hence this test is not required
-@RunWith(AndroidJUnit4.class)
-public class TaskViewInputInterceptorTest extends AbstractExtendedMockitoTestCase {
- private static final UiAutomation UI_AUTOMATION =
- InstrumentationRegistry.getInstrumentation().getUiAutomation();
- private final List<ControlledCarTaskView> mControlledCarTaskViews = new ArrayList<>();
-
- @Rule
- public ActivityScenarioRule mActivityRule = new ActivityScenarioRule<>(TestActivity.class);
-
- @Mock TaskViewManager mTaskViewManager;
- @Captor ArgumentCaptor<WindowManager.LayoutParams> mLayoutParamsArgumentCaptor;
- @Captor ArgumentCaptor<View> mSpyWindowArgumentCaptor;
- private CountDownLatch mIdleHandlerLatch = new CountDownLatch(1);
- private TestActivity mActivity;
- private TaskViewInputInterceptor mTaskViewInputInterceptor;
- private ActivityScenario<TestActivity> mScenario;
-
- @Before
- public void setup() throws Exception {
- mScenario = mActivityRule.getScenario();
- mScenario.onActivity(activity -> mActivity = activity);
-
- if (Looper.myLooper() == null) {
- Looper.prepare();
- }
- runOnMainAndWait(
- () -> {
- mTaskViewInputInterceptor =
- new TaskViewInputInterceptor(mActivity, mTaskViewManager);
- });
-
- doReturn(mControlledCarTaskViews).when(mTaskViewManager).getControlledTaskViews();
- }
-
- @After
- public void tearDown() throws InterruptedException {
- if (mActivity == null) {
- return;
- }
- mActivity.finishCompletely();
- }
-
- @Test
- public void init_addsSpyWindow() throws Exception {
- runOnMainAndWait(() -> mTaskViewInputInterceptor.init());
-
- verify(mActivity.mSpyWm).addView(any(), mLayoutParamsArgumentCaptor.capture());
- assertThat(mLayoutParamsArgumentCaptor.getValue().inputFeatures)
- .isEqualTo(INPUT_FEATURE_SPY);
- }
-
- @Test
- public void init_again_doesNothing() throws Exception {
- runOnMainAndWait(() -> mTaskViewInputInterceptor.init());
-
- runOnMainAndWait(() -> mTaskViewInputInterceptor.init());
-
- verify(mActivity.mSpyWm, times(1)).addView(any(), any());
- }
-
- @Test
- public void activityStopped_removesSpyWindow() throws Exception {
- runOnMainAndWait(() -> mTaskViewInputInterceptor.init());
-
- mScenario.moveToState(Lifecycle.State.CREATED);
-
- verify(mActivity.mSpyWm).removeView(any());
- }
-
- @Test
- public void activityStoppedStarted_addsSpyWindow() throws Exception {
- runOnMainAndWait(() -> mTaskViewInputInterceptor.init());
- mScenario.moveToState(Lifecycle.State.CREATED);
-
- mScenario.moveToState(Lifecycle.State.STARTED);
-
- verify(mActivity.mSpyWm, times(2)).addView(any(), any());
- }
-
- @Test
- public void singleTap_insideTaskView_capturingEnabled_noCapturing() throws Exception {
- runOnMainAndWait(() -> mTaskViewInputInterceptor.init());
- verify(mActivity.mSpyWm).addView(mSpyWindowArgumentCaptor.capture(), any());
- createControlledCarTaskView(new Rect(10, 0, 30, 100), /* capturingEnabled= */ true);
-
- View spyWindow = mSpyWindowArgumentCaptor.getValue();
- final long eventTime = SystemClock.uptimeMillis();
- MotionEvent downEvent =
- MotionEvent.obtain(
- /* downTime= */ eventTime,
- /* eventTime= */ eventTime,
- MotionEvent.ACTION_DOWN,
- /* x= */ 11,
- /* y= */ 2,
- /* metaState= */ 0);
- MotionEvent moveEventOnTheDownEventLocation =
- MotionEvent.obtain(
- /* downTime= */ eventTime,
- /* eventTime= */ eventTime,
- MotionEvent.ACTION_MOVE,
- /* x= */ 11,
- /* y= */ 2,
- /* metaState= */ 0);
- downEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN);
- MotionEvent upEvent =
- MotionEvent.obtain(
- /* downTime= */ eventTime,
- /* eventTime= */ eventTime,
- MotionEvent.ACTION_UP,
- /* x= */ 11,
- /* y= */ 2,
- /* metaState= */ 0);
- upEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN);
-
- // Act
- spyWindow.dispatchTouchEvent(downEvent);
- spyWindow.dispatchTouchEvent(moveEventOnTheDownEventLocation);
- spyWindow.dispatchTouchEvent(upEvent);
-
- // Assert
- verify(mActivity.mSpyInputManager, times(0)).pilferPointers(any());
- }
-
- @Test
- public void longPress_insideTaskView_capturingEnabled_capturesGesture() throws Exception {
- runOnMainAndWait(() -> mTaskViewInputInterceptor.init());
- verify(mActivity.mSpyWm).addView(mSpyWindowArgumentCaptor.capture(), any());
- ControlledCarTaskView taskView =
- createControlledCarTaskView(new Rect(10, 0, 30, 100), /* capturingEnabled= */ true);
- View.OnLongClickListener taskViewLongClickListener = mock(View.OnLongClickListener.class);
- doReturn(taskViewLongClickListener).when(taskView).getOnLongClickListener();
- View spyWindow = mSpyWindowArgumentCaptor.getValue();
-
- // Act
- final long eventTime = SystemClock.uptimeMillis();
- MotionEvent downEvent =
- MotionEvent.obtain(
- /* downTime= */ eventTime,
- /* eventTime= */ eventTime,
- MotionEvent.ACTION_DOWN,
- /* x= */ 11,
- /* y= */ 2,
- /* metaState= */ 0);
- downEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN);
- runOnMainAndWait(() -> spyWindow.dispatchTouchEvent(downEvent));
- waitForLongPressTimeout();
-
- // Assert
- verify(mActivity.mSpyInputManager, times(1)).pilferPointers(any());
- verify(taskViewLongClickListener).onLongClick(any());
- }
-
- @Test
- public void longPress_insideTaskView_capturingDisabled_noCapturing() throws Exception {
- runOnMainAndWait(() -> mTaskViewInputInterceptor.init());
- verify(mActivity.mSpyWm).addView(mSpyWindowArgumentCaptor.capture(), any());
- ControlledCarTaskView taskView =
- createControlledCarTaskView(
- new Rect(10, 0, 30, 100), /* capturingEnabled= */ false);
- View.OnLongClickListener taskViewLongClickListener = mock(View.OnLongClickListener.class);
- doReturn(taskViewLongClickListener).when(taskView).getOnLongClickListener();
- View spyWindow = mSpyWindowArgumentCaptor.getValue();
-
- // Act
- final long eventTime = SystemClock.uptimeMillis();
- MotionEvent downEvent =
- MotionEvent.obtain(
- /* downTime= */ eventTime,
- /* eventTime= */ eventTime,
- MotionEvent.ACTION_DOWN,
- /* x= */ 11,
- /* y= */ 2,
- /* metaState= */ 0);
- downEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN);
- runOnMainAndWait(() -> spyWindow.dispatchTouchEvent(downEvent));
- waitForLongPressTimeout();
-
- // Assert
- verify(mActivity.mSpyInputManager, times(0)).pilferPointers(any());
- verifyZeroInteractions(taskViewLongClickListener);
- }
-
- private void waitForLongPressTimeout() throws InterruptedException {
- CountDownLatch l = new CountDownLatch(1);
- Handler handler = new Handler(Looper.getMainLooper());
- handler.postDelayed(() -> l.countDown(), ViewConfiguration.getLongPressTimeout());
- l.await(1, TimeUnit.SECONDS);
- }
-
- @Test
- public void swipeGesture_whenActionDownInsideTaskView_capturingEnabled_capturesGesture()
- throws Exception {
- // Arrange
- runOnMainAndWait(() -> mTaskViewInputInterceptor.init());
- verify(mActivity.mSpyWm).addView(mSpyWindowArgumentCaptor.capture(), any());
- createControlledCarTaskView(new Rect(10, 0, 30, 100), /* capturingEnabled= */ true);
-
- View spyWindow = mSpyWindowArgumentCaptor.getValue();
- final long eventTime = SystemClock.uptimeMillis();
- MotionEvent downEvent =
- MotionEvent.obtain(
- /* downTime= */ eventTime,
- /* eventTime= */ eventTime,
- MotionEvent.ACTION_DOWN,
- /* x= */ 11,
- /* y= */ 2,
- /* metaState= */ 0);
- downEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN);
- MotionEvent moveEvent =
- MotionEvent.obtain(
- /* downTime= */ eventTime,
- /* eventTime= */ eventTime,
- MotionEvent.ACTION_MOVE,
- /* x= */ 12,
- /* y= */ 7,
- /* metaState= */ 0);
- moveEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN);
- MotionEvent upEvent =
- MotionEvent.obtain(
- /* downTime= */ eventTime,
- /* eventTime= */ eventTime,
- MotionEvent.ACTION_UP,
- /* x= */ 12,
- /* y= */ 9,
- /* metaState= */ 0);
- upEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN);
-
- // Act
- runOnMainAndWait(
- () -> {
- spyWindow.dispatchTouchEvent(downEvent);
- spyWindow.dispatchTouchEvent(moveEvent);
- spyWindow.dispatchTouchEvent(upEvent);
- });
-
- // Assert
- verify(mActivity.mSpyInputManager, times(2)).pilferPointers(any());
- assertThat(mActivity.mEventsReceived.size()).isEqualTo(3);
- assertThat(mActivity.mEventsReceived.get(0).getAction()).isEqualTo(MotionEvent.ACTION_DOWN);
- assertThat(mActivity.mEventsReceived.get(0).getX()).isEqualTo(12);
- assertThat(mActivity.mEventsReceived.get(0).getY()).isEqualTo(7);
- assertThat(mActivity.mEventsReceived.get(1).getAction()).isEqualTo(MotionEvent.ACTION_MOVE);
- assertThat(mActivity.mEventsReceived.get(1).getX()).isEqualTo(12);
- assertThat(mActivity.mEventsReceived.get(1).getY()).isEqualTo(7);
- assertThat(mActivity.mEventsReceived.get(2).getAction()).isEqualTo(MotionEvent.ACTION_UP);
- assertThat(mActivity.mEventsReceived.get(2).getX()).isEqualTo(12);
- assertThat(mActivity.mEventsReceived.get(2).getY()).isEqualTo(9);
- }
-
- @Test
- public void swipeGesture_whenActionDownInsideTaskView_capturingDisabled_noCapturing()
- throws Exception {
- // Arrange
- runOnMainAndWait(() -> mTaskViewInputInterceptor.init());
- verify(mActivity.mSpyWm).addView(mSpyWindowArgumentCaptor.capture(), any());
- createControlledCarTaskView(new Rect(10, 0, 30, 100), /* capturingEnabled= */ false);
-
- View spyWindow = mSpyWindowArgumentCaptor.getValue();
- final long eventTime = SystemClock.uptimeMillis();
- MotionEvent downEvent =
- MotionEvent.obtain(
- /* downTime= */ eventTime,
- /* eventTime= */ eventTime,
- MotionEvent.ACTION_DOWN,
- /* x= */ 11,
- /* y= */ 2,
- /* metaState= */ 0);
- downEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN);
- MotionEvent moveEvent =
- MotionEvent.obtain(
- /* downTime= */ eventTime,
- /* eventTime= */ eventTime,
- MotionEvent.ACTION_MOVE,
- /* x= */ 12,
- /* y= */ 7,
- /* metaState= */ 0);
- moveEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN);
- MotionEvent upEvent =
- MotionEvent.obtain(
- /* downTime= */ eventTime,
- /* eventTime= */ eventTime,
- MotionEvent.ACTION_UP,
- /* x= */ 12,
- /* y= */ 9,
- /* metaState= */ 0);
- upEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN);
-
- // Act
- runOnMainAndWait(
- () -> {
- spyWindow.dispatchTouchEvent(downEvent);
- spyWindow.dispatchTouchEvent(moveEvent);
- spyWindow.dispatchTouchEvent(upEvent);
- });
-
- // Assert
- verify(mActivity.mSpyInputManager, times(0)).pilferPointers(any());
- assertThat(mActivity.mEventsReceived.size()).isEqualTo(0);
- }
-
- @Test
- public void swipeGesture_whenActionDownOutsideTaskView_capturingEnabled_noCapturing()
- throws Exception {
- // Arrange
- runOnMainAndWait(() -> mTaskViewInputInterceptor.init());
- verify(mActivity.mSpyWm).addView(mSpyWindowArgumentCaptor.capture(), any());
- createControlledCarTaskView(new Rect(10, 0, 30, 100), /* capturingEnabled= */ true);
-
- View spyWindow = mSpyWindowArgumentCaptor.getValue();
- final long eventTime = SystemClock.uptimeMillis();
- MotionEvent downEvent =
- MotionEvent.obtain(
- /* downTime= */ eventTime,
- /* eventTime= */ eventTime,
- MotionEvent.ACTION_DOWN,
- /* x= */ 8,
- /* y= */ 2,
- /* metaState= */ 0);
- downEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN);
- MotionEvent moveEvent =
- MotionEvent.obtain(
- /* downTime= */ eventTime,
- /* eventTime= */ eventTime,
- MotionEvent.ACTION_MOVE,
- /* x= */ 8,
- /* y= */ 7,
- /* metaState= */ 0);
- moveEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN);
- MotionEvent upEvent =
- MotionEvent.obtain(
- /* downTime= */ eventTime,
- /* eventTime= */ eventTime,
- MotionEvent.ACTION_UP,
- /* x= */ 8,
- /* y= */ 9,
- /* metaState= */ 0);
- upEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN);
-
- // Act
- runOnMainAndWait(
- () -> {
- spyWindow.dispatchTouchEvent(downEvent);
- spyWindow.dispatchTouchEvent(moveEvent);
- spyWindow.dispatchTouchEvent(upEvent);
- });
-
- // Assert
- verify(mActivity.mSpyInputManager, times(0)).pilferPointers(any());
- assertThat(mActivity.mEventsReceived.size()).isEqualTo(0);
- }
-
- private ControlledCarTaskView createControlledCarTaskView(
- Rect bounds, boolean capturingEnabled) {
- ControlledCarTaskView taskView = mock(ControlledCarTaskView.class);
-
- doAnswer(
- invocation -> {
- Rect r = invocation.getArgument(0);
- r.set(bounds);
- return null;
- })
- .when(taskView)
- .getBoundsOnScreen(any());
-
- doReturn(
- ControlledCarTaskViewConfig.builder()
- .setActivityIntent(new Intent())
- .setCaptureGestures(capturingEnabled)
- .setCaptureLongPress(capturingEnabled)
- .build())
- .when(taskView)
- .getConfig();
- mControlledCarTaskViews.add(taskView);
- return taskView;
- }
-
- private void runOnMainAndWait(Runnable r) throws Exception {
- mActivity
- .getMainExecutor()
- .execute(
- () -> {
- r.run();
- mIdleHandlerLatch.countDown();
- mIdleHandlerLatch = new CountDownLatch(1);
- });
- mIdleHandlerLatch.await(5, TimeUnit.SECONDS);
- }
-
- public static class TestActivity extends Activity {
- private static final int FINISH_TIMEOUT_MS = 1000;
- private final CountDownLatch mDestroyed = new CountDownLatch(1);
- private final List<MotionEvent> mEventsReceived = new ArrayList<>();
- private WindowManager mSpyWm;
- private InputManager mSpyInputManager;
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mSpyWm = spy(getSystemService(WindowManager.class));
- mSpyInputManager = spy(getSystemService(InputManager.class));
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- mDestroyed.countDown();
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- // MotionEvent.obtain() is important otherwise the event is recycled and by the time
- // assertion happens, the values might be changed
- mEventsReceived.add(MotionEvent.obtain(event));
- return super.onTouchEvent(event);
- }
-
- @Override
- public Object getSystemService(@NonNull String name) {
- if (name.equals(Context.WINDOW_SERVICE)) {
- if (mSpyWm != null) {
- return mSpyWm;
- }
- }
- if (name.equals(Context.INPUT_SERVICE)) {
- if (mSpyInputManager != null) {
- return mSpyInputManager;
- }
- }
- return super.getSystemService(name);
- }
-
- void finishCompletely() throws InterruptedException {
- finish();
- mDestroyed.await(FINISH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
- }
- }
-}
diff --git a/app/tests/src/com/android/car/carlauncher/TaskViewManagerTest.java b/app/tests/src/com/android/car/carlauncher/TaskViewManagerTest.java
deleted file mode 100644
index 1e2411d..0000000
--- a/app/tests/src/com/android/car/carlauncher/TaskViewManagerTest.java
+++ /dev/null
@@ -1,1068 +0,0 @@
-/*
- * Copyright (C) 2022 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.carlauncher;
-
-import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
-import static android.view.Display.DEFAULT_DISPLAY;
-import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_TASK;
-import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT;
-
-import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_SWITCHING;
-import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.notNull;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-
-import android.app.Activity;
-import android.app.ActivityManager;
-import android.app.ActivityTaskManager;
-import android.app.TaskStackListener;
-import android.car.Car;
-import android.car.app.CarActivityManager;
-import android.car.test.mocks.AbstractExtendedMockitoTestCase;
-import android.car.user.CarUserManager;
-import android.content.ComponentName;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Binder;
-import android.os.IBinder;
-import android.os.Looper;
-import android.view.SurfaceControl;
-import android.view.SurfaceHolder;
-import android.window.TaskAppearedInfo;
-import android.window.WindowContainerToken;
-import android.window.WindowContainerTransaction;
-
-import androidx.annotation.NonNull;
-import androidx.lifecycle.Lifecycle;
-import androidx.test.core.app.ActivityScenario;
-import androidx.test.ext.junit.rules.ActivityScenarioRule;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.carlauncher.taskstack.TaskStackChangeListeners;
-import com.android.dx.mockito.inline.extended.ExtendedMockito;
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.HandlerExecutor;
-import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.startingsurface.StartingWindowController;
-import com.android.wm.shell.sysui.ShellController;
-import com.android.wm.shell.sysui.ShellInit;
-import com.android.wm.shell.taskview.TaskView;
-import com.android.wm.shell.transition.Transitions;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.invocation.InvocationOnMock;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.stream.Collectors;
-
-//TODO(b/266742500): Test that uses setUpLaunchRootTaskView get null listener
-@RunWith(AndroidJUnit4.class)
-public class TaskViewManagerTest extends AbstractExtendedMockitoTestCase {
- @Rule
- public ActivityScenarioRule mActivityRule = new ActivityScenarioRule<>(TestActivity.class);
-
- @Mock
- private ShellTaskOrganizer mOrganizer;
- @Mock
- private SyncTransactionQueue mSyncQueue;
- @Mock
- private Transitions mTransitions;
- @Mock
- private HandlerExecutor mShellExecutor;
- @Mock
- private CarActivityManager mCarActivityManager;
- @Mock
- private Car mCar;
- @Mock
- private TaskStackChangeListeners mTaskStackChangeListeners;
- @Mock
- private CarUserManager mCarUserManager;
- @Mock
- private WindowContainerToken mToken;
-
- @Mock
- private ShellController mShellController;
- @Mock
- private StartingWindowController mStartingWindowController;
- @Mock
- private TaskViewInputInterceptor mTaskViewInputInterceptor;
-
- @Captor
- private ArgumentCaptor<TaskStackListener> mTaskStackListenerArgumentCaptor;
- @Captor
- private ArgumentCaptor<CarUserManager.UserLifecycleListener>
- mUserLifecycleListenerArgumentCaptor;
-
- private TestActivity mActivity;
- private Car.CarServiceLifecycleListener mCarServiceLifecycleListener;
- private ActivityTaskManager mSpyActivityTaskManager;
- private CountDownLatch mIdleHandlerLatch = new CountDownLatch(1);
- private SurfaceControl mLeash;
-
- @Override
- protected void onSessionBuilder(@NonNull CustomMockitoSessionBuilder builder) {
- builder.spyStatic(ActivityTaskManager.class);
- builder.spyStatic(Car.class);
- builder.spyStatic(TaskStackChangeListeners.class);
- }
-
- @Before
- public void setUp() {
- ExtendedMockito.doAnswer(invocation -> {
- mCarServiceLifecycleListener = invocation.getArgument(3);
- return mCar;
- }).when(() -> Car.createCar(any(), any(), anyLong(), any()));
- when(mCar.getCarManager(eq(Car.CAR_ACTIVITY_SERVICE))).thenReturn(mCarActivityManager);
- when(mCar.getCarManager(eq(Car.CAR_USER_SERVICE))).thenReturn(mCarUserManager);
-
- ExtendedMockito.doReturn(mTaskStackChangeListeners).when(() ->
- TaskStackChangeListeners.getInstance());
- doNothing().when(mTaskStackChangeListeners).registerTaskStackListener(
- mTaskStackListenerArgumentCaptor.capture());
-
- doNothing().when(mCarUserManager).addListener(any(), any(),
- mUserLifecycleListenerArgumentCaptor.capture());
-
- mLeash = new SurfaceControl.Builder(null)
- .setName("test")
- .build();
-
- doAnswer((InvocationOnMock invocationOnMock) -> {
- SyncTransactionQueue.TransactionRunnable r =
- invocationOnMock.getArgument(0);
- r.runWithTransaction(new SurfaceControl.Transaction());
- return null;
- }).when(mSyncQueue).runInSync(any());
-
- doAnswer((InvocationOnMock invocationOnMock) -> {
- Runnable r = invocationOnMock.getArgument(0);
- r.run();
- return null;
- }).when(mShellExecutor).execute(any());
- doReturn(mShellExecutor).when(mOrganizer).getExecutor();
-
- mSpyActivityTaskManager = spy(ActivityTaskManager.getInstance());
- ExtendedMockito.doReturn(mSpyActivityTaskManager).when(() ->
- ActivityTaskManager.getInstance());
-
- ActivityScenario<TestActivity> scenario = mActivityRule.getScenario();
- scenario.onActivity(activity -> mActivity = activity);
- }
-
- @After
- public void tearDown() throws InterruptedException {
- mActivity.finishCompletely();
- }
-
- private TaskAppearedInfo createMultiWindowTask(int taskId) {
- ActivityManager.RunningTaskInfo taskInfo =
- new ActivityManager.RunningTaskInfo();
- taskInfo.taskId = taskId;
- taskInfo.configuration.windowConfiguration.setWindowingMode(
- WINDOWING_MODE_MULTI_WINDOW);
- taskInfo.parentTaskId = INVALID_TASK_ID;
- taskInfo.token = mock(WindowContainerToken.class);
- taskInfo.isVisible = true;
- return new TaskAppearedInfo(taskInfo, new SurfaceControl());
- }
-
- private TaskAppearedInfo createMultiWindowTask(int taskId, IBinder token) {
- TaskAppearedInfo taskInfo = createMultiWindowTask(taskId);
- when(taskInfo.getTaskInfo().token.asBinder()).thenReturn(token);
- return taskInfo;
- }
-
- @Test
- public void init_cleansUpExistingMultiWindowTasks() {
- TaskAppearedInfo existingTask1 = createMultiWindowTask(/* taskId= */ 1);
- TaskAppearedInfo existingTask2 = createMultiWindowTask(/* taskId= */ 2);
- doReturn(ImmutableList.of(existingTask1, existingTask2))
- .when(mOrganizer).registerOrganizer();
- ExtendedMockito.doReturn(false).when(mSpyActivityTaskManager).removeTask(anyInt());
-
- createTaskViewManager();
-
- verify(mSpyActivityTaskManager).removeTask(eq(1));
- verify(mSpyActivityTaskManager).removeTask(eq(2));
- }
-
- @Test
- public void testCreateControlledTaskView() throws Exception {
- TaskViewManager taskViewManager = createTaskViewManager();
-
- Intent activityIntent = new Intent();
- Set<String> packagesThatCanRestart = ImmutableSet.of("com.random.package");
- ControlledCarTaskViewCallbacks controlledCarTaskViewCallbacks = mock(
- ControlledCarTaskViewCallbacks.class);
- when(controlledCarTaskViewCallbacks.getDependingPackageNames())
- .thenReturn(packagesThatCanRestart);
-
- taskViewManager.createControlledCarTaskView(
- mActivity.getMainExecutor(),
- ControlledCarTaskViewConfig.builder()
- .setActivityIntent(activityIntent)
- .setAutoRestartOnCrash(false)
- .build(),
- controlledCarTaskViewCallbacks
- );
-
- runOnMainAndWait(() -> {});
- verify(controlledCarTaskViewCallbacks).onTaskViewCreated(any());
- verifyZeroInteractions(mTaskViewInputInterceptor);
- }
-
- @Test
- public void testCreateControlledTaskView_initializesInterceptor_whenCapturingEvents() throws
- Exception {
- TaskViewManager taskViewManager = createTaskViewManager();
-
- Intent activityIntent = new Intent();
- Set<String> packagesThatCanRestart = ImmutableSet.of("com.random.package");
- ControlledCarTaskViewCallbacks controlledCarTaskViewCallbacks = mock(
- ControlledCarTaskViewCallbacks.class);
- when(controlledCarTaskViewCallbacks.getDependingPackageNames())
- .thenReturn(packagesThatCanRestart);
-
- taskViewManager.createControlledCarTaskView(
- mActivity.getMainExecutor(),
- ControlledCarTaskViewConfig.builder()
- .setActivityIntent(activityIntent)
- .setAutoRestartOnCrash(false)
- .setCaptureLongPress(true)
- .build(),
- controlledCarTaskViewCallbacks
- );
-
- runOnMainAndWait(() -> {});
- verify(mTaskViewInputInterceptor).init();
- }
-
- @Test
- public void testCreateControlledTaskView_callsOnReadyWhenVisible() throws Exception {
- TaskViewManager taskViewManager = createTaskViewManager();
- Intent activityIntent = new Intent("ACTION_VIEW");
- Set<String> packagesThatCanRestart = ImmutableSet.of("com.random.package");
- ControlledCarTaskViewCallbacks controlledCarTaskViewCallbacks = mock(
- ControlledCarTaskViewCallbacks.class);
- when(controlledCarTaskViewCallbacks.getDependingPackageNames())
- .thenReturn(packagesThatCanRestart);
- taskViewManager.createControlledCarTaskView(
- mActivity.getMainExecutor(),
- ControlledCarTaskViewConfig.builder()
- .setActivityIntent(activityIntent)
- .setAutoRestartOnCrash(false)
- .build(),
- controlledCarTaskViewCallbacks
- );
- ControlledCarTaskView taskView = spy(taskViewManager.getControlledTaskViews().get(0));
- doNothing().when(taskView).startActivity();
-
- taskView.surfaceCreated(mock(SurfaceHolder.class));
-
- runOnMainAndWait(() -> {});
- verify(controlledCarTaskViewCallbacks).onTaskViewCreated(any());
- verify(controlledCarTaskViewCallbacks).onTaskViewReady();
- }
-
- @Test
- public void testCreateLaunchRootTaskView() throws Exception {
- LaunchRootCarTaskViewCallbacks taskViewCallbacks =
- mock(LaunchRootCarTaskViewCallbacks.class);
- TaskViewManager taskViewManager = createTaskViewManager();
-
- taskViewManager.createLaunchRootTaskView(
- mActivity.getMainExecutor(),
- taskViewCallbacks
- );
- runOnMainAndWait(() -> {});
- TaskView taskView = taskViewManager.getLaunchRootCarTaskView();
- taskView.surfaceCreated(mock(SurfaceHolder.class));
-
- runOnMainAndWait(() -> {});
- verify(taskViewCallbacks).onTaskViewCreated(any());
- verify(mOrganizer).createRootTask(eq(DEFAULT_DISPLAY),
- eq(WINDOWING_MODE_MULTI_WINDOW),
- any(ShellTaskOrganizer.TaskListener.class),
- /* removeWithTaskOrganizer= */ eq(true));
- }
-
- @Test
- public void testCreateLaunchRootTaskView_callsOnReadyWhenVisible() throws Exception {
- TaskAppearedInfo fakeLaunchRootTaskInfo = createMultiWindowTask(1);
- LaunchRootCarTaskViewCallbacks taskViewCallbacks =
- mock(LaunchRootCarTaskViewCallbacks.class);
- TaskViewManager taskViewManager = createTaskViewManager();
- doAnswer(invocation -> {
- ShellTaskOrganizer.TaskListener listener = invocation.getArgument(2);
- listener.onTaskAppeared(fakeLaunchRootTaskInfo.getTaskInfo(), mLeash);
- return null;
- }).when(mOrganizer).createRootTask(eq(DEFAULT_DISPLAY),
- eq(WINDOWING_MODE_MULTI_WINDOW),
- any(ShellTaskOrganizer.TaskListener.class),
- /* removeWithTaskOrganizer= */ eq(true));
-
- taskViewManager.createLaunchRootTaskView(
- mActivity.getMainExecutor(),
- taskViewCallbacks
- );
- runOnMainAndWait(() -> {});
- TaskView taskView = taskViewManager.getLaunchRootCarTaskView();
- taskView.surfaceCreated(mock(SurfaceHolder.class));
-
- runOnMainAndWait(() -> {});
- verify(taskViewCallbacks).onTaskViewCreated(any());
- verify(taskViewCallbacks).onTaskViewReady();
- ArgumentCaptor<WindowContainerTransaction> wctCaptor = ArgumentCaptor.forClass(
- WindowContainerTransaction.class);
- verify(mSyncQueue, atLeastOnce()).queue(wctCaptor.capture());
- List<WindowContainerTransaction> wcts = wctCaptor.getAllValues();
- assertWithMessage("There must be a WindowContainerTransaction to set the"
- + " root task as the launch root.")
- .that(wcts.stream()
- .flatMap(wct -> wct.getHierarchyOps().stream())
- .map(WindowContainerTransaction.HierarchyOp::getType)
- .anyMatch(type -> type == HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT))
- .isTrue();
- }
-
- @Test
- public void testLaunchRootTaskView_onBackPressed_removesTopTask() throws Exception {
- IBinder task1Token = new Binder();
- IBinder task2Token = new Binder();
- IBinder task3Token = new Binder();
- ActivityManager.RunningTaskInfo task1 = createMultiWindowTask(1, task1Token).getTaskInfo();
- ActivityManager.RunningTaskInfo task2 = createMultiWindowTask(2, task2Token).getTaskInfo();
- ActivityManager.RunningTaskInfo task3 = createMultiWindowTask(3, task3Token).getTaskInfo();
- TaskViewManager taskViewManager = createTaskViewManager();
- runOnMainAndWait(() -> {});
- mCarServiceLifecycleListener.onLifecycleChanged(mCar, true);
- runOnMainAndWait(() -> {});
- // Set up a LaunchRootTaskView
- AtomicReference<ShellTaskOrganizer.TaskListener> rootTaskListener = new AtomicReference<>();
- ActivityManager.RunningTaskInfo launchRootTask =
- setUpLaunchRootTaskView(taskViewManager, rootTaskListener, /* rootTaskId = */ 100);
- runOnMainAndWait(() -> {});
- // Trigger a taskAppeared on the launch root task to mimic the task appearance.
- rootTaskListener.get().onTaskAppeared(task1, mLeash);
- rootTaskListener.get().onTaskAppeared(task2, mLeash);
- rootTaskListener.get().onTaskAppeared(task3, mLeash);
- rootTaskListener.get().onTaskInfoChanged(task1);
- // The resultant stack top to bottom is task1, task3, task2
- runOnMainAndWait(() -> {});
-
- // Act
- // Press back button 3 times, trigger corresponding task vanishing as well. In real
- // scenario, removeTask() will trigger onTaskVanished.
- rootTaskListener.get().onBackPressedOnTaskRoot(launchRootTask);
- rootTaskListener.get().onTaskVanished(task1);
- rootTaskListener.get().onBackPressedOnTaskRoot(launchRootTask);
- rootTaskListener.get().onTaskVanished(task3);
- rootTaskListener.get().onBackPressedOnTaskRoot(launchRootTask);
-
- // Assert
- ArgumentCaptor<WindowContainerTransaction> wctCaptor = ArgumentCaptor.forClass(
- WindowContainerTransaction.class);
- verify(mSyncQueue, atLeastOnce()).queue(wctCaptor.capture());
- List<WindowContainerTransaction> wcts = wctCaptor.getAllValues();
- List<WindowContainerTransaction.HierarchyOp> removeTaskOps =
- wcts.stream().flatMap(wct -> wct.getHierarchyOps().stream())
- .filter(op -> op.getType() == HIERARCHY_OP_TYPE_REMOVE_TASK)
- .collect(Collectors.toList());
- assertWithMessage("There must be a WindowContainerTransaction to remove"
- + " 2 of the 3 tasks.")
- .that(removeTaskOps.size())
- .isEqualTo(2);
- assertThat(removeTaskOps.get(0).getContainer()).isEqualTo(task1Token);
- assertThat(removeTaskOps.get(1).getContainer()).isEqualTo(task3Token);
- assertThat(taskViewManager.getRootTaskCount()).isEqualTo(1);
- assertThat(taskViewManager.getTopTaskInLaunchRootTask().taskId).isEqualTo(2);
- }
-
- @Test
- public void testCreateSemiControlledTaskView() throws Exception {
- SemiControlledCarTaskViewCallbacks taskViewCallbacks =
- mock(SemiControlledCarTaskViewCallbacks.class);
- TaskViewManager taskViewManager = createTaskViewManager();
-
- taskViewManager.createSemiControlledTaskView(
- mActivity.getMainExecutor(),
- List.of(),
- taskViewCallbacks
- );
- runOnMainAndWait(() -> {});
- TaskView taskView = taskViewManager.getSemiControlledTaskViews().get(0);
- taskView.surfaceCreated(mock(SurfaceHolder.class));
-
- runOnMainAndWait(() -> {});
- verify(taskViewCallbacks).onTaskViewCreated(any());
- verify(mOrganizer).createRootTask(eq(DEFAULT_DISPLAY),
- eq(WINDOWING_MODE_MULTI_WINDOW),
- any(ShellTaskOrganizer.TaskListener.class),
- /* removeWithTaskOrganizer= */ eq(true));
- }
-
- @Test
- public void testSemiControlledTaskView_callsOnReady() throws Exception {
- List<ComponentName> persistentActivities =
- List.of(ComponentName.unflattenFromString("com.example/.MainActivity"));
- SemiControlledCarTaskViewCallbacks mockCallbacks = mock(
- SemiControlledCarTaskViewCallbacks.class);
- TaskViewManager taskViewManager = createTaskViewManager();
- runOnMainAndWait(() -> {});
- mCarServiceLifecycleListener.onLifecycleChanged(mCar, true);
- // Set up a SemiControlledCarTaskView
- AtomicReference<ShellTaskOrganizer.TaskListener> rootTaskListener = new AtomicReference<>();
-
- // Act
- setUpSemiControlledTaskView(taskViewManager,
- rootTaskListener, /* rootTaskId = */ 1, persistentActivities, mockCallbacks);
- runOnMainAndWait(() -> {});
-
- // Assert
- verify(mockCallbacks).onTaskViewReady();
- verify(mCarActivityManager).setPersistentActivitiesOnRootTask(eq(persistentActivities),
- any());
- }
-
- @Test
- public void testTaskAppeared_semiControlledTaskView_topTaskUpdated() throws Exception {
- SemiControlledCarTaskViewCallbacks mockCallbacks = mock(
- SemiControlledCarTaskViewCallbacks.class);
- TaskViewManager taskViewManager = createTaskViewManager();
- runOnMainAndWait(() -> {});
- mCarServiceLifecycleListener.onLifecycleChanged(mCar, true);
- // Set up a SemiControlledCarTaskView
- AtomicReference<ShellTaskOrganizer.TaskListener> rootTaskListener = new AtomicReference<>();
- SemiControlledCarTaskView taskView = setUpSemiControlledTaskView(taskViewManager,
- rootTaskListener, /* rootTaskId = */ 1, List.of(), mockCallbacks);
- ActivityManager.RunningTaskInfo childTaskInfo =
- createMultiWindowTask(2).getTaskInfo();
-
- // Act
- rootTaskListener.get().onTaskAppeared(childTaskInfo, mLeash);
- runOnMainAndWait(() -> {});
-
- // Assert
- assertThat(taskView.getTopTaskInTheRootTask()).isEqualTo(childTaskInfo);
- }
-
- @Test
- public void testAddAllowListedActivities() throws Exception {
- ComponentName componentName1 = ComponentName.unflattenFromString("com.example/.Activity1");
- ComponentName componentName2 = ComponentName.unflattenFromString("com.example/.Activity2");
- List<ComponentName> persistentActivities =
- List.of(componentName1);
- SemiControlledCarTaskViewCallbacks mockCallbacks = mock(
- SemiControlledCarTaskViewCallbacks.class);
- TaskViewManager taskViewManager = createTaskViewManager();
- runOnMainAndWait(() -> {});
- mCarServiceLifecycleListener.onLifecycleChanged(mCar, true);
- // Set up a SemiControlledCarTaskView
- AtomicReference<ShellTaskOrganizer.TaskListener> rootTaskListener = new AtomicReference<>();
- SemiControlledCarTaskView taskView = setUpSemiControlledTaskView(taskViewManager,
- rootTaskListener, /* rootTaskId = */ 1, persistentActivities, mockCallbacks);
- runOnMainAndWait(() -> {});
-
- // Action
- List<ComponentName> activities = new ArrayList<>(persistentActivities);
- activities.add(componentName2);
- taskViewManager.addAllowListedActivities(taskView, activities);
-
- // Assert
- verify(mCarActivityManager).setPersistentActivitiesOnRootTask(eq(List.of(componentName2)),
- any());
- }
-
- @Test
- public void testRemoveAllowListedActivities() throws Exception {
- List<ComponentName> activities =
- List.of(ComponentName.unflattenFromString("com.example/.MainActivity"));
- SemiControlledCarTaskViewCallbacks mockCallbacks = mock(
- SemiControlledCarTaskViewCallbacks.class);
- TaskViewManager taskViewManager = createTaskViewManager();
- runOnMainAndWait(() -> {});
- mCarServiceLifecycleListener.onLifecycleChanged(mCar, true);
- // Set up a SemiControlledCarTaskView
- AtomicReference<ShellTaskOrganizer.TaskListener> rootTaskListener = new AtomicReference<>();
- SemiControlledCarTaskView taskView = setUpSemiControlledTaskView(taskViewManager,
- rootTaskListener, /* rootTaskId = */ 1, activities, mockCallbacks);
- verify(mCarActivityManager).setPersistentActivitiesOnRootTask(eq(activities), any());
-
- // Action
- taskViewManager.removeAllowListedActivities(taskView, activities);
-
- // Assert
- verify(mCarActivityManager).setPersistentActivitiesOnRootTask(eq(activities), eq(null));
- assertThat(taskView.getPersistentActivities().size()).isEqualTo(0);
- }
-
- @Test
- public void testSetAllowListedActivities() throws Exception {
- ComponentName componentName1 = ComponentName.unflattenFromString("com.example/.Activity1");
- ComponentName componentName2 = ComponentName.unflattenFromString("com.example/.Activity2");
- ComponentName componentName3 = ComponentName.unflattenFromString("com.example/.Activity3");
- List<ComponentName> activities1 =
- List.of(componentName1, componentName2);
- List<ComponentName> activities2 =
- List.of(componentName2, componentName3);
-
- SemiControlledCarTaskViewCallbacks mockCallbacks = mock(
- SemiControlledCarTaskViewCallbacks.class);
- TaskViewManager taskViewManager = createTaskViewManager();
- runOnMainAndWait(() -> {
- });
- mCarServiceLifecycleListener.onLifecycleChanged(mCar, true);
- // Set up a SemiControlledCarTaskView
- AtomicReference<ShellTaskOrganizer.TaskListener> rootTaskListener = new AtomicReference<>();
- SemiControlledCarTaskView taskView = setUpSemiControlledTaskView(taskViewManager,
- rootTaskListener, /* rootTaskId = */ 1, activities1, mockCallbacks);
- runOnMainAndWait(() -> {
- });
- verify(mCarActivityManager).setPersistentActivitiesOnRootTask(eq(activities1), notNull());
- assertThat(taskView.getPersistentActivities()).isEqualTo(activities1);
-
- // Action
- taskViewManager.setAllowListedActivities(taskView, activities2);
- runOnMainAndWait(() -> {
- });
-
- // Assert
- verify(mCarActivityManager).setPersistentActivitiesOnRootTask(eq(activities1), eq(null));
- verify(mCarActivityManager, atLeastOnce()).setPersistentActivitiesOnRootTask(
- eq(activities2), notNull());
- assertThat(taskView.getPersistentActivities()).isEqualTo(activities2);
- }
-
- @Test
- public void testTaskInfoChanged_semiControlledTaskView_topTaskUpdated() throws Exception {
- TaskViewManager taskViewManager = createTaskViewManager();
- runOnMainAndWait(() -> {});
- mCarServiceLifecycleListener.onLifecycleChanged(mCar, true);
- // Set up a SemiControlledCarTaskView
- AtomicReference<ShellTaskOrganizer.TaskListener> rootTaskListener = new AtomicReference<>();
- SemiControlledCarTaskView taskView = setUpSemiControlledTaskView(taskViewManager,
- rootTaskListener, /* rootTaskId = */ 1, List.of(), mock(
- SemiControlledCarTaskViewCallbacks.class));
- ActivityManager.RunningTaskInfo childTaskInfo =
- createMultiWindowTask(1).getTaskInfo();
- rootTaskListener.get().onTaskAppeared(childTaskInfo, mLeash);
- runOnMainAndWait(() -> {});
- ActivityManager.RunningTaskInfo childTaskInfo2 =
- createMultiWindowTask(2).getTaskInfo();
- rootTaskListener.get().onTaskAppeared(childTaskInfo2, mLeash);
- runOnMainAndWait(() -> {});
-
- // Act
- rootTaskListener.get().onTaskInfoChanged(childTaskInfo);
- runOnMainAndWait(() -> {});
-
- // Assert
- assertThat(taskView.getTopTaskInTheRootTask()).isEqualTo(childTaskInfo);
- }
-
- @Test
- public void testTaskVanished_semiControlledTaskView_topTaskUpdated() throws Exception {
- TaskViewManager taskViewManager = createTaskViewManager();
- runOnMainAndWait(() -> {});
- mCarServiceLifecycleListener.onLifecycleChanged(mCar, true);
- // Set up a SemiControlledCarTaskView
- AtomicReference<ShellTaskOrganizer.TaskListener> rootTaskListener = new AtomicReference<>();
- SemiControlledCarTaskView taskView = setUpSemiControlledTaskView(taskViewManager,
- rootTaskListener, /* rootTaskId = */ 1, List.of(),
- mock(SemiControlledCarTaskViewCallbacks.class));
- ActivityManager.RunningTaskInfo childTaskInfo =
- createMultiWindowTask(1).getTaskInfo();
- rootTaskListener.get().onTaskAppeared(childTaskInfo, mLeash);
- runOnMainAndWait(() -> {});
- ActivityManager.RunningTaskInfo childTaskInfo2 =
- createMultiWindowTask(2).getTaskInfo();
- rootTaskListener.get().onTaskAppeared(childTaskInfo2, mLeash);
- runOnMainAndWait(() -> {});
-
- // Act
- rootTaskListener.get().onTaskVanished(childTaskInfo2);
- runOnMainAndWait(() -> {});
-
- // Assert
- assertThat(taskView.getTopTaskInTheRootTask()).isEqualTo(childTaskInfo);
- }
-
-
- private ActivityManager.RunningTaskInfo setUpLaunchRootTaskView(TaskViewManager taskViewManager,
- AtomicReference<ShellTaskOrganizer.TaskListener> listener,
- int rootTaskId) throws Exception {
- ActivityManager.RunningTaskInfo launchRootTaskInfo =
- createMultiWindowTask(rootTaskId).getTaskInfo();
- doAnswer(invocation -> {
- listener.set(invocation.getArgument(2));
- listener.get().onTaskAppeared(launchRootTaskInfo, mLeash);
- return null;
- }).when(mOrganizer).createRootTask(eq(DEFAULT_DISPLAY),
- eq(WINDOWING_MODE_MULTI_WINDOW),
- any(ShellTaskOrganizer.TaskListener.class),
- /* removeWithTaskOrganizer= */ eq(true));
- taskViewManager.createLaunchRootTaskView(
- mActivity.getMainExecutor(),
- mock(LaunchRootCarTaskViewCallbacks.class)
- );
- runOnMainAndWait(() -> {});
- LaunchRootCarTaskView launchRootCarTaskView = taskViewManager.getLaunchRootCarTaskView();
- launchRootCarTaskView.surfaceCreated(mock(SurfaceHolder.class));
- runOnMainAndWait(() -> {});
- return launchRootTaskInfo;
- }
-
- private SemiControlledCarTaskView setUpSemiControlledTaskView(
- TaskViewManager taskViewManager,
- AtomicReference<ShellTaskOrganizer.TaskListener> listener, int rootTaskId,
- List<ComponentName> persistentActivities,
- SemiControlledCarTaskViewCallbacks callbacks)
- throws Exception {
- IBinder taskToken = new Binder();
- ActivityManager.RunningTaskInfo rootTaskInfo =
- createMultiWindowTask(rootTaskId, taskToken).getTaskInfo();
- doAnswer(invocation -> {
- listener.set(invocation.getArgument(2));
- listener.get().onTaskAppeared(rootTaskInfo, mLeash);
- return null;
- }).when(mOrganizer).createRootTask(eq(DEFAULT_DISPLAY),
- eq(WINDOWING_MODE_MULTI_WINDOW),
- any(ShellTaskOrganizer.TaskListener.class),
- /* removeWithTaskOrganizer= */ eq(true));
- taskViewManager.createSemiControlledTaskView(
- mActivity.getMainExecutor(),
- persistentActivities,
- callbacks
- );
- runOnMainAndWait(() -> {});
- SemiControlledCarTaskView semiControlledCarTaskView =
- taskViewManager.getSemiControlledTaskViews().get(0);
- semiControlledCarTaskView.surfaceCreated(mock(SurfaceHolder.class));
- runOnMainAndWait(() -> {});
- return semiControlledCarTaskView;
- }
-
- @Test
- public void testInit_registersTaskMonitor() throws Exception {
- createTaskViewManager();
- runOnMainAndWait(() -> {});
-
- mCarServiceLifecycleListener.onLifecycleChanged(mCar, true);
-
- verify(mCarActivityManager).registerTaskMonitor();
- }
-
- @Test
- public void testTaskAppeared_launchRootTaskView_updatesCarActivityManager() throws Exception {
- TaskViewManager taskViewManager = createTaskViewManager();
- runOnMainAndWait(() -> {});
- mCarServiceLifecycleListener.onLifecycleChanged(mCar, true);
- // Set up a LaunchRootTaskView
- AtomicReference<ShellTaskOrganizer.TaskListener> rootTaskListener = new AtomicReference<>();
- setUpLaunchRootTaskView(taskViewManager, rootTaskListener, /* rootTaskId = */ 1);
- ActivityManager.RunningTaskInfo taskInfo = createMultiWindowTask(2).getTaskInfo();
-
- // Act
- rootTaskListener.get().onTaskAppeared(taskInfo, mLeash);
- runOnMainAndWait(() -> {});
-
- // Assert
- verify(mCarActivityManager).onTaskAppeared(taskInfo);
- }
-
- @Test
- public void testTaskInfoChanged_launchRootTaskView_updatesCarActivityManager()
- throws Exception {
- TaskViewManager taskViewManager = createTaskViewManager();
- runOnMainAndWait(() -> {});
- mCarServiceLifecycleListener.onLifecycleChanged(mCar, true);
- // Set up a LaunchRootTaskView
- AtomicReference<ShellTaskOrganizer.TaskListener> rootTaskListener = new AtomicReference<>();
- setUpLaunchRootTaskView(taskViewManager, rootTaskListener, /* rootTaskId = */ 1);
- ActivityManager.RunningTaskInfo taskInfo = createMultiWindowTask(2).getTaskInfo();
-
- // Act
- rootTaskListener.get().onTaskInfoChanged(taskInfo);
- runOnMainAndWait(() -> {});
-
- // Assert
- verify(mCarActivityManager).onTaskInfoChanged(taskInfo);
- }
-
- @Test
- public void testTaskVanished_launchRootTaskView_updatesCarActivityManager() throws Exception {
- TaskViewManager taskViewManager = createTaskViewManager();
- runOnMainAndWait(() -> {});
- mCarServiceLifecycleListener.onLifecycleChanged(mCar, true);
- // Set up a LaunchRootTaskView
- AtomicReference<ShellTaskOrganizer.TaskListener> rootTaskListener = new AtomicReference<>();
- setUpLaunchRootTaskView(taskViewManager, rootTaskListener, /* rootTaskId = */ 1);
- ActivityManager.RunningTaskInfo taskInfo = createMultiWindowTask(2).getTaskInfo();
-
- // Act
- rootTaskListener.get().onTaskVanished(taskInfo);
- runOnMainAndWait(() -> {});
-
- // Assert
- verify(mCarActivityManager).onTaskVanished(taskInfo);
- }
-
- @Test
- public void testHostActivityDestroyed_releasesAllTaskViews() throws Exception {
- testReleaseAllTaskViews(() -> {
- ActivityScenario<TestActivity> scenario = mActivityRule.getScenario();
- scenario.moveToState(Lifecycle.State.DESTROYED);
- });
- }
-
- private void setUpControlledTaskView(TaskViewManager taskViewManager, Intent activityIntent,
- Set<String> packagesThatCanRestart) throws Exception {
- ControlledCarTaskViewCallbacks controlledCarTaskViewCallbacks = mock(
- ControlledCarTaskViewCallbacks.class);
- when(controlledCarTaskViewCallbacks.getDependingPackageNames())
- .thenReturn(packagesThatCanRestart);
- taskViewManager.createControlledCarTaskView(
- mActivity.getMainExecutor(),
- ControlledCarTaskViewConfig.builder()
- .setActivityIntent(activityIntent)
- .setAutoRestartOnCrash(false)
- .build(),
- controlledCarTaskViewCallbacks
- );
-
- int lastIndex = Math.min(0, taskViewManager.getControlledTaskViews().size() - 1);
- ControlledCarTaskView taskView = spy(taskViewManager.getControlledTaskViews()
- .get(lastIndex));
- doNothing().when(taskView).startActivity();
-
- taskView.surfaceCreated(mock(SurfaceHolder.class));
- runOnMainAndWait(() -> {});
- }
-
- @Test
- public void testRestartControlledTask_whenHostActivityFocussed() throws Exception {
- TaskViewManager taskViewManager = createTaskViewManager();
- mCarServiceLifecycleListener.onLifecycleChanged(mCar, true);
-
- // Send onTaskVanished to mimic the task removal behavior.
- setUpControlledTaskView(taskViewManager, new Intent("ACTION_VIEW"),
- ImmutableSet.of("com.random.package2"));
- ControlledCarTaskView controlledCarTaskView =
- taskViewManager.getControlledTaskViews().get(0);
- ActivityManager.RunningTaskInfo taskInfo = createMultiWindowTask(2).getTaskInfo();
- controlledCarTaskView.dispatchTaskAppeared(taskInfo, mLeash);
- controlledCarTaskView.dispatchTaskVanished(taskInfo);
- assertThat(controlledCarTaskView.getTaskId()).isEqualTo(INVALID_TASK_ID);
-
- // Stub the taskview with a spy to assert on startActivity.
- ControlledCarTaskView spiedTaskView = spy(controlledCarTaskView);
- doNothing().when(spiedTaskView).startActivity();
- taskViewManager.getControlledTaskViews().set(0, spiedTaskView);
-
- // Act
- mTaskStackListenerArgumentCaptor.getValue().onTaskFocusChanged(mActivity.getTaskId(),
- /* focused = */ true);
-
- // Assert
- verify(spiedTaskView).startActivity();
- }
-
- @Test
- public void testControlledTaskNotRestarted_ifAlreadyRunning_whenHostActivityFocussed()
- throws Exception {
- TaskViewManager taskViewManager = createTaskViewManager();
- mCarServiceLifecycleListener.onLifecycleChanged(mCar, true);
-
- setUpControlledTaskView(taskViewManager, new Intent("ACTION_VIEW"),
- ImmutableSet.of("com.random.package2"));
- ControlledCarTaskView controlledCarTaskView =
- taskViewManager.getControlledTaskViews().get(0);
- ActivityManager.RunningTaskInfo taskInfo = createMultiWindowTask(2).getTaskInfo();
- controlledCarTaskView.dispatchTaskAppeared(taskInfo, mLeash);
-
- // Stub the taskview with a spy to assert on startActivity.
- ControlledCarTaskView spiedTaskView = spy(controlledCarTaskView);
- doNothing().when(spiedTaskView).startActivity();
- taskViewManager.getControlledTaskViews().set(0, spiedTaskView);
-
- // Act
- mTaskStackListenerArgumentCaptor.getValue().onTaskFocusChanged(mActivity.getTaskId(),
- /* focused = */ true);
-
- // Assert
- verify(spiedTaskView, times(0)).startActivity();
- }
-
- @Test
- public void testRestartControlledTask_whenHostActivityRestarted() throws Exception {
- TaskViewManager taskViewManager = createTaskViewManager();
- mCarServiceLifecycleListener.onLifecycleChanged(mCar, true);
-
- // Send onTaskVanished to mimic the task removal behavior.
- setUpControlledTaskView(taskViewManager, new Intent("ACTION_VIEW"),
- ImmutableSet.of("com.random.package2"));
- ControlledCarTaskView controlledCarTaskView =
- taskViewManager.getControlledTaskViews().get(0);
- ActivityManager.RunningTaskInfo taskInfo = createMultiWindowTask(2).getTaskInfo();
- controlledCarTaskView.dispatchTaskAppeared(taskInfo, mLeash);
- controlledCarTaskView.dispatchTaskVanished(taskInfo);
- assertThat(controlledCarTaskView.getTaskId()).isEqualTo(INVALID_TASK_ID);
-
- // Stub the taskview with a spy to assert on startActivity.
- ControlledCarTaskView spiedTaskView = spy(controlledCarTaskView);
- doNothing().when(spiedTaskView).startActivity();
- taskViewManager.getControlledTaskViews().set(0, spiedTaskView);
-
- // Act
- mTaskStackListenerArgumentCaptor.getValue().onActivityRestartAttempt(
- createMultiWindowTask(mActivity.getTaskId()).getTaskInfo(),
- /* homeTaskVisible = */ true, false,
- /* focused = */ true);
-
- // Assert
- verify(spiedTaskView).startActivity();
- }
-
- @Test
- public void testRestartControlledTask_whenPackageThatCanRestartChanged() throws Exception {
- TaskViewManager taskViewManager = createTaskViewManager();
- mCarServiceLifecycleListener.onLifecycleChanged(mCar, true);
-
- // Send onTaskVanished to mimic the task removal behavior.
- setUpControlledTaskView(taskViewManager, new Intent("ACTION_VIEW"),
- ImmutableSet.of("com.relevant.package"));
- ControlledCarTaskView controlledCarTaskView =
- taskViewManager.getControlledTaskViews().get(0);
- ActivityManager.RunningTaskInfo taskInfo = createMultiWindowTask(2).getTaskInfo();
- controlledCarTaskView.dispatchTaskAppeared(taskInfo, mLeash);
- controlledCarTaskView.dispatchTaskVanished(taskInfo);
- assertThat(controlledCarTaskView.getTaskId()).isEqualTo(INVALID_TASK_ID);
-
- // Stub the taskview with a spy to assert on startActivity.
- ControlledCarTaskView spiedTaskView = spy(controlledCarTaskView);
- doNothing().when(spiedTaskView).startActivity();
- taskViewManager.getControlledTaskViews().set(0, spiedTaskView);
-
- // Act
- taskViewManager.getPackageBroadcastReceiver().onReceive(mActivity,
- new Intent().setData(Uri.parse("package:com.relevant.package")));
-
- // Assert
- verify(spiedTaskView).startActivity();
- }
-
- @Test
- public void testControlledTaskNotRestarted_whenARandomPackageChanged() throws Exception {
- TaskViewManager taskViewManager = createTaskViewManager();
- mCarServiceLifecycleListener.onLifecycleChanged(mCar, true);
-
- // Send onTaskVanished to mimic the task removal behavior.
- setUpControlledTaskView(taskViewManager, new Intent("ACTION_VIEW"),
- ImmutableSet.of("com.relevant.package"));
- ControlledCarTaskView controlledCarTaskView =
- taskViewManager.getControlledTaskViews().get(0);
- ActivityManager.RunningTaskInfo taskInfo = createMultiWindowTask(2).getTaskInfo();
- controlledCarTaskView.dispatchTaskAppeared(taskInfo, mLeash);
- controlledCarTaskView.dispatchTaskVanished(taskInfo);
- assertThat(controlledCarTaskView.getTaskId()).isEqualTo(INVALID_TASK_ID);
-
- // Stub the taskview with a spy to assert on startActivity.
- ControlledCarTaskView spiedTaskView = spy(controlledCarTaskView);
- doNothing().when(spiedTaskView).startActivity();
- taskViewManager.getControlledTaskViews().set(0, spiedTaskView);
-
- // Act
- taskViewManager.getPackageBroadcastReceiver().onReceive(mActivity,
- new Intent().setData(Uri.parse("package:com.random.package")));
-
- // Assert
- verify(spiedTaskView, times(0)).startActivity();
- }
-
- // User switch related tests.
-
- @Test
- public void testRestartControlledTask_onUserUnlocked() throws Exception {
- TaskViewManager taskViewManager = createTaskViewManager();
- mCarServiceLifecycleListener.onLifecycleChanged(mCar, true);
-
- // Send onTaskVanished to mimic the task removal behavior.
- setUpControlledTaskView(taskViewManager, new Intent("ACTION_VIEW"),
- ImmutableSet.of("com.random.package2"));
- ControlledCarTaskView controlledCarTaskView =
- taskViewManager.getControlledTaskViews().get(0);
- ActivityManager.RunningTaskInfo taskInfo = createMultiWindowTask(2).getTaskInfo();
- controlledCarTaskView.dispatchTaskAppeared(taskInfo, mLeash);
- controlledCarTaskView.dispatchTaskVanished(taskInfo);
- assertThat(controlledCarTaskView.getTaskId()).isEqualTo(INVALID_TASK_ID);
-
- // Stub the taskview with a spy to assert on startActivity.
- ControlledCarTaskView spiedTaskView = spy(controlledCarTaskView);
- doNothing().when(spiedTaskView).startActivity();
- taskViewManager.getControlledTaskViews().set(0, spiedTaskView);
-
- // Act
- mUserLifecycleListenerArgumentCaptor.getValue().onEvent(
- new CarUserManager.UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED,
- mActivity.getUserId()));
-
- // Assert
- verify(spiedTaskView).startActivity();
- }
-
- @Test
- public void testUserSwitch_releasesAllTaskViews() throws Exception {
- testReleaseAllTaskViews(/* actionBlock= */ () ->
- mUserLifecycleListenerArgumentCaptor.getValue().onEvent(
- new CarUserManager.UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_SWITCHING,
- /* from= */ mActivity.getUserId(), /* to= */ 20))
- );
- }
-
- private void testReleaseAllTaskViews(Runnable actionBlock) throws Exception {
- TaskViewManager taskViewManager = createTaskViewManager();
- runOnMainAndWait(() -> {});
- mCarServiceLifecycleListener.onLifecycleChanged(mCar, true);
- // Create a few TaskViews
- AtomicReference<ShellTaskOrganizer.TaskListener> listener = new AtomicReference<>();
- setUpLaunchRootTaskView(taskViewManager, listener, /* rootTaskId = */ 1);
- LaunchRootCarTaskView launchRootCarTaskView = taskViewManager.getLaunchRootCarTaskView();
-
- setUpControlledTaskView(taskViewManager, new Intent("ACTION_VIEW"),
- ImmutableSet.of("com.random.package"));
- ControlledCarTaskView controlledCarTaskView = taskViewManager.getControlledTaskViews()
- .get(0);
- setUpControlledTaskView(taskViewManager, new Intent("ACTION_VIEW"),
- ImmutableSet.of("com.random.package2"));
- ControlledCarTaskView controlledCarTaskView2 = taskViewManager.getControlledTaskViews()
- .get(1);
-
- SemiControlledCarTaskViewCallbacks taskViewCallbacks =
- mock(SemiControlledCarTaskViewCallbacks.class);
- taskViewManager.createSemiControlledTaskView(
- mActivity.getMainExecutor(),
- List.of(),
- taskViewCallbacks
- );
- runOnMainAndWait(() -> {});
- taskViewManager.createSemiControlledTaskView(
- mActivity.getMainExecutor(),
- List.of(),
- taskViewCallbacks
- );
- runOnMainAndWait(() -> {});
- // Trigger surfaceCreated on SemiControlledTaskView.
- SemiControlledCarTaskView semiControlledCarTaskView =
- taskViewManager.getSemiControlledTaskViews().get(0);
- semiControlledCarTaskView.surfaceCreated(mock(SurfaceHolder.class));
- runOnMainAndWait(() -> {});
- SemiControlledCarTaskView semiControlledCarTaskView2 =
- taskViewManager.getSemiControlledTaskViews().get(1);
- semiControlledCarTaskView2.surfaceCreated(mock(SurfaceHolder.class));
- runOnMainAndWait(() -> {});
-
- // Act
- actionBlock.run();
-
- // Assert
- assertThat(launchRootCarTaskView.isInitialized()).isFalse();
- assertThat(controlledCarTaskView.isInitialized()).isFalse();
- assertThat(controlledCarTaskView2.isInitialized()).isFalse();
- assertThat(semiControlledCarTaskView.isInitialized()).isFalse();
- assertThat(semiControlledCarTaskView2.isInitialized()).isFalse();
-
- assertThat(taskViewManager.getSemiControlledTaskViews()).isEmpty();
- assertThat(taskViewManager.getLaunchRootCarTaskView()).isNull();
- assertThat(taskViewManager.getControlledTaskViews()).isEmpty();
-
- verify(mOrganizer).unregisterOrganizer();
- verify(mTaskViewInputInterceptor).release();
- }
-
- private TaskViewManager createTaskViewManager() {
- // InstrumentationTestRunner prepares a looper, but AndroidJUnitRunner does not.
- // http://b/25897652.
- Looper looper = Looper.myLooper();
- if (looper == null) {
- Looper.prepare();
- }
-
- TaskViewManager taskViewManager = new TaskViewManager(mActivity, mShellExecutor,
- mOrganizer, mSyncQueue, mTransitions, new ShellInit(mShellExecutor),
- mShellController, mStartingWindowController);
- taskViewManager.setTaskViewInputInterceptor(mTaskViewInputInterceptor);
- return taskViewManager;
- }
-
- private void runOnMainAndWait(Runnable r) throws Exception {
- mActivity.getMainExecutor().execute(() -> {
- r.run();
- mIdleHandlerLatch.countDown();
- mIdleHandlerLatch = new CountDownLatch(1);
- });
- mIdleHandlerLatch.await(5, TimeUnit.SECONDS);
- }
-
- public static class TestActivity extends Activity {
- private static final int FINISH_TIMEOUT_MS = 1000;
- private final CountDownLatch mDestroyed = new CountDownLatch(1);
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- mDestroyed.countDown();
- }
-
- void finishCompletely() throws InterruptedException {
- finish();
- mDestroyed.await(FINISH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
- }
- }
-}
diff --git a/app/tests/src/com/android/car/carlauncher/calmmode/CalmModeFragmentTest.java b/app/tests/src/com/android/car/carlauncher/calmmode/CalmModeFragmentTest.java
index 6345cc0..d092135 100644
--- a/app/tests/src/com/android/car/carlauncher/calmmode/CalmModeFragmentTest.java
+++ b/app/tests/src/com/android/car/carlauncher/calmmode/CalmModeFragmentTest.java
@@ -21,29 +21,44 @@
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.*;
+import static org.mockito.Mockito.when;
import android.app.Activity;
import androidx.fragment.app.testing.FragmentScenario;
+import androidx.test.espresso.matcher.ViewMatchers;
import com.android.car.carlauncher.R;
+import com.android.car.media.common.MediaItemMetadata;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
public class CalmModeFragmentTest {
private FragmentScenario<CalmModeFragment> mFragmentScenario;
private CalmModeFragment mCalmModeFragment;
private Activity mActivity;
+ @Mock
+ private MediaItemMetadata testMediaItem;
+
+ private static final CharSequence TEST_MEDIA_TITLE = "Title";
+ private static final CharSequence TEST_MEDIA_SUB_TITLE = "Sub Title";
@Before
public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(testMediaItem.getTitle()).thenReturn(TEST_MEDIA_TITLE);
+ when(testMediaItem.getSubtitle()).thenReturn(TEST_MEDIA_SUB_TITLE);
mFragmentScenario =
FragmentScenario.launchInContainer(
CalmModeFragment.class, null, R.style.Theme_CalmMode);
@@ -68,36 +83,62 @@
}
@Test
- public void fragmentResumed_testClock_isVisible() {
+ public void fragmentResumed_testClock_isInvisible() {
mFragmentScenario.moveToState(RESUMED);
- onView(withId(R.id.clock)).check(matches(isDisplayed()));
+ onView(withId(R.id.clock)).check(matches(withEffectiveVisibility(
+ ViewMatchers.Visibility.INVISIBLE)));
}
@Test
- public void fragmentResumed_testDate_isVisible() {
+ public void fragmentResumed_testDate_isInvisible() {
mFragmentScenario.moveToState(RESUMED);
- onView(withId(R.id.date)).check(matches(isDisplayed()));
+ onView(withId(R.id.date)).check(matches(withEffectiveVisibility(
+ ViewMatchers.Visibility.INVISIBLE)));
}
@Test
- public void fragmentResumed_testMedia_isVisible() {
- String testMediaTitle = "Test media title";
+ public void fragmentResumed_testMedia_isInvisible() {
mFragmentScenario.moveToState(RESUMED);
- mActivity.runOnUiThread(()->mCalmModeFragment.updateMediaTitle(testMediaTitle));
+ mActivity.runOnUiThread(()->mCalmModeFragment.updateMediaTitle(testMediaItem));
- onView(withId(R.id.media_title)).check(matches(isDisplayed()));
+ onView(withId(R.id.media_title)).check(matches(withEffectiveVisibility(
+ ViewMatchers.Visibility.INVISIBLE)));
+ String expectedText =
+ TEST_MEDIA_TITLE + " • " + TEST_MEDIA_SUB_TITLE;
+ onView(withId(R.id.media_title)).check(matches(withText(expectedText)));
+ }
+
+ @Test
+ public void fragmentResumed_testMediaItemNull_isGone() {
+ mFragmentScenario.moveToState(RESUMED);
+
+ mActivity.runOnUiThread(()->mCalmModeFragment.updateMediaTitle(null));
+
+ onView(withId(R.id.media_title)).check(matches(not(isDisplayed())));
}
@Test
public void fragmentResumed_testMediaTitleNull_isGone() {
- String testMediaTitle = null;
+ when(testMediaItem.getTitle()).thenReturn(null);
mFragmentScenario.moveToState(RESUMED);
- mActivity.runOnUiThread(()->mCalmModeFragment.updateMediaTitle(testMediaTitle));
+ mActivity.runOnUiThread(()->mCalmModeFragment.updateMediaTitle(testMediaItem));
onView(withId(R.id.media_title)).check(matches(not(isDisplayed())));
}
+
+ @Test
+ public void fragmentResumed_testMediaSubTitleNull_isVisible() {
+ when(testMediaItem.getSubtitle()).thenReturn(null);
+ mFragmentScenario.moveToState(RESUMED);
+
+ mActivity.runOnUiThread(()->mCalmModeFragment.updateMediaTitle(testMediaItem));
+
+ onView(withId(R.id.media_title)).check(matches(withEffectiveVisibility(
+ ViewMatchers.Visibility.INVISIBLE)));
+ onView(withId(R.id.media_title)).check(matches(withText(TEST_MEDIA_TITLE.toString())));
+ }
}
diff --git a/app/tests/src/com/android/car/carlauncher/calmmode/NavigationStateDataTest.java b/app/tests/src/com/android/car/carlauncher/calmmode/NavigationStateDataTest.java
index b614cbe..d7e60bc 100644
--- a/app/tests/src/com/android/car/carlauncher/calmmode/NavigationStateDataTest.java
+++ b/app/tests/src/com/android/car/carlauncher/calmmode/NavigationStateDataTest.java
@@ -39,6 +39,7 @@
@RunWith(AndroidJUnit4.class)
public class NavigationStateDataTest extends AbstractExtendedMockitoTestCase {
+ private static final String SEPARATOR = "\\u0020\\u0020\\u0020\\u2022\\u0020\\u0020\\u0020";
private static final String TIME_TO_DEST = "1hr 30min";
private static final double DISTANCE_TO_DEST = 100;
private static final String DISTANCE_TO_DEST_STR = "100";
@@ -85,14 +86,16 @@
@Test
public void testBuildTripStatusString_nullNavState_returnsNull() {
- assertNull(NavigationStateData.buildTripStatusString(null, LOCALE_US));
+ assertNull(NavigationStateData.buildTripStatusString(null, LOCALE_US,
+ SEPARATOR));
}
@Test
public void testBuildTripStatusString_nullTime_returnsNull() {
when(mNavigationStateDataImperial.getTimeToDestination()).thenReturn(null);
final String tripStatusActual =
- NavigationStateData.buildTripStatusString(mNavigationStateDataImperial, null);
+ NavigationStateData.buildTripStatusString(mNavigationStateDataImperial, null,
+ SEPARATOR);
assertNull(tripStatusActual);
}
@@ -100,25 +103,28 @@
public void testBuildTripStatusString_nullDistanceUnit_returnsNull() {
when(mNavigationStateDataImperial.getDistanceUnit()).thenReturn(null);
final String tripStatusActual =
- NavigationStateData.buildTripStatusString(mNavigationStateDataImperial, LOCALE_US);
+ NavigationStateData.buildTripStatusString(mNavigationStateDataImperial, LOCALE_US,
+ SEPARATOR);
assertNull(tripStatusActual);
}
@Test
public void testBuildTripStatusString_navStateDataImperial_matchesForLocaleUS() {
final String tripStatusExpected =
- TIME_TO_DEST + " " + DISTANCE_TO_DEST_STR + " " + DISTANCE_UNIT_IMPERIAL_STR;
+ TIME_TO_DEST + SEPARATOR + DISTANCE_TO_DEST_STR + " " + DISTANCE_UNIT_IMPERIAL_STR;
final String tripStatusActual =
- NavigationStateData.buildTripStatusString(mNavigationStateDataImperial, LOCALE_US);
+ NavigationStateData.buildTripStatusString(mNavigationStateDataImperial, LOCALE_US,
+ SEPARATOR);
assertEquals(tripStatusExpected, tripStatusActual);
}
@Test
public void testBuildTripStatusString_navStateDataMetric_matchesForLocaleUS() {
final String tripStatusExpected =
- TIME_TO_DEST + " " + DISTANCE_TO_DEST_STR + " " + DISTANCE_UNIT_METRIC_STR;
+ TIME_TO_DEST + SEPARATOR + DISTANCE_TO_DEST_STR + " " + DISTANCE_UNIT_METRIC_STR;
final String tripStatusActual =
- NavigationStateData.buildTripStatusString(mNavigationStateDataMetric, LOCALE_US);
+ NavigationStateData.buildTripStatusString(mNavigationStateDataMetric, LOCALE_US,
+ SEPARATOR);
assertEquals(tripStatusExpected, tripStatusActual);
}
}
diff --git a/app/tests/src/com/android/car/carlauncher/calmmode/NavigationStateViewModelTest.java b/app/tests/src/com/android/car/carlauncher/calmmode/NavigationStateViewModelTest.java
index efa1938..f9549d6 100644
--- a/app/tests/src/com/android/car/carlauncher/calmmode/NavigationStateViewModelTest.java
+++ b/app/tests/src/com/android/car/carlauncher/calmmode/NavigationStateViewModelTest.java
@@ -121,7 +121,7 @@
NavigationState.NavigationStateProto navStateProto =
NavigationState.NavigationStateProto.newBuilder().build();
- mNavigationStateViewModel.onNavigationState(navStateProto.toByteArray());
+ mNavigationStateViewModel.onNavigationStateChanged(navStateProto.toByteArray());
NavigationStateData navStateData =
mNavigationStateViewModel.getNavigationState().getValue();
@@ -130,7 +130,7 @@
@Test
public void onNavigationState_protoExceptionWhileParsing_navStateIsSetToNull() {
- mNavigationStateViewModel.onNavigationState(new byte[] {});
+ mNavigationStateViewModel.onNavigationStateChanged(new byte[] {});
NavigationStateData navStateData =
mNavigationStateViewModel.getNavigationState().getValue();
assertNull(navStateData);
@@ -143,7 +143,7 @@
NavigationState.NavigationStateProto navStateProto =
buildNavStateProto(distanceToDest, NavigationState.Distance.Unit.MILES, timeToDest);
- mNavigationStateViewModel.onNavigationState(navStateProto.toByteArray());
+ mNavigationStateViewModel.onNavigationStateChanged(navStateProto.toByteArray());
NavigationStateData navStateData =
mNavigationStateViewModel.getNavigationState().getValue();
@@ -160,7 +160,7 @@
NavigationState.NavigationStateProto navStateProto =
buildNavStateProto(
distanceToDest, NavigationState.Distance.Unit.KILOMETERS, timeToDest);
- mNavigationStateViewModel.onNavigationState(navStateProto.toByteArray());
+ mNavigationStateViewModel.onNavigationStateChanged(navStateProto.toByteArray());
NavigationStateData navStateData =
mNavigationStateViewModel.getNavigationState().getValue();
@@ -176,7 +176,7 @@
final String distanceToDest = "1000";
NavigationState.NavigationStateProto navStateProto =
buildNavStateProto(distanceToDest, NavigationState.Distance.Unit.FEET, timeToDest);
- mNavigationStateViewModel.onNavigationState(navStateProto.toByteArray());
+ mNavigationStateViewModel.onNavigationStateChanged(navStateProto.toByteArray());
NavigationStateData navStateData =
mNavigationStateViewModel.getNavigationState().getValue();
diff --git a/app/tests/src/com/android/car/carlauncher/calmmode/TemperatureDataTest.java b/app/tests/src/com/android/car/carlauncher/calmmode/TemperatureDataTest.java
index a2b4e6c..00d9fb8 100644
--- a/app/tests/src/com/android/car/carlauncher/calmmode/TemperatureDataTest.java
+++ b/app/tests/src/com/android/car/carlauncher/calmmode/TemperatureDataTest.java
@@ -161,7 +161,20 @@
String tempCExpected = "25°C";
TemperatureData tempC = new TemperatureData.Builder().setValueCelsius(tempCVal).build();
- String tempCActual = TemperatureData.buildTemperatureString(tempC, TEST_LOCALE_US);
+ String tempCActual = TemperatureData.buildTemperatureString(tempC, TEST_LOCALE_US,
+ /* showUnit = */ true);
+
+ assertEquals(tempCExpected, tempCActual);
+ }
+
+ @Test
+ public void testBuildTemperatureString_positiveCelsiusDoNotShowUnit_matchesForLocaleUS() {
+ float tempCVal = 25.12345f;
+ String tempCExpected = "25°";
+ TemperatureData tempC = new TemperatureData.Builder().setValueCelsius(tempCVal).build();
+
+ String tempCActual = TemperatureData.buildTemperatureString(tempC, TEST_LOCALE_US,
+ /* showUnit = */ false);
assertEquals(tempCExpected, tempCActual);
}
@@ -172,7 +185,20 @@
String tempFExpected = "77°F";
TemperatureData tempF = new TemperatureData.Builder().setValueFahrenheit(tempFVal).build();
- String tempFActual = TemperatureData.buildTemperatureString(tempF, TEST_LOCALE_US);
+ String tempFActual = TemperatureData.buildTemperatureString(tempF, TEST_LOCALE_US,
+ /* showUnit = */ true);
+
+ assertEquals(tempFExpected, tempFActual);
+ }
+
+ @Test
+ public void testBuildTemperatureString_positiveFahrenheitDoNotShowUnit_matchesForLocaleUS() {
+ float tempFVal = 77.2212f;
+ String tempFExpected = "77°";
+ TemperatureData tempF = new TemperatureData.Builder().setValueFahrenheit(tempFVal).build();
+
+ String tempFActual = TemperatureData.buildTemperatureString(tempF, TEST_LOCALE_US,
+ /* showUnit = */ false);
assertEquals(tempFExpected, tempFActual);
}
@@ -183,7 +209,20 @@
String tempCExpected = "-20°C";
TemperatureData tempC = new TemperatureData.Builder().setValueCelsius(tempCVal).build();
- String tempCActual = TemperatureData.buildTemperatureString(tempC, TEST_LOCALE_US);
+ String tempCActual = TemperatureData.buildTemperatureString(tempC, TEST_LOCALE_US,
+ /* showUnit = */ true);
+
+ assertEquals(tempCExpected, tempCActual);
+ }
+
+ @Test
+ public void testBuildTemperatureString_negativeCelsiusDoNotShowUnit_matchesForLocaleUS() {
+ float tempCVal = -20f;
+ String tempCExpected = "-20°";
+ TemperatureData tempC = new TemperatureData.Builder().setValueCelsius(tempCVal).build();
+
+ String tempCActual = TemperatureData.buildTemperatureString(tempC, TEST_LOCALE_US,
+ /* showUnit = */ false);
assertEquals(tempCExpected, tempCActual);
}
@@ -194,7 +233,20 @@
String tempFExpected = "-4°F";
TemperatureData tempF = new TemperatureData.Builder().setValueFahrenheit(tempFVal).build();
- String tempFActual = TemperatureData.buildTemperatureString(tempF, TEST_LOCALE_US);
+ String tempFActual = TemperatureData.buildTemperatureString(tempF, TEST_LOCALE_US,
+ /* showUnit = */ true);
+
+ assertEquals(tempFExpected, tempFActual);
+ }
+
+ @Test
+ public void testBuildTemperatureString_negativeFahrenheitDoNotShowUnit_matchesForLocaleUS() {
+ float tempFVal = -4f;
+ String tempFExpected = "-4°";
+ TemperatureData tempF = new TemperatureData.Builder().setValueFahrenheit(tempFVal).build();
+
+ String tempFActual = TemperatureData.buildTemperatureString(tempF, TEST_LOCALE_US,
+ /* showUnit = */ false);
assertEquals(tempFExpected, tempFActual);
}
@@ -205,7 +257,8 @@
String tempCExpected = "0°C";
TemperatureData tempC = new TemperatureData.Builder().setValueCelsius(tempCVal).build();
- String tempCActual = TemperatureData.buildTemperatureString(tempC, TEST_LOCALE_US);
+ String tempCActual = TemperatureData.buildTemperatureString(tempC, TEST_LOCALE_US,
+ /* showUnit = */ true);
assertEquals(tempCExpected, tempCActual);
}
@@ -216,7 +269,8 @@
String tempFExpected = "0°C";
TemperatureData tempF = new TemperatureData.Builder().setValueCelsius(tempFVal).build();
- String tempCActual = TemperatureData.buildTemperatureString(tempF, TEST_LOCALE_US);
+ String tempCActual = TemperatureData.buildTemperatureString(tempF, TEST_LOCALE_US,
+ /* showUnit = */ true);
assertEquals(tempFExpected, tempCActual);
}
diff --git a/app/tests/src/com/android/car/carlauncher/homescreen/HomeCardFragmentTest.java b/app/tests/src/com/android/car/carlauncher/homescreen/HomeCardFragmentTest.java
index 0a1afc9..7866abe 100644
--- a/app/tests/src/com/android/car/carlauncher/homescreen/HomeCardFragmentTest.java
+++ b/app/tests/src/com/android/car/carlauncher/homescreen/HomeCardFragmentTest.java
@@ -18,6 +18,7 @@
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.RootMatchers.hasWindowLayoutParams;
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
@@ -31,25 +32,30 @@
import static org.mockito.Mockito.when;
import android.graphics.drawable.Drawable;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.WindowManager;
import android.widget.ImageButton;
+import androidx.fragment.app.FragmentActivity;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.Suppress;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import com.android.car.carlauncher.CarLauncher;
+import com.android.car.carlauncher.Flags;
import com.android.car.carlauncher.R;
import com.android.car.carlauncher.homescreen.ui.CardHeader;
import com.android.car.carlauncher.homescreen.ui.DescriptiveTextView;
import com.android.car.carlauncher.homescreen.ui.DescriptiveTextWithControlsView;
import com.android.car.carlauncher.homescreen.ui.TextBlockView;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-@Suppress // To be ignored until b/224978827 is fixed
@RunWith(AndroidJUnit4.class)
public class HomeCardFragmentTest {
@@ -71,90 +77,134 @@
private static final TextBlockView TEXT_BLOCK_VIEW_NO_FOOTER = new TextBlockView(
TEXT_BLOCK_CONTENT);
+ private FragmentActivity mActivity;
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule =
+ DeviceFlagsValueProvider.createCheckFlagsRule();
@Rule
public ActivityTestRule<CarLauncher> mActivityTestRule = new ActivityTestRule<CarLauncher>(
CarLauncher.class);
+ @Before
+ public void setUp() {
+ mActivity = mActivityTestRule.getActivity();
+ mActivity.runOnUiThread(new Runnable() {
+ public void run() {
+ mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
+ }
+ });
+ }
+
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_MEDIA_CARD_FULLSCREEN)
public void updateContentView_descriptiveTextWithFooter_displaysTapForMoreView() {
- HomeCardFragment fragment = (HomeCardFragment) mActivityTestRule.getActivity()
- .getSupportFragmentManager().findFragmentById(R.id.top_card);
+ HomeCardFragment fragment = (HomeCardFragment) mActivity.getSupportFragmentManager()
+ .findFragmentById(R.id.top_card);
fragment.updateHeaderView(CARD_HEADER);
fragment.updateContentView(DESCRIPTIVE_TEXT_VIEW);
onView(allOf(withId(R.id.descriptive_text_layout),
- isDescendantOfA(withId(R.id.top_card)))).check(
- matches(isDisplayed()));
+ isDescendantOfA(withId(R.id.top_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
onView(allOf(withId(R.id.primary_text), withText(DESCRIPTIVE_TEXT_TITLE),
isDescendantOfA(withId(R.id.descriptive_text_layout)),
- isDescendantOfA(withId(R.id.top_card)))).check(matches(isDisplayed()));
+ isDescendantOfA(withId(R.id.top_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
onView(allOf(withId(R.id.secondary_text), withText(DESCRIPTIVE_TEXT_SUBTITLE),
isDescendantOfA(withId(R.id.descriptive_text_layout)),
- isDescendantOfA(withId(R.id.top_card)))).check(matches(isDisplayed()));
+ isDescendantOfA(withId(R.id.top_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
onView(allOf(withId(R.id.tap_for_more_text), withText(DESCRIPTIVE_TEXT_FOOTER),
isDescendantOfA(withId(R.id.descriptive_text_layout)),
- isDescendantOfA(withId(R.id.top_card)))).check(matches(isDisplayed()));
+ isDescendantOfA(withId(R.id.top_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_MEDIA_CARD_FULLSCREEN)
public void updateContentView_descriptiveTextWithNoFooter_hidesTapForMoreView() {
- HomeCardFragment fragment = (HomeCardFragment) mActivityTestRule.getActivity()
- .getSupportFragmentManager().findFragmentById(R.id.top_card);
+ HomeCardFragment fragment = (HomeCardFragment) mActivity.getSupportFragmentManager()
+ .findFragmentById(R.id.top_card);
fragment.updateHeaderView(CARD_HEADER);
fragment.updateContentView(DESCRIPTIVE_TEXT_VIEW_NO_FOOTER);
onView(allOf(withId(R.id.descriptive_text_layout),
- isDescendantOfA(withId(R.id.top_card)))).check(
- matches(isDisplayed()));
+ isDescendantOfA(withId(R.id.top_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
onView(allOf(withId(R.id.primary_text), withText(DESCRIPTIVE_TEXT_TITLE),
isDescendantOfA(withId(R.id.descriptive_text_layout)),
- isDescendantOfA(withId(R.id.top_card)))).check(matches(isDisplayed()));
+ isDescendantOfA(withId(R.id.top_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
onView(allOf(withId(R.id.secondary_text), withText(DESCRIPTIVE_TEXT_SUBTITLE),
isDescendantOfA(withId(R.id.descriptive_text_layout)),
- isDescendantOfA(withId(R.id.top_card)))).check(matches(isDisplayed()));
+ isDescendantOfA(withId(R.id.top_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
onView(allOf(withId(R.id.tap_for_more_text),
isDescendantOfA(withId(R.id.descriptive_text_layout)),
- isDescendantOfA(withId(R.id.top_card)))).check(matches(not(isDisplayed())));
+ isDescendantOfA(withId(R.id.top_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_MEDIA_CARD_FULLSCREEN)
public void updateContentView_textBlockWithFooter_displaysTapForMoreView() {
- HomeCardFragment fragment = (HomeCardFragment) mActivityTestRule.getActivity()
- .getSupportFragmentManager().findFragmentById(R.id.top_card);
+ HomeCardFragment fragment = (HomeCardFragment) mActivity.getSupportFragmentManager()
+ .findFragmentById(R.id.top_card);
fragment.updateHeaderView(CARD_HEADER);
fragment.updateContentView(TEXT_BLOCK_VIEW);
- onView(allOf(withId(R.id.text_block_layout), isDescendantOfA(withId(R.id.top_card)))).check(
- matches(isDisplayed()));
+ onView(allOf(withId(R.id.text_block_layout), isDescendantOfA(withId(R.id.top_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
onView(allOf(withId(R.id.text_block), withText(TEXT_BLOCK_CONTENT),
isDescendantOfA(withId(R.id.text_block_layout)),
- isDescendantOfA(withId(R.id.top_card)))).check(matches(isDisplayed()));
+ isDescendantOfA(withId(R.id.top_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
onView(allOf(withId(R.id.tap_for_more_text), withText(TEXT_BLOCK_FOOTER),
isDescendantOfA(withId(R.id.text_block_layout)),
- isDescendantOfA(withId(R.id.top_card)))).check(matches(isDisplayed()));
+ isDescendantOfA(withId(R.id.top_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_MEDIA_CARD_FULLSCREEN)
public void updateContentView_textBlockNoFooter_hidesTapForMoreView() {
- HomeCardFragment fragment = (HomeCardFragment) mActivityTestRule.getActivity()
- .getSupportFragmentManager().findFragmentById(R.id.top_card);
+ HomeCardFragment fragment = (HomeCardFragment) mActivity.getSupportFragmentManager()
+ .findFragmentById(R.id.top_card);
fragment.updateHeaderView(CARD_HEADER);
fragment.updateContentView(TEXT_BLOCK_VIEW_NO_FOOTER);
- onView(allOf(withId(R.id.text_block_layout), isDescendantOfA(withId(R.id.top_card)))).check(
- matches(isDisplayed()));
+ onView(allOf(withId(R.id.text_block_layout), isDescendantOfA(withId(R.id.top_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
onView(allOf(withId(R.id.text_block), withText(TEXT_BLOCK_CONTENT),
isDescendantOfA(withId(R.id.text_block_layout)),
- isDescendantOfA(withId(R.id.top_card)))).check(matches(isDisplayed()));
+ isDescendantOfA(withId(R.id.top_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
onView(allOf(withId(R.id.tap_for_more_text),
isDescendantOfA(withId(R.id.text_block_layout)),
- isDescendantOfA(withId(R.id.top_card)))).check(matches(not(isDisplayed())));
+ isDescendantOfA(withId(R.id.top_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_MEDIA_CARD_FULLSCREEN)
public void updateControlBarButton_updatesButtonSelectedState() {
- HomeCardFragment fragment = (HomeCardFragment) mActivityTestRule.getActivity()
- .getSupportFragmentManager().findFragmentById(R.id.top_card);
+ HomeCardFragment fragment = (HomeCardFragment) mActivity.getSupportFragmentManager()
+ .findFragmentById(R.id.top_card);
assertNotNull(fragment);
ImageButton leftImageButton = mock(ImageButton.class);
diff --git a/app/tests/src/com/android/car/carlauncher/homescreen/audio/AudioCardFragmentTest.java b/app/tests/src/com/android/car/carlauncher/homescreen/audio/AudioCardFragmentTest.java
new file mode 100644
index 0000000..a89e2f9
--- /dev/null
+++ b/app/tests/src/com/android/car/carlauncher/homescreen/audio/AudioCardFragmentTest.java
@@ -0,0 +1,556 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.homescreen.audio;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
+import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+import static androidx.test.espresso.matcher.RootMatchers.hasWindowLayoutParams;
+
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.WindowManager;
+
+import androidx.fragment.app.FragmentActivity;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.car.apps.common.CrossfadeImageView;
+import com.android.car.carlauncher.CarLauncher;
+import com.android.car.carlauncher.Flags;
+import com.android.car.carlauncher.R;
+import com.android.car.carlauncher.homescreen.audio.dialer.DialerCardFragment;
+import com.android.car.carlauncher.homescreen.audio.media.MediaCardFragment;
+import com.android.car.carlauncher.homescreen.ui.CardContent.CardBackgroundImage;
+import com.android.car.carlauncher.homescreen.ui.CardHeader;
+import com.android.car.carlauncher.homescreen.ui.DescriptiveTextView;
+import com.android.car.carlauncher.homescreen.ui.DescriptiveTextWithControlsView;
+import com.android.car.carlauncher.homescreen.ui.TextBlockView;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class AudioCardFragmentTest {
+
+ private static final CardHeader CARD_HEADER = new CardHeader("Test App Name", null);
+ private static final BitmapDrawable BITMAP = new BitmapDrawable(
+ Bitmap.createBitmap(/* width = */100, /* height = */100, Bitmap.Config.ARGB_8888));
+ private static final CardBackgroundImage CARD_BACKGROUND_IMAGE = new CardBackgroundImage(
+ BITMAP, null);
+ private static final String AUDIO_VIEW_TITLE = "Test song title";
+ private static final String AUDIO_VIEW_SUBTITLE = "Test artist name";
+ private static final long AUDIO_START_TIME = 1L;
+ private static final DescriptiveTextView DESCRIPTIVE_TEXT_VIEW = new DescriptiveTextView(
+ /* image = */ null, "Primary Text", "Secondary Text");
+ private static final TextBlockView TEXT_BLOCK_VIEW = new TextBlockView("Text");
+
+ private final DescriptiveTextWithControlsView
+ mDescriptiveTextWithControlsView = new DescriptiveTextWithControlsView(
+ CARD_BACKGROUND_IMAGE,
+ AUDIO_VIEW_TITLE,
+ AUDIO_VIEW_SUBTITLE);
+ private final DescriptiveTextWithControlsView.Control mControl =
+ new DescriptiveTextWithControlsView.Control(BITMAP, v -> {
+ });
+ private final DescriptiveTextWithControlsView
+ mDescriptiveTextWithControlsViewWithButtons = new DescriptiveTextWithControlsView(
+ CARD_BACKGROUND_IMAGE, AUDIO_VIEW_TITLE, AUDIO_VIEW_SUBTITLE, AUDIO_START_TIME,
+ mControl, mControl, mControl);
+
+ private FragmentActivity mActivity;
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule =
+ DeviceFlagsValueProvider.createCheckFlagsRule();
+ @Rule
+ public ActivityTestRule<CarLauncher> mActivityTestRule =
+ new ActivityTestRule<CarLauncher>(CarLauncher.class);
+
+ @Before
+ public void setUp() {
+ mActivity = mActivityTestRule.getActivity();
+ mActivity.runOnUiThread(new Runnable() {
+ public void run() {
+ mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
+ }
+ });
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_MEDIA_CARD_FULLSCREEN)
+ public void updateContentAndHeaderView_noControls_showsMediaPlaybackControlsBar_hidesDialer() {
+ AudioCardFragment fragment = (AudioCardFragment) mActivity.getSupportFragmentManager()
+ .findFragmentById(R.id.bottom_card);
+ mActivity.runOnUiThread(fragment::showMediaCard);
+ MediaCardFragment mediaCardFragment = (MediaCardFragment) fragment.getMediaFragment();
+
+ mediaCardFragment.updateHeaderView(CARD_HEADER);
+ mediaCardFragment.updateContentView(mDescriptiveTextWithControlsView);
+
+ onView(allOf(withId(R.id.card_view),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.card_view),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.card_background),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.card_background),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.card_background_image), is(instanceOf(CrossfadeImageView.class)),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.card_background_image), is(instanceOf(CrossfadeImageView.class)),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.media_layout),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.media_layout),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.primary_text), withText(AUDIO_VIEW_TITLE),
+ isDescendantOfA(withId(R.id.media_layout)),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.secondary_text), withText(AUDIO_VIEW_SUBTITLE),
+ isDescendantOfA(withId(R.id.media_layout)),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.optional_timer),
+ isDescendantOfA(withId(R.id.media_layout)),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.media_playback_controls_bar),
+ isDescendantOfA(withId(R.id.media_layout)),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.descriptive_text_with_controls_layout),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.descriptive_text_with_controls_layout),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.motion_layout),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(doesNotExist());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_MEDIA_CARD_FULLSCREEN)
+ public void showMediaCard_showsFullscreenMediaCardLayout_hidesDialerLayout() {
+ AudioCardFragment fragment = (AudioCardFragment) mActivity.getSupportFragmentManager()
+ .findFragmentById(R.id.bottom_card);
+ mActivity.runOnUiThread(fragment::showMediaCard);
+ MediaCardFragment mediaCardFragment = (MediaCardFragment) fragment.getMediaFragment();
+
+ onView(allOf(withId(R.id.motion_layout),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.card_view),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(doesNotExist());
+ onView(allOf(withId(R.id.card_view),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.card_background),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(doesNotExist());
+ onView(allOf(withId(R.id.card_background),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.card_background_image), is(instanceOf(CrossfadeImageView.class)),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(doesNotExist());
+ onView(allOf(withId(R.id.card_background_image), is(instanceOf(CrossfadeImageView.class)),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.media_layout),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(doesNotExist());
+ onView(allOf(withId(R.id.media_layout),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.descriptive_text_with_controls_layout),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(doesNotExist());
+ onView(allOf(withId(R.id.descriptive_text_with_controls_layout),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_MEDIA_CARD_FULLSCREEN)
+ public void updateContentAndHeaderView_showsDialerControlBarControls_hidesMediaCardControls() {
+ AudioCardFragment fragment = (AudioCardFragment) mActivity.getSupportFragmentManager()
+ .findFragmentById(R.id.bottom_card);
+ mActivity.runOnUiThread(fragment::showInCallCard);
+ DialerCardFragment dialerCardFragment = (DialerCardFragment) fragment.getInCallFragment();
+
+ dialerCardFragment.updateHeaderView(CARD_HEADER);
+ dialerCardFragment.updateContentView(mDescriptiveTextWithControlsViewWithButtons);
+
+ onView(allOf(withId(R.id.optional_timer),
+ isDescendantOfA(withId(R.id.descriptive_text_with_controls_layout)),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.button_left),
+ isDescendantOfA(withId(R.id.descriptive_text_with_controls_layout)),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.button_center),
+ isDescendantOfA(withId(R.id.descriptive_text_with_controls_layout)),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.button_right),
+ isDescendantOfA(withId(R.id.descriptive_text_with_controls_layout)),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.media_layout),
+ isDescendantOfA(withId(R.id.bottom_card)),
+ isDescendantOfA(withId(R.id.in_call_fragment_container))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.media_layout),
+ isDescendantOfA(withId(R.id.bottom_card)),
+ isDescendantOfA(withId(R.id.media_fragment_container))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.descriptive_text_with_controls_layout),
+ isDescendantOfA(withId(R.id.bottom_card)),
+ isDescendantOfA(withId(R.id.in_call_fragment_container))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.descriptive_text_with_controls_layout),
+ isDescendantOfA(withId(R.id.bottom_card)),
+ isDescendantOfA(withId(R.id.media_fragment_container))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_MEDIA_CARD_FULLSCREEN)
+ public void updateContentAndHeaderView_audioContentWithControls_showsDialer_notMediaCard() {
+ AudioCardFragment fragment = (AudioCardFragment) mActivity.getSupportFragmentManager()
+ .findFragmentById(R.id.bottom_card);
+ mActivity.runOnUiThread(fragment::showInCallCard);
+ DialerCardFragment dialerCardFragment = (DialerCardFragment) fragment.getInCallFragment();
+
+ dialerCardFragment.updateHeaderView(CARD_HEADER);
+ dialerCardFragment.updateContentView(mDescriptiveTextWithControlsViewWithButtons);
+
+ onView(allOf(withId(R.id.optional_timer),
+ isDescendantOfA(withId(R.id.descriptive_text_with_controls_layout)),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.button_left),
+ isDescendantOfA(withId(R.id.descriptive_text_with_controls_layout)),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.button_center),
+ isDescendantOfA(withId(R.id.descriptive_text_with_controls_layout)),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.button_right),
+ isDescendantOfA(withId(R.id.descriptive_text_with_controls_layout)),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.card_view),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.card_view),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(doesNotExist());
+ onView(allOf(withId(R.id.card_background),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.card_background),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(doesNotExist());
+ onView(allOf(withId(R.id.card_background_image), is(instanceOf(CrossfadeImageView.class)),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.card_background_image), is(instanceOf(CrossfadeImageView.class)),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(doesNotExist());
+ onView(allOf(withId(R.id.descriptive_text_with_controls_layout),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.descriptive_text_with_controls_layout),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(doesNotExist());
+ onView(allOf(withId(R.id.media_layout),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.media_layout),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(doesNotExist());
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_MEDIA_CARD_FULLSCREEN)
+ public void mediaFragment_updateContentView_descriptiveText_hidesPlaybackControlsBar() {
+ AudioCardFragment fragment = (AudioCardFragment) mActivity.getSupportFragmentManager()
+ .findFragmentById(R.id.bottom_card);
+ MediaCardFragment mediaCardFragment = (MediaCardFragment) fragment.getMediaFragment();
+ mediaCardFragment.updateContentView(mDescriptiveTextWithControlsView);
+ mediaCardFragment.updateContentView(DESCRIPTIVE_TEXT_VIEW);
+
+ onView(allOf(withId(R.id.card_background),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.card_background_image), is(instanceOf(CrossfadeImageView.class)),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.descriptive_text_layout),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.descriptive_text_with_controls_layout),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.media_layout),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_MEDIA_CARD_FULLSCREEN)
+ public void mediaFragment_updateContentView_textBlock_hidesPlaybackControlsBar() {
+ AudioCardFragment fragment = (AudioCardFragment) mActivity.getSupportFragmentManager()
+ .findFragmentById(R.id.bottom_card);
+ MediaCardFragment mediaCardFragment = (MediaCardFragment) fragment.getMediaFragment();
+ mediaCardFragment.updateContentView(mDescriptiveTextWithControlsView);
+ mediaCardFragment.updateContentView(TEXT_BLOCK_VIEW);
+
+ onView(allOf(withId(R.id.card_background),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.card_background_image), is(instanceOf(CrossfadeImageView.class)),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.text_block_layout),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.descriptive_text_with_controls_layout),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.media_layout),
+ isDescendantOfA(withId(R.id.media_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_MEDIA_CARD_FULLSCREEN)
+ public void dialerFragment_updateContentView_descriptiveText_hidesDescriptiveControlsView() {
+ AudioCardFragment fragment = (AudioCardFragment) mActivity.getSupportFragmentManager()
+ .findFragmentById(R.id.bottom_card);
+ mActivity.runOnUiThread(fragment::showInCallCard);
+ DialerCardFragment dialerCardFragment = (DialerCardFragment) fragment.getInCallFragment();
+ dialerCardFragment.updateContentView(mDescriptiveTextWithControlsViewWithButtons);
+ dialerCardFragment.updateContentView(DESCRIPTIVE_TEXT_VIEW);
+
+ // card_background is displayed since the onRootLayoutChangeListener sets it visible
+ onView(allOf(withId(R.id.card_background),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.card_background_image), is(instanceOf(CrossfadeImageView.class)),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.descriptive_text_layout),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.descriptive_text_with_controls_layout),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.media_layout),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ }
+
+ @Test
+ public void dialerFragment_updateContentView_textBlock_hidesDescriptiveControlsView() {
+ AudioCardFragment fragment = (AudioCardFragment) mActivity.getSupportFragmentManager()
+ .findFragmentById(R.id.bottom_card);
+ mActivity.runOnUiThread(fragment::showInCallCard);
+ DialerCardFragment dialerCardFragment = (DialerCardFragment) fragment.getInCallFragment();
+ dialerCardFragment.updateContentView(mDescriptiveTextWithControlsViewWithButtons);
+ dialerCardFragment.updateContentView(TEXT_BLOCK_VIEW);
+
+ // card_background is displayed since the onRootLayoutChangeListener sets it visible
+ onView(allOf(withId(R.id.card_background),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.card_background_image), is(instanceOf(CrossfadeImageView.class)),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.text_block_layout),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(isDisplayed()));
+ onView(allOf(withId(R.id.descriptive_text_with_controls_layout),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ onView(allOf(withId(R.id.media_layout),
+ isDescendantOfA(withId(R.id.in_call_fragment_container)),
+ isDescendantOfA(withId(R.id.bottom_card))))
+ .inRoot(hasWindowLayoutParams())
+ .check(matches(not(isDisplayed())));
+ }
+}
diff --git a/app/tests/src/com/android/car/carlauncher/homescreen/audio/AudioFragmentTest.java b/app/tests/src/com/android/car/carlauncher/homescreen/audio/AudioFragmentTest.java
deleted file mode 100644
index 1f0b5fb..0000000
--- a/app/tests/src/com/android/car/carlauncher/homescreen/audio/AudioFragmentTest.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * Copyright (C) 2020 Google Inc.
- *
- * 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.carlauncher.homescreen.audio;
-
-import static androidx.test.espresso.Espresso.onView;
-import static androidx.test.espresso.assertion.ViewAssertions.matches;
-import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
-import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static androidx.test.espresso.matcher.ViewMatchers.withId;
-import static androidx.test.espresso.matcher.ViewMatchers.withText;
-
-import static org.hamcrest.CoreMatchers.allOf;
-import static org.hamcrest.CoreMatchers.instanceOf;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.not;
-
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.Suppress;
-import androidx.test.rule.ActivityTestRule;
-
-import com.android.car.apps.common.CrossfadeImageView;
-import com.android.car.carlauncher.CarLauncher;
-import com.android.car.carlauncher.R;
-import com.android.car.carlauncher.homescreen.ui.CardContent.CardBackgroundImage;
-import com.android.car.carlauncher.homescreen.ui.CardHeader;
-import com.android.car.carlauncher.homescreen.ui.DescriptiveTextView;
-import com.android.car.carlauncher.homescreen.ui.DescriptiveTextWithControlsView;
-import com.android.car.carlauncher.homescreen.ui.TextBlockView;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@Suppress // To be ignored until b/224978827 is fixed
-@RunWith(AndroidJUnit4.class)
-public class AudioFragmentTest {
-
- private static final CardHeader CARD_HEADER = new CardHeader("Test App Name", null);
- private static final BitmapDrawable BITMAP = new BitmapDrawable(
- Bitmap.createBitmap(/* width = */100, /* height = */100, Bitmap.Config.ARGB_8888));
- private static final CardBackgroundImage CARD_BACKGROUND_IMAGE = new CardBackgroundImage(
- BITMAP, null);
- private static final String AUDIO_VIEW_TITLE = "Test song title";
- private static final String AUDIO_VIEW_SUBTITLE = "Test artist name";
- private static final long AUDIO_START_TIME = 1L;
- private static final DescriptiveTextView DESCRIPTIVE_TEXT_VIEW = new DescriptiveTextView(
- /* image = */ null, "Primary Text", "Secondary Text");
- private static final TextBlockView TEXT_BLOCK_VIEW = new TextBlockView("Text");
-
- private final DescriptiveTextWithControlsView
- mDescriptiveTextWithControlsView = new DescriptiveTextWithControlsView(
- CARD_BACKGROUND_IMAGE,
- AUDIO_VIEW_TITLE,
- AUDIO_VIEW_SUBTITLE);
- private final DescriptiveTextWithControlsView.Control mControl =
- new DescriptiveTextWithControlsView.Control(BITMAP, v -> {
- });
- private final DescriptiveTextWithControlsView
- mDescriptiveTextWithControlsViewWithButtons = new DescriptiveTextWithControlsView(
- CARD_BACKGROUND_IMAGE, AUDIO_VIEW_TITLE, AUDIO_VIEW_SUBTITLE, AUDIO_START_TIME,
- mControl, mControl, mControl);
-
- @Rule
- public ActivityTestRule<CarLauncher> mActivityTestRule =
- new ActivityTestRule<CarLauncher>(CarLauncher.class);
-
- @Test
- public void updateContentAndHeaderView_audioContentNoControls_showsMediaPlaybackControlsBar() {
- AudioFragment fragment = (AudioFragment) mActivityTestRule.getActivity()
- .getSupportFragmentManager().findFragmentById(R.id.bottom_card);
- mActivityTestRule.getActivity().runOnUiThread(fragment::hideCard);
- fragment.updateContentView(mDescriptiveTextWithControlsView);
- // Card is only made visible when the header is updated
- // But content should still be updated so it is correct when card is next made visible
- onView(allOf(withId(R.id.card_view), isDescendantOfA(withId(R.id.bottom_card))))
- .check(matches(not(isDisplayed())));
-
- // Now the card is made visible and we verify that content has been updated
- fragment.updateHeaderView(CARD_HEADER);
- onView(allOf(withId(R.id.card_background),
- isDescendantOfA(withId(R.id.bottom_card)))).check(matches(isDisplayed()));
- onView(allOf(withId(R.id.card_background_image), is(instanceOf(CrossfadeImageView.class)),
- isDescendantOfA(withId(R.id.bottom_card)))).check(matches(isDisplayed()));
- onView(allOf(withId(R.id.media_layout),
- isDescendantOfA(withId(R.id.bottom_card)))).check(matches(isDisplayed()));
- onView(allOf(withId(R.id.primary_text), withText(AUDIO_VIEW_TITLE),
- isDescendantOfA(withId(R.id.media_layout)),
- isDescendantOfA(withId(R.id.bottom_card)))).check(matches(isDisplayed()));
- onView(allOf(withId(R.id.secondary_text), withText(AUDIO_VIEW_SUBTITLE),
- isDescendantOfA(withId(R.id.media_layout)),
- isDescendantOfA(withId(R.id.bottom_card)))).check(matches(isDisplayed()));
- onView(allOf(withId(R.id.optional_timer), isDescendantOfA(withId(R.id.bottom_card)),
- isDescendantOfA(withId(R.id.media_layout)))).check(
- matches(not(isDisplayed())));
- onView(allOf(withId(R.id.media_playback_controls_bar),
- isDescendantOfA(withId(R.id.media_layout)),
- isDescendantOfA(withId(R.id.bottom_card)))).check(matches(isDisplayed()));
- onView(allOf(withId(R.id.descriptive_text_with_controls_layout),
- isDescendantOfA(withId(R.id.bottom_card)))).check(matches(not(isDisplayed())));
- }
-
- @Test
- public void updateContentAndHeaderView_audioContentWithControls_showsControlBar() {
- AudioFragment fragment = (AudioFragment) mActivityTestRule.getActivity()
- .getSupportFragmentManager().findFragmentById(R.id.bottom_card);
- mActivityTestRule.getActivity().runOnUiThread(fragment::hideCard);
- fragment.updateHeaderView(CARD_HEADER);
- fragment.updateContentView(mDescriptiveTextWithControlsViewWithButtons);
-
- onView(allOf(withId(R.id.optional_timer),
- isDescendantOfA(withId(R.id.descriptive_text_with_controls_layout)),
- isDescendantOfA(withId(R.id.bottom_card)))).check(matches(isDisplayed()));
- onView(allOf(withId(R.id.button_left),
- isDescendantOfA(withId(R.id.descriptive_text_with_controls_layout)),
- isDescendantOfA(withId(R.id.bottom_card)))).check(matches(isDisplayed()));
- onView(allOf(withId(R.id.button_center),
- isDescendantOfA(withId(R.id.descriptive_text_with_controls_layout)),
- isDescendantOfA(withId(R.id.bottom_card)))).check(matches(isDisplayed()));
- onView(allOf(withId(R.id.button_right),
- isDescendantOfA(withId(R.id.descriptive_text_with_controls_layout)),
- isDescendantOfA(withId(R.id.bottom_card)))).check(matches(isDisplayed()));
- onView(allOf(withId(R.id.media_playback_controls_bar),
- isDescendantOfA(withId(R.id.media_layout)),
- isDescendantOfA(withId(R.id.bottom_card)))).check(matches(not(isDisplayed())));
- onView(allOf(withId(R.id.media_layout), isDescendantOfA(withId(R.id.bottom_card)))).check(
- matches(not(isDisplayed())));
- }
-
- @Test
- public void updateContentView_descriptiveText_hidesPlaybackControlsBar() {
- AudioFragment fragment = (AudioFragment) mActivityTestRule.getActivity()
- .getSupportFragmentManager().findFragmentById(R.id.bottom_card);
- fragment.updateContentView(mDescriptiveTextWithControlsView);
- fragment.updateContentView(DESCRIPTIVE_TEXT_VIEW);
-
- onView(allOf(withId(R.id.card_background),
- isDescendantOfA(withId(R.id.bottom_card)))).check(matches(not(isDisplayed())));
- onView(allOf(withId(R.id.card_background_image), is(instanceOf(CrossfadeImageView.class)),
- isDescendantOfA(withId(R.id.bottom_card)))).check(matches(not(isDisplayed())));
- onView(allOf(withId(R.id.descriptive_text_layout),
- isDescendantOfA(withId(R.id.bottom_card)))).check(matches(isDisplayed()));
- onView(allOf(withId(R.id.descriptive_text_with_controls_layout),
- isDescendantOfA(withId(R.id.bottom_card)))).check(matches(not(isDisplayed())));
- onView(allOf(withId(R.id.media_layout), isDescendantOfA(withId(R.id.bottom_card)))).check(
- matches(not(isDisplayed())));
- }
-
- @Test
- public void updateContentView_textBlock_hidesPlaybackControlsBar() {
- AudioFragment fragment = (AudioFragment) mActivityTestRule.getActivity()
- .getSupportFragmentManager().findFragmentById(R.id.bottom_card);
- fragment.updateContentView(mDescriptiveTextWithControlsView);
- fragment.updateContentView(TEXT_BLOCK_VIEW);
-
- onView(allOf(withId(R.id.card_background),
- isDescendantOfA(withId(R.id.bottom_card)))).check(matches(not(isDisplayed())));
- onView(allOf(withId(R.id.card_background_image), is(instanceOf(CrossfadeImageView.class)),
- isDescendantOfA(withId(R.id.bottom_card)))).check(matches(not(isDisplayed())));
- onView(allOf(withId(R.id.text_block_layout),
- isDescendantOfA(withId(R.id.bottom_card)))).check(matches(isDisplayed()));
- onView(allOf(withId(R.id.descriptive_text_with_controls_layout),
- isDescendantOfA(withId(R.id.bottom_card)))).check(matches(not(isDisplayed())));
- onView(allOf(withId(R.id.media_layout), isDescendantOfA(withId(R.id.bottom_card)))).check(
- matches(not(isDisplayed())));
- }
-}
diff --git a/app/tests/src/com/android/car/carlauncher/homescreen/audio/HomeAudioCardPresenterTest.java b/app/tests/src/com/android/car/carlauncher/homescreen/audio/HomeAudioCardPresenterTest.java
deleted file mode 100644
index 160245d..0000000
--- a/app/tests/src/com/android/car/carlauncher/homescreen/audio/HomeAudioCardPresenterTest.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright (C) 2020 Google Inc.
- *
- * 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.carlauncher.homescreen.audio;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.view.View;
-
-import com.android.car.carlauncher.homescreen.HomeCardInterface;
-import com.android.car.carlauncher.homescreen.ui.CardHeader;
-import com.android.car.carlauncher.homescreen.ui.DescriptiveTextView;
-import com.android.car.carlauncher.homescreen.ui.DescriptiveTextWithControlsView;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(JUnit4.class)
-public class HomeAudioCardPresenterTest {
-
- private static final CardHeader CARD_HEADER = new CardHeader("testAppName", /* appIcon = */
- null);
- private static final DescriptiveTextView CARD_CONTENT = new DescriptiveTextView(/* image = */
- null, "title", "subtitle");
-
- private HomeAudioCardPresenter mPresenter;
-
- @Mock
- private View mFragmentView;
- @Mock
- private AudioFragment mView;
- @Mock
- private AudioModel mModel;
- @Mock
- private InCallModel mOtherModel;
-
- private HomeCardInterface.Model.OnModelUpdateListener mOnModelUpdateListener;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- when(mModel.getCardHeader()).thenReturn(CARD_HEADER);
- when(mModel.getCardContent()).thenReturn(CARD_CONTENT);
- mPresenter = new HomeAudioCardPresenter();
- mPresenter.setView(mView);
- mOnModelUpdateListener = mPresenter.mOnModelUpdateListener;
- }
-
- @Test
- public void onModelUpdated_updatesFragment() {
- mOnModelUpdateListener.onModelUpdate(mModel);
-
- verify(mView).updateHeaderView(CARD_HEADER);
- verify(mView).updateContentView(CARD_CONTENT);
- }
-
- @Test
- public void onModelUpdated_nullDifferentModel_doesNotUpdate() {
- when(mOtherModel.getCardHeader()).thenReturn(null);
- mOnModelUpdateListener.onModelUpdate(mModel);
- reset(mView);
-
- mOnModelUpdateListener.onModelUpdate(mOtherModel);
-
- verify(mView, never()).hideCard();
- verify(mView, never()).updateHeaderView(any());
- verify(mView, never()).updateContentView(any());
- }
-
- @Test
- public void onModelUpdated_activePhoneCall_doesNotUpdateFragment() {
- //setUpActivePhoneCall in presenter
- CardHeader callModelHeader = new CardHeader("dialer", /* appIcon = */
- null);
- DescriptiveTextWithControlsView callModelContent = new DescriptiveTextWithControlsView(
- /* image = */ null, "callerNumber", "ongoingCall");
- when(mOtherModel.getCardHeader()).thenReturn(callModelHeader);
- when(mOtherModel.getCardContent()).thenReturn(callModelContent);
- mOnModelUpdateListener.onModelUpdate(mOtherModel);
-
- // send MediaModel update during ongoing call
- mOnModelUpdateListener.onModelUpdate(mModel);
-
- //verify call
- verify(mView).updateHeaderView(callModelHeader);
- verify(mView).updateContentView(callModelContent);
- verify(mView, never()).hideCard();
- verify(mView, never()).updateHeaderView(CARD_HEADER);
- verify(mView, never()).updateContentView(CARD_CONTENT);
- }
-
- @Test
- public void onModelUpdated_nullSameModel_updatesFragment() {
- mOnModelUpdateListener.onModelUpdate(mModel);
- reset(mView);
- when(mModel.getCardHeader()).thenReturn(null);
-
- mOnModelUpdateListener.onModelUpdate(mModel);
-
- verify(mView).hideCard();
- }
-
- @Test
- public void onModelUpdated_nullModelAndNullCurrentModel_updatesFragment() {
- when(mModel.getCardHeader()).thenReturn(null);
-
- mOnModelUpdateListener.onModelUpdate(mModel);
-
- verify(mView, never()).hideCard();
- }
-}
diff --git a/app/tests/src/com/android/car/carlauncher/homescreen/audio/MediaViewModelTest.java b/app/tests/src/com/android/car/carlauncher/homescreen/audio/MediaViewModelTest.java
index f5e0b01..e6e6583 100644
--- a/app/tests/src/com/android/car/carlauncher/homescreen/audio/MediaViewModelTest.java
+++ b/app/tests/src/com/android/car/carlauncher/homescreen/audio/MediaViewModelTest.java
@@ -30,6 +30,7 @@
import android.graphics.drawable.Drawable;
import androidx.lifecycle.MutableLiveData;
+import androidx.test.annotation.UiThreadTest;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -111,6 +112,7 @@
}
@Before
+ @UiThreadTest
public void setUp() {
MockitoAnnotations.initMocks(this);
mMediaViewModel = new MediaViewModel(ApplicationProvider.getApplicationContext(),
diff --git a/app/tests/src/com/android/car/carlauncher/homescreen/audio/dialer/DialerCardPresenterTest.java b/app/tests/src/com/android/car/carlauncher/homescreen/audio/dialer/DialerCardPresenterTest.java
new file mode 100644
index 0000000..0be8313
--- /dev/null
+++ b/app/tests/src/com/android/car/carlauncher/homescreen/audio/dialer/DialerCardPresenterTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.homescreen.audio.dialer;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.car.carlauncher.homescreen.HomeCardInterface;
+import com.android.car.carlauncher.homescreen.ui.CardHeader;
+import com.android.car.carlauncher.homescreen.ui.DescriptiveTextView;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(JUnit4.class)
+public class DialerCardPresenterTest {
+
+ private static final CardHeader CARD_HEADER = new CardHeader("testAppName", /* appIcon = */
+ null);
+ private static final DescriptiveTextView CARD_CONTENT = new DescriptiveTextView(/* image = */
+ null, "title", "subtitle");
+
+ private DialerCardPresenter mPresenter;
+
+ @Mock
+ private DialerCardFragment mView;
+ @Mock
+ private DialerCardModel mModel;
+
+ @Mock
+ private DialerCardPresenter.OnInCallStateChangeListener mOnInCallStateChangeListener;
+
+ private HomeCardInterface.Model.OnModelUpdateListener mOnModelUpdateListener;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mModel.getCardHeader()).thenReturn(CARD_HEADER);
+ when(mModel.getCardContent()).thenReturn(CARD_CONTENT);
+ when(mModel.hasActiveCall()).thenReturn(true);
+ mPresenter = new DialerCardPresenter();
+ mPresenter.setView(mView);
+ mPresenter.setModel(mModel);
+ mPresenter.setOnInCallStateChangeListener(mOnInCallStateChangeListener);
+ mOnModelUpdateListener = mPresenter.mOnInCallModelUpdateListener;
+ }
+
+ @Test
+ public void onModelUpdated_updatesFragment_hasActiveCall_callsStateChangedWithTrue() {
+ when(mModel.hasActiveCall()).thenReturn(true);
+ mPresenter.mHasActiveCall = false;
+
+ mOnModelUpdateListener.onModelUpdate(mModel);
+
+ verify(mView).updateHeaderView(CARD_HEADER);
+ verify(mView).updateContentView(CARD_CONTENT);
+ verify(mOnInCallStateChangeListener).onInCallStateChanged(true);
+ }
+
+ @Test
+ public void onModelUpdated_updatesFragment_noActiveCall_callStateChangedWithFalse() {
+ when(mModel.hasActiveCall()).thenReturn(false);
+ mPresenter.mHasActiveCall = true;
+
+ mOnModelUpdateListener.onModelUpdate(mModel);
+
+ verify(mView).updateHeaderView(CARD_HEADER);
+ verify(mView).updateContentView(CARD_CONTENT);
+ verify(mOnInCallStateChangeListener).onInCallStateChanged(false);
+ }
+
+ @Test
+ public void onModelUpdated_updatesFragment_noCallStateChange_doesNotCallStateChange() {
+ when(mModel.hasActiveCall()).thenReturn(true);
+ mPresenter.mHasActiveCall = true;
+
+ mOnModelUpdateListener.onModelUpdate(mModel);
+
+ verify(mView).updateHeaderView(CARD_HEADER);
+ verify(mView).updateContentView(CARD_CONTENT);
+ verify(mOnInCallStateChangeListener, never()).onInCallStateChanged(anyBoolean());
+ }
+
+ @Test
+ public void onModelUpdated_nullHeaderAndContent_doesNotUpdateFragment() {
+ when(mModel.getCardHeader()).thenReturn(null);
+ when(mModel.getCardContent()).thenReturn(null);
+
+ mOnModelUpdateListener.onModelUpdate(mModel);
+
+ verify(mView, never()).updateContentView(any());
+ verify(mView, never()).updateHeaderView(any());
+ }
+}
diff --git a/app/tests/src/com/android/car/carlauncher/homescreen/audio/media/MediaCardPresenterTest.java b/app/tests/src/com/android/car/carlauncher/homescreen/audio/media/MediaCardPresenterTest.java
new file mode 100644
index 0000000..029db1b
--- /dev/null
+++ b/app/tests/src/com/android/car/carlauncher/homescreen/audio/media/MediaCardPresenterTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.homescreen.audio.media;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.car.carlauncher.homescreen.HomeCardInterface;
+import com.android.car.carlauncher.homescreen.audio.MediaViewModel;
+import com.android.car.carlauncher.homescreen.ui.CardHeader;
+import com.android.car.carlauncher.homescreen.ui.DescriptiveTextView;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(JUnit4.class)
+public class MediaCardPresenterTest {
+
+ private static final CardHeader CARD_HEADER = new CardHeader("testAppName", /* appIcon = */
+ null);
+ private static final DescriptiveTextView CARD_CONTENT = new DescriptiveTextView(/* image = */
+ null, "title", "subtitle");
+
+ private MediaCardPresenter mPresenter;
+
+ @Mock
+ private MediaCardFragment mView;
+ @Mock
+ private MediaViewModel mModel;
+
+ private HomeCardInterface.Model.OnModelUpdateListener mOnModelUpdateListener;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mModel.getCardHeader()).thenReturn(CARD_HEADER);
+ when(mModel.getCardContent()).thenReturn(CARD_CONTENT);
+ mPresenter = new MediaCardPresenter();
+ mPresenter.setView(mView);
+ mPresenter.setModel(mModel);
+ mOnModelUpdateListener = mPresenter.mOnMediaModelUpdateListener;
+ }
+
+ @Test
+ public void onModelUpdated_updatesFragment() {
+ mOnModelUpdateListener.onModelUpdate(mModel);
+
+ verify(mView).updateHeaderView(CARD_HEADER);
+ verify(mView).updateContentView(CARD_CONTENT);
+ }
+
+ @Test
+ public void onModelUpdated_nullHeaderAndContent_doesNotUpdateFragment() {
+ when(mModel.getCardHeader()).thenReturn(null);
+ when(mModel.getCardContent()).thenReturn(null);
+
+ mOnModelUpdateListener.onModelUpdate(mModel);
+
+ verify(mView, never()).updateContentView(any());
+ verify(mView, never()).updateHeaderView(any());
+ }
+
+ @Test
+ public void onModelUpdated_activePhoneCall_doesNotUpdateFragment() {
+ //mimic active phone call in presenter
+ mPresenter.setShowMedia(false);
+
+ // send MediaModel update during ongoing call
+ mOnModelUpdateListener.onModelUpdate(mModel);
+
+ //verify call
+ verify(mView, never()).updateHeaderView(any());
+ verify(mView, never()).updateContentView(any());
+ }
+}
diff --git a/app/tests/src/com/android/car/carlauncher/recents/RecentTasksProviderTest.java b/app/tests/src/com/android/car/carlauncher/recents/RecentTasksProviderTest.java
index b03ebe4..71aa8d8 100644
--- a/app/tests/src/com/android/car/carlauncher/recents/RecentTasksProviderTest.java
+++ b/app/tests/src/com/android/car/carlauncher/recents/RecentTasksProviderTest.java
@@ -18,9 +18,9 @@
import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE;
-import static com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_FREEFORM;
-import static com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_SINGLE;
-import static com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_SPLIT;
+import static com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_FREEFORM;
+import static com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_SINGLE;
+import static com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_SPLIT;
import static com.google.common.truth.Truth.assertThat;
@@ -56,7 +56,7 @@
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.PackageManagerWrapper;
import com.android.wm.shell.recents.IRecentTasks;
-import com.android.wm.shell.util.GroupedRecentTaskInfo;
+import com.android.wm.shell.shared.GroupedRecentTaskInfo;
import com.google.common.util.concurrent.MoreExecutors;
diff --git a/build.gradle b/build.gradle
index 5590825..2d3cb5d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -27,12 +27,16 @@
gradle.ext.lib_car_ui_lib_aar = gradle.ext.prebuiltSdkPath + "current/aaos-libs/car-ui-lib.aar"
gradle.ext.lib_car_ui_lib_oem_apis = gradle.ext.prebuiltSdkPath + "current/aaos-libs/car-ui-lib-oem-apis.jar"
gradle.ext.lib_system_stubs = gradle.ext.prebuiltSdkPath + gradle.ext.aaosLatestSDK + "/system/android.jar"
+ gradle.ext.lib_car_test_api = gradle.ext.prebuiltSdkPath + gradle.ext.aaosLatestSDK + "/system/android.car.testapi.jar"
gradle.ext.debugCertPath = gradle.ext.repoRootPath + "/packages/apps/Car/Launcher/libs/appgrid/keys/com_android_car_launcher_test.jks"
gradle.ext.soongBash = gradle.ext.repoRootPath + "/build/soong/soong_ui.bash"
- gradle.ext.platformSdkVersion = "34" // Change this to the most recent android API level.
+ gradle.ext.platformSdkVersion = "35" // Change this to the most recent android API level.
if (file(gradle.ext.soongBash).exists()) {
- gradle.ext.platformSdkVersion = (gradle.ext.soongBash + " --dumpvar-mode PLATFORM_SDK_VERSION").execute().text.trim()
+ def soongPlatformSdkVersion = (gradle.ext.soongBash + " --dumpvar-mode PLATFORM_SDK_VERSION").execute().text.trim()
+ if (!soongPlatformSdkVersion.isEmpty()) {
+ gradle.ext.platformSdkVersion = soongPlatformSdkVersion
+ }
}
gradle.ext.getVersionCode = { ->
@@ -48,13 +52,13 @@
}
plugins {
- id 'com.android.application' version '8.1.2' apply false
- id 'com.android.library' version '8.1.2' apply false
+ // TODO b/324426571: The version for com.android.application and com.android.library is 8.1.2
+ id 'com.android.application' apply false
+ id 'com.android.library' apply false
id 'org.jetbrains.kotlin.android' version '1.8.21' apply false
id 'com.google.protobuf' version '0.9.1' apply false
}
-
allprojects {
tasks.withType(JavaCompile).tap {
configureEach {
@@ -64,4 +68,5 @@
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
}
}
+ ext.ANDROID_TOP = gradle.ext.repoRootPath
}
diff --git a/app/src/com/android/car/carlauncher/LaunchRootCarTaskViewCallbacks.java b/buildSrc/build.gradle.kts
similarity index 68%
copy from app/src/com/android/car/carlauncher/LaunchRootCarTaskViewCallbacks.java
copy to buildSrc/build.gradle.kts
index e30ffe5..d53f40c 100644
--- a/app/src/com/android/car/carlauncher/LaunchRootCarTaskViewCallbacks.java
+++ b/buildSrc/build.gradle.kts
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -14,10 +14,16 @@
* limitations under the License.
*/
-package com.android.car.carlauncher;
+plugins {
+ `java-gradle-plugin`
+ `kotlin-dsl`
+}
-/**
- * A callbacks interface for {@link LaunchRootCarTaskView}.
- */
-public interface LaunchRootCarTaskViewCallbacks extends
- CarTaskViewCallbacks {}
+repositories {
+ mavenCentral()
+ google()
+}
+
+dependencies {
+ implementation("com.android.tools.build:gradle:8.1.2")
+}
diff --git a/app/src/com/android/car/carlauncher/LaunchRootCarTaskViewCallbacks.java b/buildSrc/src/main/java/aconfig.gradle.kts
similarity index 68%
rename from app/src/com/android/car/carlauncher/LaunchRootCarTaskViewCallbacks.java
rename to buildSrc/src/main/java/aconfig.gradle.kts
index e30ffe5..d40d8ca 100644
--- a/app/src/com/android/car/carlauncher/LaunchRootCarTaskViewCallbacks.java
+++ b/buildSrc/src/main/java/aconfig.gradle.kts
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -14,10 +14,7 @@
* limitations under the License.
*/
-package com.android.car.carlauncher;
+import aconfig.AConfigPlugin
+import org.gradle.kotlin.dsl.apply
-/**
- * A callbacks interface for {@link LaunchRootCarTaskView}.
- */
-public interface LaunchRootCarTaskViewCallbacks extends
- CarTaskViewCallbacks {}
+apply<AConfigPlugin>()
diff --git a/buildSrc/src/main/java/aconfig/AConfigCreateCacheTask.kt b/buildSrc/src/main/java/aconfig/AConfigCreateCacheTask.kt
new file mode 100644
index 0000000..5212e33
--- /dev/null
+++ b/buildSrc/src/main/java/aconfig/AConfigCreateCacheTask.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 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 aconfig
+
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.AbstractExecTask
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.OutputFile
+
+abstract class AConfigCreateCacheTask :
+ AbstractExecTask<AConfigCreateCacheTask>(AConfigCreateCacheTask::class.java) {
+
+ @get:InputFile
+ abstract val aconfigPath: RegularFileProperty
+
+ @get:Input
+ abstract var packageName: String
+
+ @get:InputFiles
+ abstract val srcFiles: ConfigurableFileCollection
+
+ @get:OutputFile
+ abstract val outputFile: RegularFileProperty
+
+ override fun exec() {
+ commandLine(aconfigPath.get())
+ args("create-cache", "--package", packageName)
+
+ srcFiles.files.forEach { aconfigFile ->
+ args("--declarations", aconfigFile)
+ }
+ args("--cache", "${outputFile.get()}")
+ super.exec()
+ }
+}
diff --git a/buildSrc/src/main/java/aconfig/AConfigCreateJavaLibTask.kt b/buildSrc/src/main/java/aconfig/AConfigCreateJavaLibTask.kt
new file mode 100644
index 0000000..95e53ca
--- /dev/null
+++ b/buildSrc/src/main/java/aconfig/AConfigCreateJavaLibTask.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 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 aconfig
+
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.AbstractExecTask
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.OutputDirectory
+
+abstract class AConfigCreateJavaLibTask :
+ AbstractExecTask<AConfigCreateJavaLibTask>(AConfigCreateJavaLibTask::class.java) {
+
+ @get:InputFile
+ abstract val aconfigPath: RegularFileProperty
+
+ @get:InputFile
+ abstract val cacheFile: RegularFileProperty
+
+ @get:OutputDirectory
+ abstract val outputFolder: DirectoryProperty
+
+ override fun exec() {
+ commandLine(aconfigPath.get())
+ args(
+ "create-java-lib",
+ "--mode",
+ "production",
+ "--cache",
+ cacheFile.get(),
+ "--out",
+ outputFolder.get()
+ )
+ super.exec()
+ }
+}
diff --git a/buildSrc/src/main/java/aconfig/AConfigExtension.kt b/buildSrc/src/main/java/aconfig/AConfigExtension.kt
new file mode 100644
index 0000000..c2eb1ff
--- /dev/null
+++ b/buildSrc/src/main/java/aconfig/AConfigExtension.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 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 aconfig
+
+import org.gradle.api.Action
+import org.gradle.api.DomainObjectSet
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.provider.Property
+
+interface AConfigDeclaration {
+ val packageName: Property<String>
+ val srcFile: ConfigurableFileCollection
+}
+
+open class AConfigExtension(private val objectFactory: ObjectFactory) {
+
+ val declarations: DomainObjectSet<AConfigDeclaration> = objectFactory.domainObjectSet(
+ AConfigDeclaration::class.java
+ )
+
+ fun aconfigDeclaration(action: Action<AConfigDeclaration>) {
+ val declaration = objectFactory.newInstance(AConfigDeclaration::class.java)
+ action.execute(declaration)
+ declarations.add(declaration)
+ }
+}
diff --git a/buildSrc/src/main/java/aconfig/AConfigPlugin.kt b/buildSrc/src/main/java/aconfig/AConfigPlugin.kt
new file mode 100644
index 0000000..7c3c868
--- /dev/null
+++ b/buildSrc/src/main/java/aconfig/AConfigPlugin.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 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 aconfig
+
+import com.android.build.api.variant.AndroidComponentsExtension
+import java.io.File
+import org.apache.tools.ant.taskdefs.condition.Os
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.configurationcache.extensions.capitalized
+import org.gradle.kotlin.dsl.create
+import org.gradle.kotlin.dsl.extra
+import org.gradle.kotlin.dsl.getByType
+import org.gradle.kotlin.dsl.register
+
+abstract class AConfigPlugin : Plugin<Project> {
+
+ override fun apply(project: Project) {
+ project.dependencies.add("implementation", project.project(":libs:aconfig-platform-compat"))
+ project.extensions.create<AConfigExtension>("aconfig", project.objects)
+ val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
+ val androidTop = project.extra["ANDROID_TOP"].toString()
+ val platform = if (Os.isFamily(Os.FAMILY_MAC)) "darwin" else "linux"
+ androidComponents.onVariants { variant ->
+ val variantName = variant.name.capitalized()
+ val aconfigExtension = project.extensions.getByType<AConfigExtension>()
+ val aconfigBin = File("$androidTop/prebuilts/build-tools/$platform-x86/bin/aconfig")
+
+ aconfigExtension.declarations.forEach {
+ val pkgName = it.packageName.get()
+ val addFlagCacheTaskProvider = project.tasks.register<AConfigCreateCacheTask>(
+ "generate${variantName}FlagCache_$pkgName"
+ ) {
+ aconfigPath.set(aconfigBin)
+ packageName = pkgName
+ srcFiles.setFrom(it.srcFile)
+ outputFile.set(
+ project.layout.buildDirectory.file(
+ "intermediates/${variant.name}/aconfig/flag-cache-$pkgName.pb"
+ )
+ )
+ }
+ val addFlagLibTaskProvider = project.tasks.register<AConfigCreateJavaLibTask>(
+ "generate${variantName}FlagLib_$pkgName"
+ ) {
+ aconfigPath.set(aconfigBin)
+ cacheFile.set(
+ addFlagCacheTaskProvider.flatMap(AConfigCreateCacheTask::outputFile)
+ )
+ }
+ variant.sources.java?.addGeneratedSourceDirectory(
+ addFlagLibTaskProvider,
+ AConfigCreateJavaLibTask::outputFolder
+ )
+ }
+ }
+ }
+}
diff --git a/buildSrc/src/main/java/aconfig/README.md b/buildSrc/src/main/java/aconfig/README.md
new file mode 100644
index 0000000..d560409
--- /dev/null
+++ b/buildSrc/src/main/java/aconfig/README.md
@@ -0,0 +1,23 @@
+# AAOS AConfig gradle Plugin
+This plugin is copied over from:
+```
+$ANDROID_BUILD_TOP/packages/apps/ManagedProvisioning/studio-dev/ManagedProvisioningGradleProject/buildSrc/src/main/java/
+```
+
+This gradle plugin generates Trunk-stable Flag helper classes.
+
+## Using in module's build.gradle
+Add `id 'aconfig'` in plugins blocks and specify `packageName` and `.aconfig src file`
+For example:
+```
+plugins {
+ id 'aconfig'
+}
+
+aconfig {
+ aconfigDeclaration {
+ packageName.set("com.example.package.name")
+ srcFile.setFrom(files("some_flags.aconfig"))
+ }
+}
+```
diff --git a/docklib-util/Android.bp b/docklib-util/Android.bp
index d7e2f5a..29ae215 100644
--- a/docklib-util/Android.bp
+++ b/docklib-util/Android.bp
@@ -17,21 +17,32 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
-
android_library {
name: "CarDockUtilLib",
srcs: [
"src/**/*.java",
"src/**/*.kt",
+ ":hidden_api_enabled_srcs",
],
resource_dirs: ["res"],
static_libs: [
+ "dock_flags_java_lib",
"androidx.lifecycle_lifecycle-extensions",
- "com.google.android.material_material",
- "car-ui-lib",
],
manifest: "AndroidManifest.xml",
}
+
+aconfig_declarations {
+ name: "dock_flags",
+ package: "com.android.car.dockutil",
+ container: "system",
+ srcs: ["dock_flags.aconfig"],
+}
+
+java_aconfig_library {
+ name: "dock_flags_java_lib",
+ aconfig_declarations: "dock_flags",
+}
diff --git a/docklib-util/OWNERS b/docklib-util/OWNERS
index c7be970..7c73cd2 100644
--- a/docklib-util/OWNERS
+++ b/docklib-util/OWNERS
@@ -5,4 +5,3 @@
[email protected]
[email protected]
[email protected]
[email protected]
diff --git a/docklib-util/build.gradle b/docklib-util/build.gradle
new file mode 100644
index 0000000..c7f87d4
--- /dev/null
+++ b/docklib-util/build.gradle
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+plugins {
+ id 'com.android.library'
+ id 'kotlin-android'
+ id 'aconfig'
+}
+
+aconfig {
+ aconfigDeclaration {
+ packageName.set("com.android.car.dockutil")
+ srcFile.setFrom(files("dock_flags.aconfig"))
+ }
+}
+
+android {
+ namespace 'com.android.car.dockutil'
+ compileSdk gradle.ext.aaosTargetSDK
+
+ defaultConfig {
+ minSdk gradle.ext.aaosLatestSDK
+ targetSdk gradle.ext.aaosLatestSDK
+ versionCode gradle.ext.getVersionCode()
+ versionName gradle.ext.getVersionName()
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
+ sourceSets {
+ main {
+ manifest.srcFile 'AndroidManifest.xml'
+ res.srcDirs = ['res']
+ java.srcDirs = ['src']
+ }
+
+ androidTest {
+ java.srcDirs += 'tests/src'
+ }
+ }
+
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
+ returnDefaultValues = true
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ lintOptions {
+ disable 'PrivateResource'
+ }
+}
+
+dependencies {
+ implementation project(":libs:hidden-apis-compat:hidden-apis-disabled")
+ implementation 'androidx.annotation:annotation:1.7.1'
+
+ //Android Test dependencies
+ androidTestImplementation project(":libs:hidden-apis-compat:hidden-apis-disabled")
+ androidTestImplementation 'junit:junit:4.13.2'
+ androidTestImplementation "org.mockito:mockito-android:5.10.0"
+ androidTestImplementation "org.mockito:mockito-core:5.10.0"
+ androidTestImplementation "org.mockito.kotlin:mockito-kotlin:3.2.0"
+ androidTestImplementation "androidx.test:rules:1.5.0"
+ androidTestImplementation 'androidx.test:runner:1.5.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation "com.google.truth:truth:1.3.0"
+}
+
+java {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+}
diff --git a/docklib-util/dock_flags.aconfig b/docklib-util/dock_flags.aconfig
new file mode 100644
index 0000000..b9e7369
--- /dev/null
+++ b/docklib-util/dock_flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.car.dockutil"
+container: "system"
+
+flag {
+ name: "dock_feature"
+ namespace: "car_sys_exp"
+ description: "This flag enables dock in Car"
+ bug: "301482374"
+}
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-af/strings.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-af/strings.xml
index 2c24df7..3b9c4bd 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-af/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Speld app vas"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Ontspeld app"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-am/strings.xml
similarity index 60%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-am/strings.xml
index 2c24df7..b003435 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-am/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"መተግበሪያን ፒን ያድርጉ"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"መተግበሪያ ይንቀሉ"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-ar/strings.xml
similarity index 61%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-ar/strings.xml
index 2c24df7..ddcc49a 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-ar/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"تثبيت التطبيق"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"إزالة تثبيت التطبيق"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-as/strings.xml
similarity index 60%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-as/strings.xml
index 2c24df7..e21128b 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-as/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"এপ্ পিন কৰক"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"এপ্ আনপিন কৰক"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-az/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-az/strings.xml
index 2c24df7..92f48ca 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-az/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Tətbiqi bərkidin"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Tətbiqi bərkitməyin"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-b+sr+Latn/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-b+sr+Latn/strings.xml
index 2c24df7..b19e719 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-b+sr+Latn/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Zakači aplikaciju"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Otkači aplikaciju"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-be/strings.xml
similarity index 60%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-be/strings.xml
index 2c24df7..8969359 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-be/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Замацаваць праграму"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Адмацаваць праграму"</string>
</resources>
diff --git a/docklib-util/res/values-bg/strings.xml b/docklib-util/res/values-bg/strings.xml
new file mode 100644
index 0000000..752ec64
--- /dev/null
+++ b/docklib-util/res/values-bg/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Фиксиране на приложението"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Освобождаване на приложението"</string>
+</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-bn/strings.xml
similarity index 60%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-bn/strings.xml
index 2c24df7..1b0dbe3 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-bn/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"অ্যাপ পিন করুন"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"অ্যাপ আনপিন করুন"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-bs/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-bs/strings.xml
index 2c24df7..9b9734b 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-bs/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Kačenje aplikacije"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Otkačivanje aplikacije"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-ca/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-ca/strings.xml
index 2c24df7..3cc848c 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-ca/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Fixa l\'aplicació"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Deixa de fixar l\'aplicació"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-cs/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-cs/strings.xml
index 2c24df7..9e9312e 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-cs/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Připnout aplikaci"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Odepnout aplikaci"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-da/strings.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-da/strings.xml
index 2c24df7..636417f 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-da/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Fastgør app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Frigør app"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-de/strings.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-de/strings.xml
index 2c24df7..4c1593d 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-de/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"App anpinnen"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"App loslösen"</string>
</resources>
diff --git a/docklib-util/res/values-el/strings.xml b/docklib-util/res/values-el/strings.xml
new file mode 100644
index 0000000..75454fd
--- /dev/null
+++ b/docklib-util/res/values-el/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Καρφίτσωμα εφαρμογής"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Ξεκαρφίτσωμα εφαρμογής"</string>
+</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-en-rAU/strings.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-en-rAU/strings.xml
index 2c24df7..6424b1a 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-en-rAU/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Pin app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Unpin app"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-en-rGB/strings.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-en-rGB/strings.xml
index 2c24df7..6424b1a 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-en-rGB/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Pin app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Unpin app"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-en-rIN/strings.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-en-rIN/strings.xml
index 2c24df7..6424b1a 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-en-rIN/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Pin app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Unpin app"</string>
</resources>
diff --git a/docklib-util/res/values-en-rXC/strings.xml b/docklib-util/res/values-en-rXC/strings.xml
new file mode 100644
index 0000000..54b03e0
--- /dev/null
+++ b/docklib-util/res/values-en-rXC/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Pin app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Unpin app"</string>
+</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-es-rUS/strings.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-es-rUS/strings.xml
index 2c24df7..2dc2ca5 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-es-rUS/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Fijar app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Dejar de fijar app"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-es/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-es/strings.xml
index 2c24df7..8370b21 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-es/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Fijar aplicación"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Dejar de fijar aplicación"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-et/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-et/strings.xml
index 2c24df7..2161fee 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-et/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Rakenduse kinnitamine"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Rakenduse vabastamine"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-eu/strings.xml
similarity index 61%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-eu/strings.xml
index 2c24df7..fcfdb19 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-eu/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Ainguratu aplikazioa"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Kendu aplikazioaren aingura"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-fa/strings.xml
similarity index 60%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-fa/strings.xml
index 2c24df7..2edabaa 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-fa/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"سنجاق کردن برنامه"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"برداشتن سنجاق برنامه"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-fi/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-fi/strings.xml
index 2c24df7..cfc4bf3 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-fi/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Kiinnitä sovellus"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Irrota sovellus"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-fr-rCA/strings.xml
similarity index 61%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-fr-rCA/strings.xml
index 2c24df7..2e6a162 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-fr-rCA/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Épingler l\'application"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Annuler l\'épinglage de l\'application"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-fr/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-fr/strings.xml
index 2c24df7..3b22b81 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-fr/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Épingler l\'appli"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Retirer l\'appli"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-gl/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-gl/strings.xml
index 2c24df7..7168357 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-gl/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Fixar aplicación"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Deixar de fixar aplicación"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-gu/strings.xml
similarity index 61%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-gu/strings.xml
index 2c24df7..6c3710b 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-gu/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"ઍપ પિન કરો"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"ઍપ અનપિન કરો"</string>
</resources>
diff --git a/docklib-util/res/values-hi/strings.xml b/docklib-util/res/values-hi/strings.xml
new file mode 100644
index 0000000..4a66c72
--- /dev/null
+++ b/docklib-util/res/values-hi/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"ऐप्लिकेशन को पिन करें"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"ऐप्लिकेशन को अनपिन करें"</string>
+</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-hr/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-hr/strings.xml
index 2c24df7..8bd8dd4 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-hr/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Prikvačite aplikaciju"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Otkvačite aplikaciju"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-hu/strings.xml
similarity index 61%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-hu/strings.xml
index 2c24df7..e4857fe 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-hu/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Alkalmazás kitűzése"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Alkalmazás kitűzésének megszüntetése"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-hy/strings.xml
similarity index 60%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-hy/strings.xml
index 2c24df7..b5ae9e2 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-hy/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Ամրացնել հավելվածը"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Ապամրացնել հավելվածը"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-in/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-in/strings.xml
index 2c24df7..4588f2d 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-in/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Sematkan aplikasi"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Lepaskan aplikasi"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-is/strings.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-is/strings.xml
index 2c24df7..208fa99 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-is/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Festa forrit"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Losa forrit"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-it/strings.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-it/strings.xml
index 2c24df7..9372bc3 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-it/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Fissa app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Stacca app"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-iw/strings.xml
similarity index 60%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-iw/strings.xml
index 2c24df7..de0d846 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-iw/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"הצמדת האפליקציה"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"ביטול הצמדת האפליקציה"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-ja/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-ja/strings.xml
index 2c24df7..04061a6 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-ja/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"アプリを固定"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"アプリの固定を解除"</string>
</resources>
diff --git a/docklib-util/res/values-ka/strings.xml b/docklib-util/res/values-ka/strings.xml
new file mode 100644
index 0000000..e180e73
--- /dev/null
+++ b/docklib-util/res/values-ka/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"აპის ჩამაგრება"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"აპის ჩამაგრების მოხსნა"</string>
+</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-kk/strings.xml
similarity index 60%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-kk/strings.xml
index 2c24df7..3b33b0b 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-kk/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Қолданбаны бекіту"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Қолданбаны босату"</string>
</resources>
diff --git a/docklib-util/res/values-km/strings.xml b/docklib-util/res/values-km/strings.xml
new file mode 100644
index 0000000..1429d24
--- /dev/null
+++ b/docklib-util/res/values-km/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"ខ្ទាស់កម្មវិធី"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"ដកខ្ទាស់កម្មវិធី"</string>
+</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-kn/strings.xml
similarity index 60%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-kn/strings.xml
index 2c24df7..c148268 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-kn/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"ಆ್ಯಪ್ ಪಿನ್ ಮಾಡಿ"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"ಆ್ಯಪ್ ಅನ್ಪಿನ್ ಮಾಡಿ"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-ko/strings.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-ko/strings.xml
index 2c24df7..b13e0bd 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-ko/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"앱 고정"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"앱 고정 해제"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-ky/strings.xml
similarity index 60%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-ky/strings.xml
index 2c24df7..25463e6 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-ky/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Колдонмону кадап коюу"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Колдонмону бошотуу"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-lo/strings.xml
similarity index 60%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-lo/strings.xml
index 2c24df7..3459dee 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-lo/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"ປັກໝຸດແອັບ"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"ຖອດປັກໝຸດແອັບ"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-lt/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-lt/strings.xml
index 2c24df7..206c28c 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-lt/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Prisegti programą"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Atsegti programą"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-lv/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-lv/strings.xml
index 2c24df7..9424827 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-lv/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Piespraust lietotni"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Atspraust lietotni"</string>
</resources>
diff --git a/docklib-util/res/values-mk/strings.xml b/docklib-util/res/values-mk/strings.xml
new file mode 100644
index 0000000..9351b67
--- /dev/null
+++ b/docklib-util/res/values-mk/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Закачување на апликацијата"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Откачување на апликацијата"</string>
+</resources>
diff --git a/docklib-util/res/values-ml/strings.xml b/docklib-util/res/values-ml/strings.xml
new file mode 100644
index 0000000..472f559
--- /dev/null
+++ b/docklib-util/res/values-ml/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"ആപ്പ് പിൻ ചെയ്യുക"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"ആപ്പ് അൺപിൻ ചെയ്യുക"</string>
+</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-mn/strings.xml
similarity index 60%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-mn/strings.xml
index 2c24df7..04ef4c9 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-mn/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Аппыг бэхлэх"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Аппыг бэхэлснийг болиулах"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-mr/strings.xml
similarity index 60%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-mr/strings.xml
index 2c24df7..5145620 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-mr/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"अॅप पिन करा"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"अॅप अनपिन करा"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-ms/strings.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-ms/strings.xml
index 2c24df7..ac93edf 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-ms/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Semat apl"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Nyahsemat apl"</string>
</resources>
diff --git a/docklib-util/res/values-my/strings.xml b/docklib-util/res/values-my/strings.xml
new file mode 100644
index 0000000..eb48e4f
--- /dev/null
+++ b/docklib-util/res/values-my/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"အက်ပ်ကို ပင်ထိုးရန်"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"အက်ပ်ကို ပင်ဖြုတ်ရန်"</string>
+</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-nb/strings.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-nb/strings.xml
index 2c24df7..b72bf4e 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-nb/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Fest appen"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Løsne appen"</string>
</resources>
diff --git a/docklib-util/res/values-ne/strings.xml b/docklib-util/res/values-ne/strings.xml
new file mode 100644
index 0000000..ad0e79a
--- /dev/null
+++ b/docklib-util/res/values-ne/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"एप पिन गर्नुहोस्"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"एप अनपिन गर्नुहोस्"</string>
+</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-nl/strings.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-nl/strings.xml
index 2c24df7..c5b068c 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-nl/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"App vastzetten"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"App losmaken"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-or/strings.xml
similarity index 60%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-or/strings.xml
index 2c24df7..87a4a09 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-or/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"ଆପ ପିନ କରନ୍ତୁ"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"ଆପ ଅନପିନ କରନ୍ତୁ"</string>
</resources>
diff --git a/docklib-util/res/values-pa/strings.xml b/docklib-util/res/values-pa/strings.xml
new file mode 100644
index 0000000..b482240
--- /dev/null
+++ b/docklib-util/res/values-pa/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"ਐਪ ਨੂੰ ਪਿੰਨ ਕਰੋ"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"ਐਪ ਨੂੰ ਅਣਪਿੰਨ ਕਰੋ"</string>
+</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-pl/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-pl/strings.xml
index 2c24df7..7695a48 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-pl/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Przypnij aplikację"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Odepnij aplikację"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-pt-rPT/strings.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-pt-rPT/strings.xml
index 2c24df7..63ba79e 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-pt-rPT/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Afixar app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Desafixar app"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-pt/strings.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-pt/strings.xml
index 2c24df7..19ed9e2 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-pt/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Fixar app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Liberar app"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-ro/strings.xml
similarity index 61%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-ro/strings.xml
index 2c24df7..0f3d303 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-ro/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Fixează aplicația"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Anulează fixarea aplicației"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-ru/strings.xml
similarity index 60%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-ru/strings.xml
index 2c24df7..d7c1efc 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-ru/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Закрепить приложение"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Открепить приложение"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-si/strings.xml
similarity index 60%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-si/strings.xml
index 2c24df7..d386dab 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-si/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"යෙදුම අමුණන්න"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"යෙදුම නොඅමුණන්න"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-sk/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-sk/strings.xml
index 2c24df7..d711648 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-sk/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Pripnúť aplikáciu"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Odopnúť aplikáciu"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-sl/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-sl/strings.xml
index 2c24df7..7a97c75 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-sl/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Pripenjanje aplikacije"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Odpenjanje aplikacije"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-sq/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-sq/strings.xml
index 2c24df7..d1b3afe 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-sq/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Gozhdo aplikacionin"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Zhgozhdo aplikacionin"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-sr/strings.xml
similarity index 60%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-sr/strings.xml
index 2c24df7..4a1fbc6 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-sr/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Закачи апликацију"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Откачи апликацију"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-sv/strings.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-sv/strings.xml
index 2c24df7..9a0907f 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-sv/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Fäst appen"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Lossa appen"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-sw/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-sw/strings.xml
index 2c24df7..973e2b4 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-sw/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Bandika programu"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Bandua programu"</string>
</resources>
diff --git a/docklib-util/res/values-ta/strings.xml b/docklib-util/res/values-ta/strings.xml
new file mode 100644
index 0000000..d4bd741
--- /dev/null
+++ b/docklib-util/res/values-ta/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"ஆப்ஸைப் பின் செய்தல்"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"ஆப்ஸை அகற்றுதல்"</string>
+</resources>
diff --git a/docklib-util/res/values-te/strings.xml b/docklib-util/res/values-te/strings.xml
new file mode 100644
index 0000000..4b9ebca
--- /dev/null
+++ b/docklib-util/res/values-te/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"యాప్ను పిన్ చేయండి"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"యాప్ను అన్పిన్ చేయండి"</string>
+</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-th/strings.xml
similarity index 60%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-th/strings.xml
index 2c24df7..2d07171 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-th/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"ปักหมุดแอป"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"เลิกปักหมุดแอป"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-tl/strings.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-tl/strings.xml
index 2c24df7..1978d27 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-tl/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"I-pin ang app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"I-unpin ang app"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-tr/strings.xml
similarity index 61%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-tr/strings.xml
index 2c24df7..0c34641 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-tr/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Uygulamayı sabitle"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Uygulamanın sabitlemesini kaldır"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-uk/strings.xml
similarity index 60%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-uk/strings.xml
index 2c24df7..f0530b4 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-uk/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Закріпити додаток"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Відкріпити додаток"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-ur/strings.xml
similarity index 61%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-ur/strings.xml
index 2c24df7..78627b5 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-ur/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"ایپ پن کریں"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"ایپ کی پن ہٹائیں"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-uz/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-uz/strings.xml
index 2c24df7..ef2fe7c 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-uz/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Ilovani mahkamlash"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Ilovani yechish"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-vi/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-vi/strings.xml
index 2c24df7..5edae37 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-vi/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Ghim ứng dụng"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Bỏ ghim ứng dụng"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-zh-rCN/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-zh-rCN/strings.xml
index 2c24df7..25a1bcc 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-zh-rCN/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"固定应用"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"取消固定应用"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-zh-rHK/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-zh-rHK/strings.xml
index 2c24df7..88eebc5 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-zh-rHK/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"固定應用程式"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"取消固定應用程式"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-zh-rTW/strings.xml
similarity index 62%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-zh-rTW/strings.xml
index 2c24df7..88eebc5 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-zh-rTW/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"固定應用程式"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"取消固定應用程式"</string>
</resources>
diff --git a/docklib-util/res/values/strings.xml b/docklib-util/res/values-zu/strings.xml
similarity index 63%
copy from docklib-util/res/values/strings.xml
copy to docklib-util/res/values-zu/strings.xml
index 2c24df7..3b4a509 100644
--- a/docklib-util/res/values/strings.xml
+++ b/docklib-util/res/values-zu/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,10 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="2366240044962379929">"Phina i-app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="4995780367670958298">"Susa ukuphina i-app"</string>
</resources>
diff --git a/docklib-util/res/values/config.xml b/docklib-util/res/values/config.xml
index 93ff11d..2a28350 100644
--- a/docklib-util/res/values/config.xml
+++ b/docklib-util/res/values/config.xml
@@ -16,7 +16,4 @@
-->
<resources>
- <!-- Flag to enable/disable dock -->
- <!-- todo(b/304320644): remove flag -->
- <bool name="config_enableDock">false</bool>
</resources>
diff --git a/docklib-util/src/com/android/car/dockutil/events/DockEventSenderHelper.java b/docklib-util/src/com/android/car/dockutil/events/DockEventSenderHelper.java
index 4f8b52f..42197d6 100644
--- a/docklib-util/src/com/android/car/dockutil/events/DockEventSenderHelper.java
+++ b/docklib-util/src/com/android/car/dockutil/events/DockEventSenderHelper.java
@@ -16,6 +16,8 @@
package com.android.car.dockutil.events;
+import static com.android.car.hidden.apis.HiddenApiAccess.getDisplayId;
+
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
@@ -26,7 +28,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import com.android.car.dockutil.R;
+import com.android.car.dockutil.Flags;
/**
* Helper used to send Dock Events.
@@ -35,11 +37,9 @@
public static final String EXTRA_COMPONENT = "EXTRA_COMPONENT";
private final Context mContext;
- private final boolean mIsDockEnabled;
public DockEventSenderHelper(Context context) {
mContext = context;
- mIsDockEnabled = mContext.getResources().getBoolean(R.bool.config_enableDock);
}
/**
@@ -80,8 +80,8 @@
@VisibleForTesting
void sendEventBroadcast(@NonNull DockEvent event,
- @NonNull ActivityManager.RunningTaskInfo taskInfo) {
- if (taskInfo.getDisplayId() != Display.DEFAULT_DISPLAY) {
+ @NonNull ActivityManager.RunningTaskInfo taskInfo) {
+ if (getDisplayId(taskInfo) != Display.DEFAULT_DISPLAY) {
return;
}
ComponentName component = getComponentName(taskInfo);
@@ -91,7 +91,7 @@
}
private void sendEventBroadcast(@NonNull DockEvent event, @NonNull ComponentName component) {
- if (!mIsDockEnabled) {
+ if (!Flags.dockFeature()) {
return;
}
diff --git a/docklib-util/tests/Android.bp b/docklib-util/tests/Android.bp
index 6883e97..4832e21 100644
--- a/docklib-util/tests/Android.bp
+++ b/docklib-util/tests/Android.bp
@@ -1,5 +1,5 @@
//
-// Copyright (C) 2023 Google Inc.
+// Copyright (C) 2024 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.
@@ -16,6 +16,7 @@
package {
default_applicable_licenses: ["Android-Apache-2.0"],
+ default_team: "trendy_team_system_experience",
}
android_test {
@@ -27,7 +28,7 @@
],
libs: [
- "android.test.base",
+ "android.test.base.stubs.system",
],
optimize: {
@@ -41,6 +42,7 @@
"mockito-kotlin2",
"truth",
"CarDockUtilLib",
+ "flag-junit",
],
manifest: "AndroidManifest.xml",
diff --git a/docklib-util/tests/src/com/android/car/dockutil/events/DockEventSenderHelperTest.java b/docklib-util/tests/src/com/android/car/dockutil/events/DockEventSenderHelperTest.java
index 892e99b..81f8bbb 100644
--- a/docklib-util/tests/src/com/android/car/dockutil/events/DockEventSenderHelperTest.java
+++ b/docklib-util/tests/src/com/android/car/dockutil/events/DockEventSenderHelperTest.java
@@ -34,12 +34,14 @@
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
+import android.platform.test.flag.junit.SetFlagsRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.android.car.dockutil.R;
+import com.android.car.dockutil.Flags;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -49,6 +51,8 @@
@RunWith(AndroidJUnit4.class)
public class DockEventSenderHelperTest {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Mock
public ActivityManager.RunningTaskInfo mRunningTaskInfo;
@Mock
@@ -67,7 +71,7 @@
public void setup() {
MockitoAnnotations.initMocks(this);
when(mContext.getResources()).thenReturn(mResources);
- when(mResources.getBoolean(R.bool.config_enableDock)).thenReturn(true);
+ mSetFlagsRule.enableFlags(Flags.FLAG_DOCK_FEATURE);
mDockEventSenderHelper = new DockEventSenderHelper(mContext);
}
diff --git a/docklib/Android.bp b/docklib/Android.bp
index 79137d0..8fd6785 100644
--- a/docklib/Android.bp
+++ b/docklib/Android.bp
@@ -17,6 +17,17 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
+java_library_static {
+ name: "dock_item",
+ host_supported: true,
+ proto: {
+ type: "lite",
+ },
+ sdk_version: "module_current",
+ min_sdk_version: "31",
+ srcs: ["src/com/android/car/docklib/data/proto/dock_item.proto"],
+}
+
android_library {
name: "CarDockLib",
srcs: [
@@ -30,12 +41,18 @@
static_libs: [
"androidx.recyclerview_recyclerview",
- "androidx.core_core-animation-nodeps",
+ "androidx.core_core-animation",
"car-ui-lib-no-overlayable",
"androidx.lifecycle_lifecycle-extensions",
"com.google.android.material_material",
"CarDockUtilLib",
- "SystemUISharedLib"
+ "CarLauncherCommon",
+ "SystemUISharedLib",
+ "//frameworks/libs/systemui:iconloader",
+ "car-resource-common",
+ "dock_item",
+ "car_launcher_flags_java_lib",
+ "car-media-common-no-overlayable",
],
platform_apis: true,
diff --git a/docklib/AndroidManifest.xml b/docklib/AndroidManifest.xml
index 64ecaf7..c3178e5 100644
--- a/docklib/AndroidManifest.xml
+++ b/docklib/AndroidManifest.xml
@@ -17,6 +17,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.car.docklib">
+ <!-- Permission to allow Dock to launch apps -->
+ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
+ <!-- System permission to query active media sessions -->
+ <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/>
+ <!-- System permission to access notifications -->
+ <uses-permission android:name="android.permission.ACCESS_NOTIFICATIONS"/>
<!-- Permission to allow packages to broadcast events to the dock -->
<permission
diff --git a/docklib/OWNERS b/docklib/OWNERS
index 3cb098d..3a6dfd6 100644
--- a/docklib/OWNERS
+++ b/docklib/OWNERS
@@ -5,5 +5,4 @@
[email protected]
[email protected]
[email protected]
[email protected]
diff --git a/docklib/res/layout/dock_view.xml b/docklib/res/layout/dock_view.xml
index d5b61e5..0446685 100644
--- a/docklib/res/layout/dock_view.xml
+++ b/docklib/res/layout/dock_view.xml
@@ -22,5 +22,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
+ android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</merge>
diff --git a/docklib/res/values-af/strings.xml b/docklib/res/values-af/strings.xml
new file mode 100644
index 0000000..66d922d
--- /dev/null
+++ b/docklib/res/values-af/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Dokuitsendingstuurder"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Toestemming word vereis vir pakket om geleenthede na die Dok uit te saai."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Dokuitsendingontvanger"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Toestemming word vereis vir pakket om te luister na uitsendinggeleenthede vir die Dok."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Geen plek beskikbaar om vas te speld nie"</string>
+</resources>
diff --git a/docklib/res/values-am/strings.xml b/docklib/res/values-am/strings.xml
new file mode 100644
index 0000000..82a8765
--- /dev/null
+++ b/docklib/res/values-am/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"የመትከያ የስርጭት ላኪ"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"ጥቅል ክስተቶችን ወደ መትከያ ለማሰራጨት ፈቃድ ያስፈልገዋል።"</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"የመትከያ የስርጭት ተቀባይ"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"የጥቅል ማዳመጥ ለመትከያ ክስተቶችን ለማሰራጨት ፈቃድ ያስፈልገዋል።"</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"ፒን ለማድረግ ምንም ቦታ የለም"</string>
+</resources>
diff --git a/docklib/res/values-ar/strings.xml b/docklib/res/values-ar/strings.xml
new file mode 100644
index 0000000..ea3e6ac
--- /dev/null
+++ b/docklib/res/values-ar/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"مرسِل بث قاعدة الإرساء"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"يجب الحصول على إذن لتتمكّن الحزمة من بث الأحداث إلى \"قاعدة الإرساء\"."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"مستقبِل بث قاعدة الإرساء"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"يجب الحصول على إذن بالاستماع لتتمكّن الحزمة من بث الأحداث إلى \"قاعدة الإرساء\"."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"ما مِن مكان متاح للتثبيت فيه"</string>
+</resources>
diff --git a/docklib/res/values-as/strings.xml b/docklib/res/values-as/strings.xml
new file mode 100644
index 0000000..6a32e5d
--- /dev/null
+++ b/docklib/res/values-as/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"ড’কৰ সম্প্ৰচাৰ প্ৰেৰক"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"ড’কলৈ ইভেণ্ট সম্প্ৰচাৰ কৰিবলৈ পেকেজক অনুমতিৰ প্ৰয়োজন।"</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"ড’কৰ সম্প্ৰচাৰ ৰিচিভাৰ"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"ড’কটোৰ বাবে অনুষ্ঠান সম্প্ৰচাৰ কৰিবলৈ পেকেজ শুনাৰ অনুমতিৰ আৱশ্যক।"</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"পিন কৰিবলৈ কোনো স্থান নাই"</string>
+</resources>
diff --git a/docklib/res/values-az/strings.xml b/docklib/res/values-az/strings.xml
new file mode 100644
index 0000000..4819f03
--- /dev/null
+++ b/docklib/res/values-az/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Dok yayımı üçün göndərən"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Paket tədbirlərinin Dokda yayımlanması üçün icazə lazımdır."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Dok üzrə yayımı qəbul edən"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Dok üçün tədbirləri yayımlamaqdan ötrü paket dinləmə üçün icazə lazımdır."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Bərkitmək üçün yer yoxdur"</string>
+</resources>
diff --git a/docklib/res/values-b+sr+Latn/strings.xml b/docklib/res/values-b+sr+Latn/strings.xml
new file mode 100644
index 0000000..6fbe1c0
--- /dev/null
+++ b/docklib/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Pošiljalac emitovanja za traku s aplikacijama"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Potrebna je dozvola da bi paket emitovao događaje za traku s aplikacijama."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Prijemnik emitovanja za traku s aplikacijama"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Potrebna je dozvola da bi osluškivač paketa emitovao događaje za traku s aplikacijama."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Nije dostupno mesto za kačenje"</string>
+</resources>
diff --git a/docklib/res/values-be/strings.xml b/docklib/res/values-be/strings.xml
new file mode 100644
index 0000000..8bed26d
--- /dev/null
+++ b/docklib/res/values-be/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Адпраўшчык трансляцыі паліцы"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Дазвол, які патрабуецца пакету, каб трансліраваць паліцы даныя аб падзеях"</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Атрымальнік трансляцыі паліцы"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Дазвол, які патрабуецца пакету, каб чакаць перадачы ад паліцы даных аб падзеях"</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Няма свабоднага месца для замацавання"</string>
+</resources>
diff --git a/docklib/res/values-bg/strings.xml b/docklib/res/values-bg/strings.xml
new file mode 100644
index 0000000..0e20d0a
--- /dev/null
+++ b/docklib/res/values-bg/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Подател на съобщение към докинг станцията"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Изисква се разрешение, за да може пакетът да съобщава събития към докинг станцията"</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Приемник на излъчвания от докинг станцията"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Изисква се разрешение за слушане на пакета с цел съобщаване на събития към докинг станцията."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Няма налично място за фиксиране"</string>
+</resources>
diff --git a/docklib/res/values-bn/strings.xml b/docklib/res/values-bn/strings.xml
new file mode 100644
index 0000000..b1f0efa
--- /dev/null
+++ b/docklib/res/values-bn/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"ডক ব্রডকাস্টের প্রেরক"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"ডকে ইভেন্ট ব্রডকাস্ট করতে প্যাকেজের জন্য অনুমতি প্রয়োজন।"</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"ডক ব্রডকাস্টের রিসিভার"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"ডকের জন্য ইভেন্ট সম্প্রচার করতে প্যাকেজ শোনার ক্ষেত্রে অনুমতি প্রয়োজন।"</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"পিন করার জন্য কোনও স্পট উপলভ্য নেই"</string>
+</resources>
diff --git a/docklib/res/values-bs/strings.xml b/docklib/res/values-bs/strings.xml
new file mode 100644
index 0000000..53f6c83
--- /dev/null
+++ b/docklib/res/values-bs/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Pošiljalac emitiranja na priključnoj stanici"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Potrebno je odobrenje da paket emitira događaje na priključnu stanicu."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Prijemnik emitiranog sadržaja na priključnoj stanici"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Potrebno je odobrenje za paketno slušanje radi emitiranja događaja za priključnu stanicu."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Nijedno mjesto nije dostupno za kačenje"</string>
+</resources>
diff --git a/docklib/res/values-ca/strings.xml b/docklib/res/values-ca/strings.xml
new file mode 100644
index 0000000..dc3be4e
--- /dev/null
+++ b/docklib/res/values-ca/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Emissor de difusió al connector"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Es necessita permís perquè el paquet emeti esdeveniments al connector."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Receptor d\'emissió al connector"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Es necessita permís perquè el paquet escolti els esdeveniments d\'emissió per al connector."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"No hi ha cap lloc disponible per fixar"</string>
+</resources>
diff --git a/docklib/res/values-cs/strings.xml b/docklib/res/values-cs/strings.xml
new file mode 100644
index 0000000..ec575a5
--- /dev/null
+++ b/docklib/res/values-cs/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Odesílatel vysílání do doku"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Aby balíček mohl vysílat události do doku, potřebuje oprávnění."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Příjem oznámení pro dok"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Aby mohl balíček poslouchat události vysílání pro dok, potřebuje oprávnění."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Žádné dostupné místo k připnutí"</string>
+</resources>
diff --git a/docklib/res/values-da/strings.xml b/docklib/res/values-da/strings.xml
new file mode 100644
index 0000000..b32f477
--- /dev/null
+++ b/docklib/res/values-da/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Signalsender i applinjen"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Tilladelse påkræves, før pakken kan sende beskeder om hændelser til applinjen."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Signalmodtager i applinjen"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Tilladelse påkræves, før pakken kan sende beskeder om hændelser til applinjen."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Der er intet sted, hvor du kan fastgøre din app"</string>
+</resources>
diff --git a/docklib/res/values-de/strings.xml b/docklib/res/values-de/strings.xml
new file mode 100644
index 0000000..8d5f8d7
--- /dev/null
+++ b/docklib/res/values-de/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Sender für die Übertragung an das Dock"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Es ist eine Berechtigung erforderlich, damit das Paket Ereignisse an das Dock übertragen kann."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Dock-Übertragungsempfänger"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Es ist eine Berechtigung erforderlich, damit das Paket Ereignisse abrufen und an das Dock übertragen kann."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Kein Platz zum Anpinnen verfügbar"</string>
+</resources>
diff --git a/docklib/res/values-el/strings.xml b/docklib/res/values-el/strings.xml
new file mode 100644
index 0000000..1bce60c
--- /dev/null
+++ b/docklib/res/values-el/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Αποστολέας μετάδοσης γραμμής εργαλείων"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Απαιτείται άδεια προκειμένου το πακέτο να μεταδίδει συμβάντα στη γραμμή εργαλείων."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Δέκτης μετάδοσης γραμμής εργαλείων"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Απαιτείται άδεια προκειμένου το πακέτο να ανιχνεύει τη μετάδοση συμβάντων για τη γραμμή εργαλείων."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Δεν υπάρχει διαθέσιμο σημείο για καρφίτσωμα"</string>
+</resources>
diff --git a/docklib/res/values-en-rAU/strings.xml b/docklib/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..6882693
--- /dev/null
+++ b/docklib/res/values-en-rAU/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Dock broadcast sender"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Permission required for package to broadcast events to the dock."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Dock broadcast receiver"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Permission required for package to listen to broadcast events for the dock."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"No spot available to pin"</string>
+</resources>
diff --git a/docklib/res/values-en-rCA/strings.xml b/docklib/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000..bafbfda
--- /dev/null
+++ b/docklib/res/values-en-rCA/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Dock broadcast sender"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Permission required for package to broadcast events to the Dock."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Dock broadcast receiver"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Permission required for package listen to broadcast events for the Dock."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"No spot available to pin"</string>
+</resources>
diff --git a/docklib/res/values-en-rGB/strings.xml b/docklib/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..6882693
--- /dev/null
+++ b/docklib/res/values-en-rGB/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Dock broadcast sender"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Permission required for package to broadcast events to the dock."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Dock broadcast receiver"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Permission required for package to listen to broadcast events for the dock."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"No spot available to pin"</string>
+</resources>
diff --git a/docklib/res/values-en-rIN/strings.xml b/docklib/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..6882693
--- /dev/null
+++ b/docklib/res/values-en-rIN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Dock broadcast sender"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Permission required for package to broadcast events to the dock."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Dock broadcast receiver"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Permission required for package to listen to broadcast events for the dock."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"No spot available to pin"</string>
+</resources>
diff --git a/docklib/res/values-en-rXC/strings.xml b/docklib/res/values-en-rXC/strings.xml
index a48b2a2..d88ef4a 100644
--- a/docklib/res/values-en-rXC/strings.xml
+++ b/docklib/res/values-en-rXC/strings.xml
@@ -21,4 +21,5 @@
<string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Permission required for package to broadcast events to the Dock."</string>
<string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Dock broadcast receiver"</string>
<string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Permission required for package listen to broadcast events for the Dock."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"No spot available to pin"</string>
</resources>
diff --git a/docklib/res/values-es-rUS/strings.xml b/docklib/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..8d2bde2
--- /dev/null
+++ b/docklib/res/values-es-rUS/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Remitente de la transmisión del conector"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Se requiere permiso para que el paquete transmita eventos al conector."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Receptor de transmisiones del conector"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Se requiere permiso para que el paquete escuche los eventos de transmisión para el conector."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"No hay una ubicación disponible para fijar"</string>
+</resources>
diff --git a/docklib/res/values-es/strings.xml b/docklib/res/values-es/strings.xml
new file mode 100644
index 0000000..46d11c1
--- /dev/null
+++ b/docklib/res/values-es/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Emisor de las aplicaciones ancladas"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Se necesita permiso para que el paquete emita eventos a las aplicaciones ancladas."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Receptor de emisión de las aplicaciones ancladas"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Se necesita permiso para que el paquete escuche los eventos de emisión para las aplicaciones ancladas."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"No hay espacio disponible para fijar"</string>
+</resources>
diff --git a/docklib/res/values-et/strings.xml b/docklib/res/values-et/strings.xml
new file mode 100644
index 0000000..a884cc4
--- /dev/null
+++ b/docklib/res/values-et/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Doki ülekande saatja"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Pakett vajab sündmuste dokki edastamiseks luba."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Doki ülekande vastuvõtja"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Pakett vajab sündmuste kuulamiseks doki jaoks edastamise luba."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Kinnitamiseks pole kohta saadaval"</string>
+</resources>
diff --git a/docklib/res/values-eu/strings.xml b/docklib/res/values-eu/strings.xml
new file mode 100644
index 0000000..0f4de1f
--- /dev/null
+++ b/docklib/res/values-eu/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Oinarrira igortzeko igorlea"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Paketeak oinarrira gertaerak igortzeko baimena behar du."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Oinarriaren igorpen-hargailua"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Entzuteko paketeak oinarrira gertaerak igortzeko baimena behar du."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Ez dago ainguratzeko tokirik erabilgarri"</string>
+</resources>
diff --git a/docklib/res/values-fa/strings.xml b/docklib/res/values-fa/strings.xml
new file mode 100644
index 0000000..c16d50b
--- /dev/null
+++ b/docklib/res/values-fa/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"فرستنده رویداد ثبتشده در «پایه اتصال»"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"برای اینکه بسته رویدادها را به «پایه اتصال» همهفرستی کند به اجازه نیاز است."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"گیرنده رویداد ثبتشده در «پایه اتصال»"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"برای اینکه بسته رویدادها را برای «پایه اتصال» همهفرستی کند به اجازه نیاز است."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"فضایی برای سنجاق کردن وجود ندارد"</string>
+</resources>
diff --git a/docklib/res/values-fi/strings.xml b/docklib/res/values-fi/strings.xml
new file mode 100644
index 0000000..2f0e89a
--- /dev/null
+++ b/docklib/res/values-fi/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Telineen lähetyksen lähde"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Tarvitaan lupa, jotta paketti voi lähettää tapahtumia telineeseen."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Telineen lähetysvastaanotin"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Paketti tarvitsee luvan kuunteluun, jotta se voi lähettää tapahtumia telineeseen."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Ei kohtaa, johon sovelluksen voi kiinnittää"</string>
+</resources>
diff --git a/docklib/res/values-fr-rCA/strings.xml b/docklib/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..a578384
--- /dev/null
+++ b/docklib/res/values-fr-rCA/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Émetteur de diffusion vers la barre d\'applications"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Autorisation requise pour le paquet afin de diffuser des événements sur la barre d\'applications."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Récepteur de diffusion vers la barre d\'applications"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Autorisation requise pour le paquet afin de diffuser des événements sur la barre d\'applications."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Pas de place libre pour l\'épinglage"</string>
+</resources>
diff --git a/docklib/res/values-fr/strings.xml b/docklib/res/values-fr/strings.xml
new file mode 100644
index 0000000..0110ef9
--- /dev/null
+++ b/docklib/res/values-fr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Expéditeur de diffusion sur la barre des applications"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Autorisation requise pour le package afin de diffuser des événements sur la barre des applications."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Broadcast receiver sur la barre des applications"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Autorisation requise pour que le paquet puisse écouter les événements diffusés pour la barre des applications."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Aucun espace disponible pour épingler"</string>
+</resources>
diff --git a/docklib/res/values-gl/strings.xml b/docklib/res/values-gl/strings.xml
new file mode 100644
index 0000000..71a235a
--- /dev/null
+++ b/docklib/res/values-gl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Emisor de difusións da barra de aplicacións"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Requírese permiso para que o paquete poida difundir eventos á barra de aplicacións."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Receptor de difusións da barra de aplicacións"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Requírese permiso para que o paquete poida escoitar co fin de difundir eventos á barra de aplicacións."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Non hai ningún oco para fixar o contido"</string>
+</resources>
diff --git a/docklib/res/values-gu/strings.xml b/docklib/res/values-gu/strings.xml
new file mode 100644
index 0000000..f7b5bd6
--- /dev/null
+++ b/docklib/res/values-gu/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"ડૉક પર બ્રોડકાસ્ટ મોકલનાર"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"ડૉક પર ઇવેન્ટ બ્રોડકાસ્ટ કરવા, પૅકેજ માટે પરવાનગી આવશ્યક છે."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"ડૉક પર બ્રોડકાસ્ટ પ્રાપ્તકર્તા"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"ડૉક પર ઇવેન્ટ બ્રોડકાસ્ટ કરવા, પૅકેજ સાંભળવા માટેની પરવાનગી આવશ્યક છે."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"પિન કરવા માટે, કોઈ સ્પૉટ ઉપલબ્ધ નથી"</string>
+</resources>
diff --git a/docklib/res/values-hi/strings.xml b/docklib/res/values-hi/strings.xml
new file mode 100644
index 0000000..a186467
--- /dev/null
+++ b/docklib/res/values-hi/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"डॉक ब्रॉडकास्ट सेंडर"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"डॉक पर इवेंट ब्रॉडकास्ट करने के लिए, पैकेज को अनुमति चाहिए."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"डॉक ब्रॉडकास्ट रिसीवर"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"डॉक पर इवेंट ब्रॉडकास्ट करने के लिए, पैकेज को अनुमति चाहिए."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"पिन करने के लिए कोई जगह नहीं है"</string>
+</resources>
diff --git a/docklib/res/values-hr/strings.xml b/docklib/res/values-hr/strings.xml
new file mode 100644
index 0000000..758523d
--- /dev/null
+++ b/docklib/res/values-hr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Pošiljatelj emitiranja na priključnu stanicu"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Potrebno je dopuštenje da paket emitira događaje na priključnu stanicu."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Prijamnik emitiranja za priključnu stanicu"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Dopuštenje za slušanje paketa za emitiranje događaja za priključnu stanicu."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Nema dostupnog mjesta za prikvačiti"</string>
+</resources>
diff --git a/docklib/res/values-hu/strings.xml b/docklib/res/values-hu/strings.xml
new file mode 100644
index 0000000..700d6e1
--- /dev/null
+++ b/docklib/res/values-hu/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Dokk-közvetítés forrása"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Engedély szükséges a csomag számára, hogy eseményeket közvetíthessen a dokkra."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Dokk broadcast receivere"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Engedély szükséges a csomag számára, hogy figyelhesse a dokkhoz kapcsolódó közvetítési eseményeket."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Nincs rendelkezésre álló hely a kitűzéshez"</string>
+</resources>
diff --git a/docklib/res/values-hy/strings.xml b/docklib/res/values-hy/strings.xml
new file mode 100644
index 0000000..ed058e1
--- /dev/null
+++ b/docklib/res/values-hy/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Դոկ-կայանին բովանդակություն հեռարձակող"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Թույլտվություն, որն անհրաժեշտ է իրադարձությունների մասին տեղեկությունները դոկ-կայանին հեռարձակելու համար։"</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Դոկ-կայանինից հեռարձակման ընդունիչ"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Թույլտվություն, որն անհրաժեշտ է իրադարձությունների մասին՝ դոկ-կայանից հեռարձակվող տեղեկությունները լսելու համար։"</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Ամրացնելու համար տեղ չկա"</string>
+</resources>
diff --git a/docklib/res/values-in/strings.xml b/docklib/res/values-in/strings.xml
new file mode 100644
index 0000000..729865d
--- /dev/null
+++ b/docklib/res/values-in/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Pengirim siaran Dok"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Izin diperlukan agar paket dapat menyiarkan acara ke Dok."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Penerima siaran dok"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Izin diperlukan agar paket dapat memproses siaran acara untuk Dok."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Tidak ada tempat yang tersedia untuk menyematkan"</string>
+</resources>
diff --git a/docklib/res/values-is/strings.xml b/docklib/res/values-is/strings.xml
new file mode 100644
index 0000000..597930c
--- /dev/null
+++ b/docklib/res/values-is/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Útsendir dokku"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Heimildar er krafist svo pakkinn geti sent út viðburði í dokku."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Móttakari útsendinga á dokku"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Heimildar er krafist svo pakkinn geti hlustað á útsenda viðburði í dokku."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Enginn staður tiltækur til að festa"</string>
+</resources>
diff --git a/docklib/res/values-it/strings.xml b/docklib/res/values-it/strings.xml
new file mode 100644
index 0000000..31ee89c
--- /dev/null
+++ b/docklib/res/values-it/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Broadcast sender del dock"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Autorizzazione necessaria per consentire al pacchetto di trasmettere eventi al dock."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Broadcast receiver del dock"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Autorizzazione necessaria per l\'ascolto del pacchetto al fine di trasmettere eventi per il dock."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Non c\'è posto per fissare"</string>
+</resources>
diff --git a/docklib/res/values-iw/strings.xml b/docklib/res/values-iw/strings.xml
new file mode 100644
index 0000000..4599c73
--- /dev/null
+++ b/docklib/res/values-iw/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"ספק השידורים למגש האפליקציות"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"נדרשת הרשאה כדי שהחבילה תוכל לשדר אירועים למגש האפליקציות."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"מקלט השידורים של מגש האפליקציות"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"נדרשת הרשאה כדי שהחבילה תוכל לקלוט אירועים שישודרו למגש האפליקציות."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"אין מקום פנוי להצמדה"</string>
+</resources>
diff --git a/docklib/res/values-ja/strings.xml b/docklib/res/values-ja/strings.xml
new file mode 100644
index 0000000..e983846
--- /dev/null
+++ b/docklib/res/values-ja/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"ホルダー ブロードキャスト送信側"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"イベントをホルダーにブロードキャストする権限がパッケージに必要です"</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"ホルダー ブロードキャスト受信側"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"ホルダーのブロードキャスト イベントをリッスンする権限がパッケージに必要です"</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"固定できる場所がありません"</string>
+</resources>
diff --git a/docklib/res/values-ka/strings.xml b/docklib/res/values-ka/strings.xml
new file mode 100644
index 0000000..05116e7
--- /dev/null
+++ b/docklib/res/values-ka/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"სამაგრის მაუწყებლობის გამგზავნი"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"პაკეტისთვის საჭიროა ნებართვა სამაგრში ღონისძიებების ტრანსლაციისთვის."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"სამაგრის მაუწყებლობათა მიმღები"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"პაკეტისთვის საჭიროა ნებართვა სამაგრში ღონისძიებების ტრანსლაციისთვის."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"ჩასამაგრებელი ადგილი მიუწვდომელია"</string>
+</resources>
diff --git a/docklib/res/values-kk/strings.xml b/docklib/res/values-kk/strings.xml
new file mode 100644
index 0000000..605c2d1
--- /dev/null
+++ b/docklib/res/values-kk/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Қондыру станциясының таратушысы"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Іс-шараларды қондыру станциясына тарату үшін пакетке рұқсат қажет."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Қондыру станциясының қабылдағышы"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Қондыру станциясындағы іс-шаралар жайлы ақпарат алу үшін пакетке рұқсат қажет."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Бекіту үшін ешқандай орын жоқ."</string>
+</resources>
diff --git a/docklib/res/values-km/strings.xml b/docklib/res/values-km/strings.xml
new file mode 100644
index 0000000..b6fbe0d
--- /dev/null
+++ b/docklib/res/values-km/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"កម្មវិធីបញ្ជូនការផ្សាយរបស់ធ្នើរ"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"ការអនុញ្ញាតដែលតម្រូវឱ្យមានសម្រាប់កញ្ចប់ ដើម្បីផ្សាយព្រឹត្តិការណ៍ទៅធ្នើរ។"</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"កម្មវិធីទទួលការផ្សាយរបស់ធ្នើរ"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"ការអនុញ្ញាតដែលតម្រូវឱ្យមានសម្រាប់ការស្ដាប់កញ្ចប់ ដើម្បីផ្សាយព្រឹត្តិការណ៍សម្រាប់ធ្នើរ។"</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"មិនមានកន្លែងដែលអាចខ្ទាស់បានទេ"</string>
+</resources>
diff --git a/docklib/res/values-kn/strings.xml b/docklib/res/values-kn/strings.xml
new file mode 100644
index 0000000..f26435b
--- /dev/null
+++ b/docklib/res/values-kn/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"ಡಾಕ್ ಪ್ರಸಾರವನ್ನು ಕಳುಹಿಸಿದವರು"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"ಈವೆಂಟ್ಗಳನ್ನು ಡಾಕ್ಗೆ ಪ್ರಸಾರ ಮಾಡಲು ಪ್ಯಾಕೇಜ್ಗೆ ಅನುಮತಿಯ ಅಗತ್ಯವಿದೆ."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"ಡಾಕ್ ಪ್ರಸಾರ ಸ್ವೀಕರಿಸುವವರು"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"ಈವೆಂಟ್ಗಳನ್ನು ಡಾಕ್ಗೆ ಪ್ರಸಾರ ಮಾಡಲು ಪ್ಯಾಕೇಜ್ ಆಲಿಸುವಿಕೆಗೆ ಅನುಮತಿಯ ಅಗತ್ಯವಿದೆ."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"ಪಿನ್ ಮಾಡಲು ಯಾವುದೇ ಸ್ಪಾಟ್ ಲಭ್ಯವಿಲ್ಲ"</string>
+</resources>
diff --git a/docklib/res/values-ko/strings.xml b/docklib/res/values-ko/strings.xml
new file mode 100644
index 0000000..97067b4
--- /dev/null
+++ b/docklib/res/values-ko/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"broadcast receiver 도킹"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"패키지에서 이벤트를 도크로 브로드캐스트하려면 권한이 필요합니다."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"broadcast receiver 도킹"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"패키지에서 도크를 위한 이벤트를 브로드캐스트하려면 리슨 권한이 필요합니다."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"고정할 수 있는 공간 없음"</string>
+</resources>
diff --git a/docklib/res/values-ky/strings.xml b/docklib/res/values-ky/strings.xml
new file mode 100644
index 0000000..cd65459
--- /dev/null
+++ b/docklib/res/values-ky/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Док бекетке кабарды жөнөтүүчү"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Иш-чараларды Док бекетке кабарлоо үчүн таңгакка уруксат талап кылынат."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Док бекеттеги кең диапазондогу кабыл алгыч"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Иш-чараларды Док бекетте угуу үчүн таңгакка уруксат талап кылынат."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Кадап коюу үчүн бош жер жок"</string>
+</resources>
diff --git a/docklib/res/values-lo/strings.xml b/docklib/res/values-lo/strings.xml
new file mode 100644
index 0000000..345a58d
--- /dev/null
+++ b/docklib/res/values-lo/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"ຕົວສົ່ງປະກາດໄປຫາດັອກ"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"ຕ້ອງມີການອະນຸຍາດສຳລັບແພັກເກດເພື່ອປະກາດເຫດການໄປຫາດັອກ."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"ຕົວຮັບປະກາດຈາກດັອກ"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"ຕ້ອງມີການອະນຸຍາດສຳລັບການຟັງແພັກເກດເພື່ອປະກາດເຫດການສຳລັບດັອກ."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"ບໍ່ມີບ່ອນໃຫ້ປັກໝຸດ"</string>
+</resources>
diff --git a/docklib/res/values-lt/strings.xml b/docklib/res/values-lt/strings.xml
new file mode 100644
index 0000000..872a83a
--- /dev/null
+++ b/docklib/res/values-lt/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Prisegtų programų juostos transliacijos siuntėjas"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Paketui reikalingas leidimas transliuoti įvykius į prisegtų programų juostą."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Prisegtų programų juostos transliacijos gavėjas"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Paketui reikalingas leidimas priimti transliacijos įvykius, skirtus prisegtų programų juostai."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Nėra vietos, kad būtų galima prisegti"</string>
+</resources>
diff --git a/docklib/res/values-lv/strings.xml b/docklib/res/values-lv/strings.xml
new file mode 100644
index 0000000..df54fdd
--- /dev/null
+++ b/docklib/res/values-lv/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Doka apraides sūtītājs"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Pakotnei ir nepieciešama atļauja, lai varētu apraidīt notikumus uz doku."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Doka apraides uztvērējs"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Pakotnei ir nepieciešama klausīšanās atļauja, lai varētu apraidīt notikumus uz doku."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Nav brīvas vietas, kur piespraust"</string>
+</resources>
diff --git a/docklib/res/values-mk/strings.xml b/docklib/res/values-mk/strings.xml
new file mode 100644
index 0000000..868bcbe
--- /dev/null
+++ b/docklib/res/values-mk/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Испраќач за емитувања на Dock"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Потребна е дозвола за пакетот да емитува настани на Dock."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Приемник за емитување на Dock"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Потребна е дозвола за пакетот за слушање да емитува настани за Dock."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Нема достапно место за закачување"</string>
+</resources>
diff --git a/docklib/res/values-ml/strings.xml b/docklib/res/values-ml/strings.xml
new file mode 100644
index 0000000..476c444
--- /dev/null
+++ b/docklib/res/values-ml/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"ഡോക്ക് ബ്രോഡ്കാസ്റ്റ് സെൻഡർ"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"ഡോക്കിലേക്ക് ഇവന്റുകൾ ബ്രോഡ്കാസ്റ്റ് ചെയ്യാൻ പാക്കേജിന് അനുമതി ആവശ്യമാണ്."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"ഡോക്ക് ബ്രോഡ്കാസ്റ്റ് റിസീവർ"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"ഡോക്കിലേക്ക് ഇവന്റുകൾ ബ്രോഡ്കാസ്റ്റ് ചെയ്യുന്നതിന്, പാക്കേജ് കേൾക്കൽ അനുമതി ആവശ്യമാണ്."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"പിൻ ചെയ്യാൻ സ്പോട്ട് ഒന്നും ലഭ്യമല്ല"</string>
+</resources>
diff --git a/docklib/res/values-mn/strings.xml b/docklib/res/values-mn/strings.xml
new file mode 100644
index 0000000..c0bbb0c
--- /dev/null
+++ b/docklib/res/values-mn/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Суурилуулагчийн дамжуулалт илгээгч"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Суурилуулагч руу үйл явдлуудыг дамжуулахад багцад шаардлагатай зөвшөөрөл."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Суурилуулагчийн дамжуулалт хүлээн авагч"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Суурилуулагчид үйл явдлуудыг дамжуулахад багцад шаардлагатай зөвшөөрөл."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Бэхлэх зай байхгүй байна"</string>
+</resources>
diff --git a/docklib/res/values-mr/strings.xml b/docklib/res/values-mr/strings.xml
new file mode 100644
index 0000000..35bc2f5
--- /dev/null
+++ b/docklib/res/values-mr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"डॉक ब्रॉडकास्ट पाठवणारा"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"डॉक वर इव्हेंट ब्रॉडकास्ट करण्यासाठी पॅकेजला परवानगी हवी आहे."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"डॉक ब्रॉडकास्ट प्राप्तकर्ता"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"डॉक साठीचे ब्रॉडकास्ट इव्हेंट ऐकण्याकरिता पॅकेजला परवानगी हवी आहे."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"पिन करण्यासाठी जागा उपलब्ध नाही"</string>
+</resources>
diff --git a/docklib/res/values-ms/strings.xml b/docklib/res/values-ms/strings.xml
new file mode 100644
index 0000000..72d0826
--- /dev/null
+++ b/docklib/res/values-ms/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Pengirim siaran dok"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Kebenaran diperlukan untuk membolehkan pakej menyiarkan acara pada Dok."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Penerima siaran dok"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Kebenaran diperlukan bagi pendengaran pakej untuk menyiarkan acara bagi Dok."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Tiada tempat yang tersedia untuk disemat"</string>
+</resources>
diff --git a/docklib/res/values-my/strings.xml b/docklib/res/values-my/strings.xml
new file mode 100644
index 0000000..9c53da2
--- /dev/null
+++ b/docklib/res/values-my/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"အထိုင်ဆိုင်ရာ ထုတ်လွှင့်မှုပို့သောစနစ်"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"‘အထိုင်’ သို့ အစီအစဉ်များ ထုတ်လွှင့်ရန် ပက်ကေ့ဂျ်အတွက် လိုအပ်သည့် ခွင့်ပြုချက်။"</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"အထိုင်ဆိုင်ရာ အသံလွှင့်ခြင်းကို လက်ခံသည့်စနစ်"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"အထိုင်တွင် ထုတ်လွှင့်မှုအစီအစဉ်များ နားထောင်ရန် ပက်ကေ့ဂျ်အတွက် လိုအပ်သည့် ခွင့်ပြုချက်။"</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"ပင်ထိုးရန် နေရာမရှိပါ"</string>
+</resources>
diff --git a/docklib/res/values-nb/strings.xml b/docklib/res/values-nb/strings.xml
new file mode 100644
index 0000000..ac2e0d3
--- /dev/null
+++ b/docklib/res/values-nb/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Sender for kringkasting til dokken"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Tillatelse som kreves for at pakker skal kunne kringkaste hendelser til dokken."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Mottaker for kringkasting til dokken"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Tillatelse som kreves for at pakker skal kunne lytte etter kringkastede hendelser for dokken."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Det finnes ingen steder der appen kan festes"</string>
+</resources>
diff --git a/docklib/res/values-ne/strings.xml b/docklib/res/values-ne/strings.xml
new file mode 100644
index 0000000..2470c03
--- /dev/null
+++ b/docklib/res/values-ne/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"डक ब्रोडकास्ट सेन्डर"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"डकमा गतिविधिसम्बन्धी सूचनाहरू पठाउन प्याकेजलाई अनुमति चाहिन्छ।"</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"डक ब्रोडकास्ट रिसिभर"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"डकले पठाएका गतिविधिसम्बन्धी सूचनाहरू प्राप्त गर्न प्याकेजलाई अनुमति चाहिन्छ।"</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"पिन गर्नका लागि कुनै पनि ठाउँ छैन"</string>
+</resources>
diff --git a/docklib/res/values-nl/strings.xml b/docklib/res/values-nl/strings.xml
new file mode 100644
index 0000000..74d18c6
--- /dev/null
+++ b/docklib/res/values-nl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Transmissieverzender voor dock"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Toestemming nodig voordat pakket gebeurtenissen kan uitzenden naar het dockstation."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Dockontvanger"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Toestemming nodig voordat pakket kan luisteren naar transmissiegebeurtenissen voor het dock."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Geen plaats beschikbaar voor vastzetten"</string>
+</resources>
diff --git a/docklib/res/values-or/strings.xml b/docklib/res/values-or/strings.xml
new file mode 100644
index 0000000..5760ab1
--- /dev/null
+++ b/docklib/res/values-or/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"ଡକ ବ୍ରଡକାଷ୍ଟ ପ୍ରେରକ"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"ଡକରେ ଇଭେଣ୍ଟଗୁଡ଼ିକ ବ୍ରଡକାଷ୍ଟ କରିବା ପାଇଁ ପେକେଜକୁ ଅନୁମତିର ଆବଶ୍ୟକତା ଅଛି।"</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"ଡକ ବ୍ରଡକାଷ୍ଟ ରିସିଭର"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"ଡକ ପାଇଁ ଇଭେଣ୍ଟଗୁଡ଼ିକ ବ୍ରଡକାଷ୍ଟ କରିବାକୁ ଶୁଣିବା ନିମନ୍ତେ ପେକେଜକୁ ଅନୁମତିର ଆବଶ୍ୟକତା ଅଛି।"</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"ପିନ କରିବାକୁ କୌଣସି ସ୍ଥାନ ଉପଲବ୍ଧ ନାହିଁ"</string>
+</resources>
diff --git a/docklib/res/values-pa/strings.xml b/docklib/res/values-pa/strings.xml
new file mode 100644
index 0000000..c8228d4
--- /dev/null
+++ b/docklib/res/values-pa/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"ਡੌਕ ਪ੍ਰਸਾਰਨ ਭੇਜਣ ਵਾਲਾ"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"ਡੌਕ \'ਤੇ ਇਵੈਂਟਾਂ ਦਾ ਪ੍ਰਸਾਰਨ ਕਰਨ ਲਈ ਪੈਕੇਜ ਵਾਸਤੇ ਇਜਾਜ਼ਤ ਦੀ ਲੋੜ ਹੈ।"</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"ਡੌਕ broadcast receiver"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"ਡੌਕ \'ਤੇ ਇਵੈਂਟਾਂ ਦਾ ਪ੍ਰਸਾਰਨ ਕਰਨ ਲਈ ਪੈਕੇਜ ਵਾਸਤੇ ਇਜਾਜ਼ਤ ਦੀ ਲੋੜ ਹੈ।"</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"ਪਿੰਨ ਕਰਨ ਲਈ ਕੋਈ ਥਾਂ ਉਪਲਬਧ ਨਹੀਂ ਹੈ"</string>
+</resources>
diff --git a/docklib/res/values-pl/strings.xml b/docklib/res/values-pl/strings.xml
new file mode 100644
index 0000000..d4ce0f9
--- /dev/null
+++ b/docklib/res/values-pl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Moduł wysyłający transmisję na stację dokującą"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Pakiet potrzebuje pozwolenia na transmitowanie wydarzeń na stacji dokującej."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Odbiornik stacji dokującej"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Pakiet potrzebuje pozwolenia na odtwarzanie transmitowanych wydarzeń w przypadku stacji dokującej."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Brak miejsca do przypięcia"</string>
+</resources>
diff --git a/docklib/res/values-pt-rPT/strings.xml b/docklib/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..1841bf5
--- /dev/null
+++ b/docklib/res/values-pt-rPT/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Remetente de transmissão da estação de ancoragem"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"O pacote precisa de autorização para transmitir eventos para a estação de ancoragem."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Recetor de transmissão da estação de ancoragem"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"O pacote precisa de autorização para ouvir eventos de transmissão para a estação de ancoragem."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Nenhum local disponível para afixar"</string>
+</resources>
diff --git a/docklib/res/values-pt/strings.xml b/docklib/res/values-pt/strings.xml
new file mode 100644
index 0000000..613f06a
--- /dev/null
+++ b/docklib/res/values-pt/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Remetente da transmissão para a base"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"O pacote precisa de permissão para transmitir eventos para a base."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Broadcast receiver da base"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"O pacote precisa de permissão para ouvir eventos de transmissão para a base."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Nenhum espaço disponível para fixar"</string>
+</resources>
diff --git a/docklib/res/values-ro/strings.xml b/docklib/res/values-ro/strings.xml
new file mode 100644
index 0000000..9b603a7
--- /dev/null
+++ b/docklib/res/values-ro/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Broadcast sender pentru bara de activități"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Permisiune necesară pentru ca pachetul să transmită evenimente către bara de activități."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Broadcast receiver pentru bara de activități"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Permisiune necesară pentru ca pachetul să asculte evenimente transmise pentru bara de activități."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Nu există un loc disponibil de fixat"</string>
+</resources>
diff --git a/docklib/res/values-ru/strings.xml b/docklib/res/values-ru/strings.xml
new file mode 100644
index 0000000..95a7268
--- /dev/null
+++ b/docklib/res/values-ru/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Широковещательный передатчик для док-станции"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Чтобы отправлять информацию о событиях на док-станцию, передатчику требуется разрешение."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Широковещательный приемник для док-станции"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Чтобы получать информацию о событиях на док-станции, приемнику требуется разрешение."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Нет места"</string>
+</resources>
diff --git a/docklib/res/values-si/strings.xml b/docklib/res/values-si/strings.xml
new file mode 100644
index 0000000..1d3cc91
--- /dev/null
+++ b/docklib/res/values-si/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"ඩොක් විකාශන යවන්නා"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"ඩොකය වෙත සිදුවීම් විකාශනය කිරීමට පැකේජය සඳහා අවසරය අවශ්යයි."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"ඩොක් විකාශන ග්රාහකය"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"ඩොකය සඳහා විකාශන සිදුවීම් වෙත සවන්දීම සඳහා අවසරය අවශ්යයි."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"ඇමිණීමට ස්ථානයක් නොමැත"</string>
+</resources>
diff --git a/docklib/res/values-sk/strings.xml b/docklib/res/values-sk/strings.xml
new file mode 100644
index 0000000..16db20f
--- /dev/null
+++ b/docklib/res/values-sk/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Broadcast sender pre dok"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Na to, aby balíček mohol vysielať udalosti do doku, potrebuje povolenie."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Broadcast receiver pre dok"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Na to, aby balíček mohol počúvať udalosti vysielania do doku, potrebuje povolenie."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Žiadne dostupné miesto na pripnutie"</string>
+</resources>
diff --git a/docklib/res/values-sl/strings.xml b/docklib/res/values-sl/strings.xml
new file mode 100644
index 0000000..41d7f7e
--- /dev/null
+++ b/docklib/res/values-sl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Orodje za objavljanje v vrstici z aplikacijami"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Paket potrebuje dovoljenje za objavljanje dogodkov v vrstici z aplikacijami."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Sprejemnik za objavljanje v vrstici z aplikacijami"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Poslušanje paketov potrebuje dovoljenje za objavljanje dogodkov za vrstico z aplikacijami."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Ni mesta za pripenjanje"</string>
+</resources>
diff --git a/docklib/res/values-sq/strings.xml b/docklib/res/values-sq/strings.xml
new file mode 100644
index 0000000..870fff2
--- /dev/null
+++ b/docklib/res/values-sq/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Dërguesi i transmetimit të \"Stacionit\""</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Kërkohet leje që paketa të transmetojë ngjarjet te \"Stacioni\"."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Marrësi i transmetimit të \"Stacionit\""</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Kërkohet leje që paketa të dëgjojë ngjarjet e transmetimit për \"Stacionin\"."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Nuk ka vend për të gozhduar"</string>
+</resources>
diff --git a/docklib/res/values-sr/strings.xml b/docklib/res/values-sr/strings.xml
new file mode 100644
index 0000000..5a1457a
--- /dev/null
+++ b/docklib/res/values-sr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Пошиљалац емитовања за траку с апликацијама"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Потребна је дозвола да би пакет емитовао догађаје за траку с апликацијама."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Пријемник емитовања за траку с апликацијама"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Потребна је дозвола да би ослушкивач пакета емитовао догађаје за траку с апликацијама."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Није доступно место за качење"</string>
+</resources>
diff --git a/docklib/res/values-sv/strings.xml b/docklib/res/values-sv/strings.xml
new file mode 100644
index 0000000..42f8c6f
--- /dev/null
+++ b/docklib/res/values-sv/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Utsändare till appraden"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Paketet behöver behörighet för att sända ut händelser till appraden."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Sändningsmottagare för appraden"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Behörighet krävs för att paketet ska kunna ta emot utsända händelser för appraden"</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Det finns ingen plats att fästa"</string>
+</resources>
diff --git a/docklib/res/values-sw/strings.xml b/docklib/res/values-sw/strings.xml
new file mode 100644
index 0000000..f9588d1
--- /dev/null
+++ b/docklib/res/values-sw/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Mtumaji wa arifa ya Dock"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Ruhusa inahitajika ili kifurushi kitoe arifa za matukio kwenye Dock."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Kipokeaji cha matangazo ya Dock"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Inahitaji ruhusa ya kipengele cha kusikiliza kifurushi ili kusambaza matukio ya Dock."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Hakuna nafasi ya kubandika"</string>
+</resources>
diff --git a/docklib/res/values-ta/strings.xml b/docklib/res/values-ta/strings.xml
new file mode 100644
index 0000000..f90c400
--- /dev/null
+++ b/docklib/res/values-ta/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"டாக் பிராட்காஸ்ட் அனுப்புநர்"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"டாக்கில் நிகழ்வுகளை பிராட்காஸ்ட் செய்ய பேக்கேஜிற்கு அனுமதி தேவை."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"டாக் பிராட்காஸ்ட் பெறுநர்"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"டாக்கில் நிகழ்வுகளை பிராட்காஸ்ட் செய்ய பேக்கேஜ் லிஸனிற்கு அனுமதி தேவை."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"பின் செய்ய எதுவுமில்லை"</string>
+</resources>
diff --git a/docklib/res/values-te/strings.xml b/docklib/res/values-te/strings.xml
new file mode 100644
index 0000000..5e0beb0
--- /dev/null
+++ b/docklib/res/values-te/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"డాక్ ప్రసారం పంపే వారు"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"డాక్కి ఈవెంట్లను ప్రసారం చేయడానికి ప్యాకేజీకి అనుమతి అవసరం."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"డాక్ ప్రసార రిసీవర్"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"డాక్ చేయడం కోసం ప్రసార ఈవెంట్లను వినడానికి ప్యాకేజీకి అనుమతి అవసరం."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"పిన్ చేయడానికి స్పాట్ లేదు"</string>
+</resources>
diff --git a/docklib/res/values-th/strings.xml b/docklib/res/values-th/strings.xml
new file mode 100644
index 0000000..2cbe132
--- /dev/null
+++ b/docklib/res/values-th/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Broadcast Sender ของ Dock"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"ต้องมีสิทธิ์สำหรับแพ็กเกจเพื่อประกาศเหตุการณ์ไปยัง Dock"</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Broadcast Receiver ของ Dock"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"ต้องมีสิทธิ์สำหรับแพ็กเกจเพื่อฟังประกาศเหตุการณ์ที่ Dock"</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"ไม่มีพื้นที่ว่างให้ปักหมุด"</string>
+</resources>
diff --git a/docklib/res/values-tl/strings.xml b/docklib/res/values-tl/strings.xml
new file mode 100644
index 0000000..0fe93b2
--- /dev/null
+++ b/docklib/res/values-tl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Sender ng broadcast sa Dock"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Kinakailangan ang pahintulot para makapag-broadcast ng mga event sa Dock ang package."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Tagatanggap ng broadcast ng Dock"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Kinakailangan ang pahintulot para makarinig sa broadcast ng mga event sa Dock ang package."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Walang lugar na available para i-pin"</string>
+</resources>
diff --git a/docklib/res/values-tr/strings.xml b/docklib/res/values-tr/strings.xml
new file mode 100644
index 0000000..783298b
--- /dev/null
+++ b/docklib/res/values-tr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Yuva yayını gönderici"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Etkinlikleri Yuvaya yayınlama paketi için izin gerekiyor."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Yuva yayını alıcı"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Paketin Yuva ile ilgili yayınlanan etkinlikleri dinlemesi için izin gerekiyor."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Sabitlemek için yer yok"</string>
+</resources>
diff --git a/docklib/res/values-uk/strings.xml b/docklib/res/values-uk/strings.xml
new file mode 100644
index 0000000..93724f8
--- /dev/null
+++ b/docklib/res/values-uk/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Відправник широкомовної розсилки на закріплену панель"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Пакету потрібен дозвіл, щоб здійснювати широкомовну розсилку подій на закріплену панель."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Приймач широкомовної розсилки на закріпленій панелі"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Пакету потрібен дозвіл, щоб слухати широкомовну розсилку подій на закріпленій панелі."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Немає місця, щоб закріпити"</string>
+</resources>
diff --git a/docklib/res/values-ur/strings.xml b/docklib/res/values-ur/strings.xml
new file mode 100644
index 0000000..ce2bbcf
--- /dev/null
+++ b/docklib/res/values-ur/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"ڈاک براڈ کاسٹ ارسال کنندہ"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"ڈاک پر ایونٹس کو براڈ کاسٹ کرنے کی خاطر پیکیج کیلئے اجازت درکار ہے۔"</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"ڈاک براڈکاسٹ وصول کنندہ"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"پیکیج کے لیے اجازت درکار ہے ڈاک کے لیے براڈ کاسٹ ایونٹس سنیں۔"</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"پن کرنے کے لیے کوئی جگہ دستیاب نہیں ہے"</string>
+</resources>
diff --git a/docklib/res/values-uz/strings.xml b/docklib/res/values-uz/strings.xml
new file mode 100644
index 0000000..0f23cbc
--- /dev/null
+++ b/docklib/res/values-uz/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Dok-stansiyaga maʼlumotlarni yuboruvchi"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Hodisa haqidagi maʼlumotlarni dok-stansiyaga yuborish uchun ilova ruxsat talab qiladi"</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Dok translatsiya resiveri"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Dokka hodisalarni uzatishda paket tinglash uchun ruxsat talab etiladi."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Mahkamlash uchun joy qolmagan"</string>
+</resources>
diff --git a/docklib/res/values-vi/strings.xml b/docklib/res/values-vi/strings.xml
new file mode 100644
index 0000000..5a3f807
--- /dev/null
+++ b/docklib/res/values-vi/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Broadcast sender của thanh Dock ứng dụng"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Cần có quyền để gói ứng dụng truyền sự kiện lên thanh Dock ứng dụng."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Broadcast receiver của thanh Dock ứng dụng"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Cần có quyền để gói ứng dụng nghe được sự kiện truyền lên thanh Dock ứng dụng."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Không còn chỗ nào để ghim"</string>
+</resources>
diff --git a/docklib/res/values-zh-rCN/strings.xml b/docklib/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..7d3df20
--- /dev/null
+++ b/docklib/res/values-zh-rCN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"基座广播发送器"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"您必须授予权限,软件包才能将事件广播到基座。"</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"基座广播接收器"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"您必须授予权限,软件包监听才能广播针对基座的事件。"</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"没有可固定的位置"</string>
+</resources>
diff --git a/docklib/res/values-zh-rHK/strings.xml b/docklib/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..81b428c
--- /dev/null
+++ b/docklib/res/values-zh-rHK/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"捷徑列廣播傳送器"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"檔案包必須獲得權限,才能在捷徑列廣播活動。"</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"捷徑列廣播接收器"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"檔案包必須獲得權限,才能在捷徑列廣播活動。"</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"沒有可用的位置固定"</string>
+</resources>
diff --git a/docklib/res/values-zh-rTW/strings.xml b/docklib/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..9ad0f02
--- /dev/null
+++ b/docklib/res/values-zh-rTW/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"工具列廣播傳送器"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"必須授予權限,套件才能將事件播送到工具列。"</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"工具列廣播接收器"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"必須授予權限,套件監聽才能播送工具列的事件。"</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"沒有可以固定的地點"</string>
+</resources>
diff --git a/docklib/res/values-zu/strings.xml b/docklib/res/values-zu/strings.xml
new file mode 100644
index 0000000..b07de97
--- /dev/null
+++ b/docklib/res/values-zu/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="broadcast_sender_permission_label" msgid="5269973644784898827">"Dokha umthumeli wokusakaza"</string>
+ <string name="broadcast_sender_permission_desc" msgid="5052882219053515363">"Kudingeka imvume yephakheji yokusakaza imicimbi Kudokhi."</string>
+ <string name="broadcast_receiver_permission_label" msgid="6015991948761587466">"Dokha isamukeli somsakazi"</string>
+ <string name="broadcast_receiver_permission_desc" msgid="1623002370607914795">"Kudingeka imvume yephakheji yokulalela imicimbi yokusakaza Yedokhi."</string>
+ <string name="pin_failed_no_spots" msgid="745687732976464502">"Ayikho indawo etholakalayo yokuphina"</string>
+</resources>
diff --git a/docklib/res/values/colors.xml b/docklib/res/values/colors.xml
index fd2f30e..ea791c6 100644
--- a/docklib/res/values/colors.xml
+++ b/docklib/res/values/colors.xml
@@ -14,6 +14,9 @@
limitations under the License.
-->
<resources>
- <color name="icon_default_stroke_color">#FFFFFF</color>
- <color name="icon_excited_stroke_color">#737272</color>
+ <!-- todo(b/314859977): reset color to #000000 -->
+ <color name="icon_default_color">#FFFFFF</color>
+ <color name="icon_static_stroke_color">#000000</color>
+ <color name="icon_excited_stroke_color">@*android:color/car_grey_900</color>
+ <color name="icon_restricted_stroke_color">#69696952</color>
</resources>
diff --git a/docklib/res/values/config.xml b/docklib/res/values/config.xml
index 2c355fd..e026e7f 100644
--- a/docklib/res/values/config.xml
+++ b/docklib/res/values/config.xml
@@ -21,22 +21,35 @@
<!-- A list of components that are shown on Dock by default -->
<string-array name="config_defaultDockApps" translatable="false">
- <item>com.android.vending/com.google.android.finsky.carmainactivity.MainActivity</item>
<item>com.android.car.settings/com.android.car.settings.Settings_Launcher_Homepage</item>
+ <item>com.android.car.radio/com.android.car.radio.service.RadioAppService</item>
<item>com.android.car.dialer/com.android.car.dialer.ui.TelecomActivity</item>
- <item>com.google.android.apps.maps/com.google.android.maps.MapsActivity</item>
</string-array>
<!-- A list of components that are excluded from being shown on Dock -->
<string-array name="config_packagesExcludedFromDock" translatable="false">
<item>com.android.car.carlauncher</item>
+ <item>android.car.usb.handler</item>
</string-array>
<!-- A list of components that are excluded from being shown on Dock -->
<string-array name="config_componentsExcludedFromDock" translatable="false">
<item>com.google.android.apps.maps/com.google.android.apps.gmm.car.embedded.activity.LimitedMapsActivity</item>
<item>com.google.android.carassistant/com.google.android.libraries.assistant.auto.tng.assistant.ui.activity.AutoAssistantActivity</item>
+ <item>com.android.car.media/com.android.car.media.MediaDispatcherActivity</item>
+ <item>com.android.car.media/com.android.car.media.MediaBlockingActivity</item>
+ <item>com.android.systemui/com.android.systemui.car.wm.activity.LaunchOnPrivateDisplayRouterActivity</item>
+ <item>com.android.car.settings/com.android.car.settings.sound.AudioRouteSelectionActivity</item>
</string-array>
- <integer name="drag_drop_animate_in_duration">800</integer>
+ <!--
+ During drag and drop, when an item is dropped there are two consecutive animations:
+ 1. move to the proper location and to a scaled down version (scale down)
+ 2. change from the scaled down version to the final scale version (scale up)
+ -->
+ <integer name="drop_animation_scale_down_duration_ms">400</integer>
+ <integer name="drop_animation_scale_up_duration_ms">250</integer>
+
+ <!-- Duration of animation for when item is dragged over the drop location. -->
+ <integer name="excite_icon_animation_duration_ms">100</integer>
</resources>
diff --git a/docklib/res/values/dimens.xml b/docklib/res/values/dimens.xml
index d38260f..0013f19 100644
--- a/docklib/res/values/dimens.xml
+++ b/docklib/res/values/dimens.xml
@@ -18,11 +18,14 @@
<resources>
<!-- Default size and spacing for an icon on the Dock -->
- <dimen name="dock_item_size">72dp</dimen>
- <dimen name="dock_item_spacing">2dp</dimen>
+ <dimen name="dock_item_size">64dp</dimen>
- <dimen name="static_icon_stroke_width">0dp</dimen>
- <dimen name="dynamic_icon_stroke_width">6dp</dimen>
- <dimen name="icon_stroke_width_excited">30dp</dimen>
+ <dimen name="icon_stroke_width_static">0dp</dimen>
+ <dimen name="icon_stroke_width_dynamic">8dp</dimen>
+ <dimen name="icon_stroke_width_excited">24dp</dimen>
+ <item name="icon_colorFilter_alpha_excited" format="float" type="dimen">0.2</item>
+
+ <!-- Width to scale down the drop animation before scaling it back to required size -->
+ <dimen name="drop_animation_scale_down_width">6dp</dimen>
</resources>
diff --git a/docklib/res/values/strings.xml b/docklib/res/values/strings.xml
index c3100a5..95aaa1b 100644
--- a/docklib/res/values/strings.xml
+++ b/docklib/res/values/strings.xml
@@ -24,4 +24,5 @@
<string name="broadcast_receiver_permission_desc">
Permission required for package listen to broadcast events for the Dock.
</string>
+ <string name="pin_failed_no_spots">No spot available to pin</string>
</resources>
diff --git a/docklib/res/values/styles.xml b/docklib/res/values/styles.xml
index 8fee4a8..24f400e 100644
--- a/docklib/res/values/styles.xml
+++ b/docklib/res/values/styles.xml
@@ -17,13 +17,14 @@
<style name="ItemContainer">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">match_parent</item>
- <item name="android:paddingStart">15dp</item>
- <item name="android:paddingEnd">15dp</item>
+ <item name="android:paddingStart">10dp</item>
+ <item name="android:paddingEnd">10dp</item>
</style>
<style name="AppIcon">
<item name="android:layout_width">@dimen/dock_item_size</item>
<item name="android:layout_height">@dimen/dock_item_size</item>
+ <item name="android:layout_gravity">center</item>
<item name="android:clipToOutline">true</item>
<item name="android:scaleType">centerCrop</item>
<item name="android:adjustViewBounds">false</item>
diff --git a/docklib/src/com/android/car/docklib/DockHelper.kt b/docklib/src/com/android/car/docklib/DockHelper.kt
deleted file mode 100644
index 152a413..0000000
--- a/docklib/src/com/android/car/docklib/DockHelper.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2023 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.docklib
-
-import android.car.content.pm.CarPackageManager
-import android.content.ComponentName
-import android.content.Context
-import android.content.pm.PackageManager
-import com.android.car.docklib.data.DockAppItem
-
-/**
- * Helper that reads configs for defaults app to be showed on Dock and converts between
- * ComponentName and DockItem
- */
-class DockHelper(
- private val context: Context,
- private val carPackageManager: CarPackageManager,
-) {
- private val packageManager: PackageManager = context.packageManager
- val defaultApps by lazy {
- val defaultComponents = context.resources.getStringArray(R.array.config_defaultDockApps)
- defaultComponents.mapNotNull { component ->
- val componentName = ComponentName.unflattenFromString(component)
- componentName?.let { toDockAppItem(componentName) }
- }
- }
- val excludedComponents by lazy {
- HashSet<String>().apply {
- addAll(context.resources.getStringArray(R.array.config_componentsExcludedFromDock))
- }
- }
- val excludedPackages by lazy {
- HashSet<String>().apply {
- addAll(context.resources.getStringArray(R.array.config_packagesExcludedFromDock))
- }
- }
-
- /* Convert to Dock item from a ComponentName. */
- fun toDockAppItem(
- componentName: ComponentName,
- itemType: DockAppItem.Type = DockAppItem.Type.DYNAMIC
- ): DockAppItem {
- // TODO: Compare the component against LauncherApps to make sure the component
- // is launchable, similar to what app grid has
- val icon = packageManager.getApplicationIcon(componentName.packageName)
- val name = packageManager.getActivityInfo(componentName, 0).name
- return DockAppItem(
- itemType,
- componentName,
- name,
- icon,
- carPackageManager.isActivityDistractionOptimized(
- componentName.packageName,
- componentName.className
- )
- )
- }
-}
diff --git a/docklib/src/com/android/car/docklib/DockInterface.kt b/docklib/src/com/android/car/docklib/DockInterface.kt
index 0959099..0509e78 100644
--- a/docklib/src/com/android/car/docklib/DockInterface.kt
+++ b/docklib/src/com/android/car/docklib/DockInterface.kt
@@ -17,14 +17,46 @@
package com.android.car.docklib
import android.content.ComponentName
+import com.android.car.docklib.data.DockItemId
+import java.util.UUID
interface DockInterface {
- /** called when an app is statically pinned to the Dock */
+ /** called when an app is pinned to the Dock */
fun appPinned(componentName: ComponentName)
+ /** called when an app is pinned to the Dock at a particular position */
+ fun appPinned(componentName: ComponentName, index: Int)
+
+ /** called when an app already in the dock is pinned */
+ fun appPinned(@DockItemId id: UUID)
+
+ /** called when an app already in the dock is unpinned */
+ fun appUnpinned(componentName: ComponentName)
+
+ /** called when an app already in the dock is unpinned */
+ fun appUnpinned(@DockItemId id: UUID)
+
/** called when an app is launched */
fun appLaunched(componentName: ComponentName)
- /** called when an app should be removed from the Dock */
- fun appUnpinned(componentName: ComponentName)
+ /**
+ * called when an app is uninstalled/removed from the system or is inaccessible in the dock.
+ * @param packageName packageName of removed package
+ */
+ fun packageRemoved(packageName: String)
+
+ /**
+ * called when an app is installed in the system or is enabled in the dock.
+ * @param packageName packageName of removed package
+ */
+ fun packageAdded(packageName: String)
+
+ /** called to launch an app */
+ fun launchApp(componentName: ComponentName, isMediaApp: Boolean)
+
+ /** @return the dominant color to be used with the icon corresponding to [componentName] */
+ fun getIconColorWithScrim(componentName: ComponentName): Int
+
+ /** get the set of all media service components */
+ fun getMediaServiceComponents(): Set<ComponentName>
}
diff --git a/docklib/src/com/android/car/docklib/DockViewController.kt b/docklib/src/com/android/car/docklib/DockViewController.kt
index 8285253..a3b179e 100644
--- a/docklib/src/com/android/car/docklib/DockViewController.kt
+++ b/docklib/src/com/android/car/docklib/DockViewController.kt
@@ -16,107 +16,253 @@
package com.android.car.docklib
+import android.annotation.CallSuper
+import android.app.ActivityOptions
+import android.app.NotificationManager
import android.car.Car
import android.car.content.pm.CarPackageManager
+import android.car.drivingstate.CarUxRestrictionsManager
+import android.car.media.CarMediaManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.content.pm.LauncherApps
+import android.media.session.MediaController
+import android.media.session.MediaSessionManager
+import android.media.session.PlaybackState
import android.os.Build
+import android.os.RemoteException
+import android.os.UserHandle
import android.util.Log
+import androidx.core.content.getSystemService
+import com.android.car.carlauncher.Flags
+import com.android.car.docklib.data.DockProtoDataController
import com.android.car.docklib.events.DockEventsReceiver
+import com.android.car.docklib.events.DockPackageChangeReceiver
+import com.android.car.docklib.media.MediaUtils
import com.android.car.docklib.task.DockTaskStackChangeListener
import com.android.car.docklib.view.DockAdapter
import com.android.car.docklib.view.DockView
+import com.android.launcher3.icons.IconFactory
import com.android.systemui.shared.system.TaskStackChangeListeners
+import java.io.File
import java.lang.ref.WeakReference
-import java.util.function.Consumer
+import java.util.UUID
/**
* Create a controller for DockView. It initializes the view with default and persisted icons. Upon
* initializing, it will listen to broadcast events, and update the view.
*
- * @param userContext the foreground user context, since the view may be hosted on system context
* @param dockView the inflated dock view
- * @param intentDelegate the system context will need to handle clicks and actions on the icons
+ * @param userContext the foreground user context, since the view may be hosted on system context
+ * @param dataFile a file to store user's pinned apps with read and write permission
*/
-class DockViewController(
- private val userContext: Context,
- dockView: DockView,
- intentDelegate: Consumer<Intent>
+open class DockViewController(
+ dockView: DockView,
+ private val userContext: Context = dockView.context,
+ dataFile: File,
) : DockInterface {
- private companion object {
+ companion object {
private const val TAG = "DockViewController"
private val DEBUG = Build.isDebuggable()
}
- private val numItems: Int
+ private val numItems = dockView.context.resources.getInteger(R.integer.config_numDockApps)
private val car: Car
private val dockViewWeakReference: WeakReference<DockView>
private val dockViewModel: DockViewModel
- private var dockHelper: DockHelper? = null
+ private val adapter: DockAdapter
private val dockEventsReceiver: DockEventsReceiver
+ private val dockPackageChangeReceiver: DockPackageChangeReceiver
private val taskStackChangeListeners: TaskStackChangeListeners
private val dockTaskStackChangeListener: DockTaskStackChangeListener
+ private val launcherApps = userContext.getSystemService<LauncherApps>()
+ private val excludedItemsProviders: Set<ExcludedItemsProvider> =
+ hashSetOf(ResourceExcludedItemsProvider(userContext))
+ private val mediaSessionManager: MediaSessionManager
+ private val sessionChangedListener: MediaSessionManager.OnActiveSessionsChangedListener =
+ MediaSessionManager.OnActiveSessionsChangedListener { mediaControllers ->
+ handleMediaSessionChange(mediaControllers)
+ }
init {
- numItems = userContext.resources.getInteger(R.integer.config_numDockApps)
- val adapter = DockAdapter(numItems, intentDelegate, userContext)
+ if (DEBUG) Log.d(TAG, "Init DockViewController for user ${userContext.userId}")
+ adapter = DockAdapter(this, userContext)
dockView.setAdapter(adapter)
dockViewWeakReference = WeakReference(dockView)
- dockViewModel = DockViewModel(numItems) { updatedApps ->
- dockViewWeakReference.get()?.getAdapter()?.setItems(updatedApps)
- ?: throw NullPointerException("the View referenced does not exist")
+
+ val launcherActivities = launcherApps
+ ?.getActivityList(null, userContext.user)
+ ?.map { it.componentName }
+ ?.toMutableSet() ?: mutableSetOf()
+
+ dockViewModel = DockViewModel(
+ maxItemsInDock = numItems,
+ context = userContext,
+ packageManager = userContext.packageManager,
+ launcherActivities = launcherActivities,
+ defaultPinnedItems = dockView.resources
+ .getStringArray(R.array.config_defaultDockApps)
+ .mapNotNull(ComponentName::unflattenFromString),
+ isPackageExcluded = { pkg ->
+ getExcludedItemsProviders()
+ .map { it.isPackageExcluded(pkg) }
+ .reduce { res1, res2 -> res1 or res2 }
+ },
+ isComponentExcluded = { cmp ->
+ getExcludedItemsProviders()
+ .map { it.isComponentExcluded(cmp) }
+ .reduce { res1, res2 -> res1 or res2 }
+ },
+ iconFactory = IconFactory.obtain(dockView.context),
+ dockProtoDataController = DockProtoDataController(dataFile),
+ ) { updatedApps ->
+ dockViewWeakReference.get()?.getAdapter()?.submitList(updatedApps)
+ ?: throw NullPointerException("the View referenced does not exist")
}
- car =
- Car.createCar(
+ car = Car.createCar(
userContext,
null, // handler
Car.CAR_WAIT_TIMEOUT_DO_NOT_WAIT
- ) { car, ready ->
- run {
- if (ready) {
- val carPackageManager = car.getCarManager(CarPackageManager::class.java)
- carPackageManager?.let { carPM ->
- adapter.setCarPackageManager(carPM)
- // todo(b/314859963): create the DockHelper without depending on carPM
- dockHelper = DockHelper(userContext, carPM)
- dockHelper?.let { dockViewModel.updateDefaultApps(it.defaultApps) }
+ ) { car, ready ->
+ run {
+ if (ready) {
+ car.getCarManager(CarPackageManager::class.java)?.let { carPM ->
+ dockViewModel.setCarPackageManager(carPM)
+ }
+ car.getCarManager(CarMediaManager::class.java)?.let { carMM ->
+ adapter.setCarMediaManager(carMM)
+ }
+ car.getCarManager(CarUxRestrictionsManager::class.java)?.let {
+ adapter.setUxRestrictions(
+ isUxRestrictionEnabled =
+ it.currentCarUxRestrictions?.isRequiresDistractionOptimization ?: false
+ )
+ it.registerListener { carUxRestrictions ->
+ adapter.setUxRestrictions(
+ isUxRestrictionEnabled =
+ carUxRestrictions.isRequiresDistractionOptimization
+ )
}
}
}
}
+ }
+
+ mediaSessionManager =
+ userContext.getSystemService(MediaSessionManager::class.java) as MediaSessionManager
+ if (Flags.mediaSessionCard()) {
+ handleMediaSessionChange(mediaSessionManager.getActiveSessionsForUser(
+ /* notificationListener= */
+ null,
+ UserHandle.of(userContext.userId)
+ ))
+ mediaSessionManager.addOnActiveSessionsChangedListener(
+ /* notificationListener= */
+ null,
+ UserHandle.of(userContext.userId),
+ userContext.getMainExecutor(),
+ sessionChangedListener
+ )
+ }
+
dockEventsReceiver = DockEventsReceiver.registerDockReceiver(userContext, this)
- dockTaskStackChangeListener = DockTaskStackChangeListener { appLaunched(it) }
+ dockPackageChangeReceiver = DockPackageChangeReceiver.registerReceiver(userContext, this)
+ dockTaskStackChangeListener =
+ DockTaskStackChangeListener(userContext.userId, this)
taskStackChangeListeners = TaskStackChangeListeners.getInstance()
taskStackChangeListeners.registerTaskStackListener(dockTaskStackChangeListener)
}
/** Method to stop the dock. Call this upon View being destroyed. */
- fun destroy() {
+ @CallSuper
+ open fun destroy() {
if (DEBUG) Log.d(TAG, "Destroy called")
+ car.getCarManager(CarUxRestrictionsManager::class.java)?.unregisterListener()
car.disconnect()
userContext.unregisterReceiver(dockEventsReceiver)
- dockViewModel.destroy()
+ userContext.unregisterReceiver(dockPackageChangeReceiver)
taskStackChangeListeners.unregisterTaskStackListener(dockTaskStackChangeListener)
+ mediaSessionManager.removeOnActiveSessionsChangedListener(sessionChangedListener)
+ dockViewModel.destroy()
}
- override fun appPinned(componentName: ComponentName) {
- // TODO("Not yet implemented")
- }
+ open fun getExcludedItemsProviders(): Set<ExcludedItemsProvider> = excludedItemsProviders
- override fun appLaunched(componentName: ComponentName) {
- if (DEBUG) Log.d(TAG, "App launched: $componentName")
- dockHelper?.let {
- if (it.excludedPackages.contains(componentName.packageName)) return
- if (it.excludedComponents.contains(componentName.flattenToString())) return
+ override fun appPinned(componentName: ComponentName) = dockViewModel.pinItem(componentName)
- val appItem = it.toDockAppItem(componentName)
- if (DEBUG) Log.d(TAG, "Dynamic app add to dock: $appItem")
- dockViewModel.addDynamicItem(appItem)
- }
- }
+ override fun appPinned(componentName: ComponentName, index: Int) =
+ dockViewModel.pinItem(componentName, index)
+
+ override fun appPinned(id: UUID) = dockViewModel.pinItem(id)
override fun appUnpinned(componentName: ComponentName) {
- // TODO("Not yet implemented")
+ // TODO: Not yet implemented
+ }
+
+ override fun appUnpinned(id: UUID) = dockViewModel.removeItem(id)
+
+ override fun appLaunched(componentName: ComponentName) =
+ dockViewModel.addDynamicItem(componentName)
+
+ override fun launchApp(componentName: ComponentName, isMediaApp: Boolean) {
+ val intent = if (isMediaApp) {
+ MediaUtils.createLaunchIntent(componentName)
+ } else {
+ Intent(Intent.ACTION_MAIN)
+ .setComponent(componentName)
+ .addCategory(Intent.CATEGORY_LAUNCHER)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ val options = ActivityOptions.makeBasic()
+ options.setLaunchDisplayId(userContext.display.displayId)
+ // todo(b/312718542): hidden api(context.startActivityAsUser) usage
+ userContext.startActivityAsUser(intent, options.toBundle(), userContext.user)
+ }
+
+ override fun getIconColorWithScrim(componentName: ComponentName) =
+ dockViewModel.getIconColorWithScrim(componentName)
+
+ override fun packageRemoved(packageName: String) = dockViewModel.removeItems(packageName)
+
+ override fun packageAdded(packageName: String) {
+ dockViewModel.addMediaComponents(packageName)
+ dockViewModel.addLauncherComponents(
+ launcherApps?.getActivityList(packageName, userContext.user)
+ ?.map { it.componentName } ?: listOf()
+ )
+ }
+
+ override fun getMediaServiceComponents(): Set<ComponentName> =
+ dockViewModel.getMediaServiceComponents()
+
+ private fun handleMediaSessionChange(mediaControllers: List<MediaController>?) {
+ val mediaNotificationPackages = getActiveMediaNotificationPackages()
+ val activeMediaSessions = mediaControllers?.filter {
+ it.playbackState?.let { playbackState ->
+ (playbackState.isActive || playbackState.state == PlaybackState.STATE_PAUSED)
+ } ?: false
+ }?.map { it.packageName }?.filter { mediaNotificationPackages.contains(it) } ?: emptyList()
+
+ adapter.onMediaSessionChange(activeMediaSessions)
+ }
+
+ private fun getActiveMediaNotificationPackages(): List<String> {
+ try {
+ // todo(b/312718542): hidden api(NotificationManager.getService()) usage
+ return NotificationManager.getService()
+ .getActiveNotificationsWithAttribution(
+ userContext.packageName,
+ null
+ ).toList().filter {
+ it.notification.extras != null && it.notification.isMediaNotification
+ }.map { it.packageName }
+ } catch (e: RemoteException) {
+ Log.e(
+ TAG,
+ "Exception trying to get active notifications $e"
+ )
+ return listOf()
+ }
}
}
diff --git a/docklib/src/com/android/car/docklib/DockViewModel.kt b/docklib/src/com/android/car/docklib/DockViewModel.kt
index 8037ee0..c169c18 100644
--- a/docklib/src/com/android/car/docklib/DockViewModel.kt
+++ b/docklib/src/com/android/car/docklib/DockViewModel.kt
@@ -16,69 +16,349 @@
package com.android.car.docklib
+import android.app.ActivityManager
+import android.app.ActivityTaskManager
+import android.car.content.pm.CarPackageManager
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageItemInfo
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.util.Log
+import android.view.Display
+import android.widget.Toast
+import androidx.annotation.VisibleForTesting
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import com.android.car.docklib.data.DockAppItem
+import com.android.car.docklib.data.DockItemId
+import com.android.car.docklib.data.DockProtoDataController
+import com.android.car.docklib.media.MediaUtils
+import com.android.car.docklib.task.TaskUtils
+import com.android.launcher3.icons.BaseIconFactory
+import com.android.launcher3.icons.ColorExtractor
+import com.android.launcher3.icons.IconFactory
+import java.util.Collections
+import java.util.UUID
/**
* This class contains a live list of dock app items. All changes to dock items will go through it
* and will be observed by the view layer.
*/
-class DockViewModel(private val numItems: Int, private val observer: Observer<List<DockAppItem?>>) {
- private val currentItems = MutableLiveData<List<DockAppItem?>>()
+open class DockViewModel(
+ private val maxItemsInDock: Int,
+ private val context: Context,
+ private val packageManager: PackageManager,
+ private var carPackageManager: CarPackageManager? = null,
+ private val userId: Int = context.userId,
+ private var launcherActivities: MutableSet<ComponentName>,
+ defaultPinnedItems: List<ComponentName>,
+ private val isPackageExcluded: (pkg: String) -> Boolean,
+ private val isComponentExcluded: (component: ComponentName) -> Boolean,
+ private val iconFactory: IconFactory = IconFactory.obtain(context),
+ private val dockProtoDataController: DockProtoDataController,
+ private val observer: Observer<List<DockAppItem>>,
+) {
- /* Maintain a mapping of dock index to dock item, with the order of addition,
+ private companion object {
+ private const val TAG = "DockViewModel"
+ private val DEBUG = Build.isDebuggable()
+ private const val MAX_UNIQUE_ID_TRIES = 20
+ private const val MAX_TASKS_TO_FETCH = 20
+ }
+
+ private val noSpotAvailableToPinToastMsg = context.getString(R.string.pin_failed_no_spots)
+ private val colorExtractor = ColorExtractor()
+ private val defaultIconColor = context.resources.getColor(
+ R.color.icon_default_color,
+ null // theme
+ )
+ private val currentItems = MutableLiveData<List<DockAppItem>>()
+ private val mediaServiceComponents = MediaUtils.fetchMediaServiceComponents(packageManager)
+
+ /*
+ * Maintain a mapping of dock index to dock item, with the order of addition,
* so it's easier to find least recently updated position.
* The order goes from least recently updated item to most recently updated item.
* The key in each mapping is the index/position of the item being shown in Dock.
*/
- private val internalItems = LinkedHashMap<Int, DockAppItem>()
+ @VisibleForTesting
+ val internalItems: MutableMap<Int, DockAppItem> =
+ Collections.synchronizedMap(LinkedHashMap<Int, DockAppItem>())
init {
- currentItems.value = List(numItems) { null }
+ initializeDockItems(defaultPinnedItems)
+ currentItems.value = createDockList()
currentItems.observeForever(observer)
}
- /** Update default apps if the list is not populated */
- fun updateDefaultApps(defaultApps: List<DockAppItem>) {
- synchronized(internalItems) {
- if (internalItems.size >= numItems) return
- defaultApps.forEachIndexed { index, defaultAppItem ->
- if (!internalItems.containsKey(index) && index < numItems) {
- // change to default app if the position is not populated
- internalItems[index] = defaultAppItem
+ private fun initializeDockItems(defaultPinnedItems: List<ComponentName>) {
+ dockProtoDataController.loadFromFile()?.let { savedPinnedDockItems ->
+ if (DEBUG) Log.d(TAG, "Initialized using saved items")
+ savedPinnedDockItems.forEach { (index, component) ->
+ createDockItem(component, DockAppItem.Type.STATIC, isMediaApp(component))?.let {
+ internalItems[index] = it
}
}
- currentItems.value = convertMapToList(internalItems)
+ } ?: run {
+ if (DEBUG) Log.d(TAG, "Initialized using default items")
+ for (index in 0..<minOf(maxItemsInDock, defaultPinnedItems.size)) {
+ createDockItem(
+ defaultPinnedItems[index],
+ DockAppItem.Type.STATIC,
+ isMediaApp(defaultPinnedItems[index])
+ )?.let {
+ internalItems[index] = it
+ }
+ }
}
}
+ /** Pin an existing dock item with given [id]. It is assumed the item is not pinned/static. */
+ fun pinItem(@DockItemId id: UUID) {
+ if (DEBUG) Log.d(TAG, "Pin Item, id: $id")
+ internalItems
+ .filter { mapEntry -> mapEntry.value.id == id }
+ .firstNotNullOfOrNull { it }
+ ?.let { mapEntry ->
+ if (DEBUG) {
+ Log.d(TAG, "Pinning ${mapEntry.value.component} at ${mapEntry.key}")
+ }
+ internalItems[mapEntry.key] =
+ mapEntry.value.copy(type = DockAppItem.Type.STATIC)
+ }
+ // update list regardless to update the listeners
+ currentItems.value = createDockList()
+ savePinnedItemsToProto()
+ }
+
+ /**
+ * Pin a new item that is not previously present in the dock. It is assumed the item is not
+ * pinned/static.
+ *
+ * @param component [ComponentName] of the pinned item.
+ * @param indexToPin the index to pin the item at. For null value, a suitable index is searched
+ * to pin to. If no index is suitable the user is notified.
+ */
+ fun pinItem(component: ComponentName, indexToPin: Int? = null) {
+ if (DEBUG) Log.d(TAG, "Pin Item, component: $component, indexToPin: $indexToPin")
+ createDockItem(
+ component,
+ DockAppItem.Type.STATIC,
+ isMediaApp(component)
+ )?.let { dockItem ->
+ if (indexToPin != null) {
+ if (indexToPin in 0..<maxItemsInDock) {
+ if (DEBUG) Log.d(TAG, "Pinning $component at $indexToPin")
+ internalItems[indexToPin] = dockItem
+ } else {
+ if (DEBUG) Log.d(TAG, "Invalid index provided")
+ }
+ } else {
+ val index = findIndexToPin()
+ if (index == null) {
+ if (DEBUG) Log.d(TAG, "No dynamic or empty spots available to pin")
+ // if no dynamic or empty spots available, notify the user
+ showToast(noSpotAvailableToPinToastMsg)
+ return@pinItem
+ }
+ if (DEBUG) Log.d(TAG, "Pinning $component at $index")
+ internalItems[index] = dockItem
+ }
+ }
+ // update list regardless to update the listeners
+ currentItems.value = createDockList()
+ savePinnedItemsToProto()
+ }
+
+ /** Removes item with the given [id] from the dock. */
+ fun removeItem(id: UUID) {
+ if (DEBUG) Log.d(TAG, "Unpin Item, id: $id")
+ internalItems
+ .filter { mapEntry -> mapEntry.value.id == id }
+ .firstNotNullOfOrNull { it }
+ ?.let { mapEntry ->
+ if (DEBUG) {
+ Log.d(TAG, "Unpinning ${mapEntry.value.component} at ${mapEntry.key}")
+ }
+ internalItems.remove(mapEntry.key)
+ }
+ // update list regardless to update the listeners
+ currentItems.value = createDockList()
+ savePinnedItemsToProto()
+ }
+
+ /** Removes all items of the given [packageName] from the dock. */
+ fun removeItems(packageName: String) {
+ internalItems.entries.removeAll { it.value.component.packageName == packageName }
+ val areMediaComponentsRemoved =
+ mediaServiceComponents.removeIf { it.packageName == packageName }
+ if (areMediaComponentsRemoved && DEBUG) {
+ Log.d(TAG, "Media components were removed for $packageName")
+ }
+ launcherActivities.removeAll { it.packageName == packageName }
+ currentItems.value = createDockList()
+ savePinnedItemsToProto()
+ }
+
+ /** Adds all media service components for the given [packageName]. */
+ fun addMediaComponents(packageName: String) {
+ val components = MediaUtils.fetchMediaServiceComponents(packageManager, packageName)
+ if (DEBUG) Log.d(TAG, "Added media components: $components")
+ mediaServiceComponents.addAll(components)
+ }
+
+ /** Adds all launcher components. */
+ fun addLauncherComponents(components: List<ComponentName>) {
+ launcherActivities.addAll(components)
+ }
+
+ fun getMediaServiceComponents(): Set<ComponentName> = mediaServiceComponents
+
/**
* Add a new app to the dock. If the app is already in the dock, the recency of the app is
* refreshed. If not, and the dock has dynamic item(s) to update, then it will replace the least
* recent dynamic item.
*/
- fun addDynamicItem(appItem: DockAppItem) {
- synchronized(internalItems) {
- val indexToUpdate =
- indexOfItemWithPackageName(appItem.component.packageName)
- ?: indexOfLeastRecentDynamicItemInDock()
-
- indexToUpdate?.let {
- internalItems.remove(it)
- internalItems[it] = appItem
- currentItems.value = convertMapToList(internalItems)
- }
+ fun addDynamicItem(component: ComponentName) {
+ if (DEBUG) Log.d(TAG, "Add dynamic item, component: $component")
+ if (isItemExcluded(component)) {
+ if (DEBUG) Log.d(TAG, "Dynamic item is excluded")
+ return
}
+ if (isItemInDock(component, DockAppItem.Type.STATIC)) {
+ if (DEBUG) Log.d(TAG, "Dynamic item is already present in the dock as static item")
+ return
+ }
+ val indexToUpdate =
+ indexOfItemWithPackageName(component.packageName)
+ ?: indexOfLeastRecentDynamicItemInDock()
+ if (indexToUpdate == null || indexToUpdate >= maxItemsInDock) return
+
+ createDockItem(
+ component,
+ DockAppItem.Type.DYNAMIC,
+ isMediaApp(component)
+ )?.let { newDockItem ->
+ if (DEBUG) Log.d(TAG, "Updating $component at $indexToUpdate")
+ internalItems.remove(indexToUpdate)
+ internalItems[indexToUpdate] = newDockItem
+ currentItems.value = createDockList()
+ }
+ }
+
+ fun getIconColorWithScrim(componentName: ComponentName): Int {
+ return DockAppItem.getIconColorWithScrim(getIconColor(componentName))
}
fun destroy() {
currentItems.removeObserver(observer)
}
+ fun setCarPackageManager(carPackageManager: CarPackageManager) {
+ this.carPackageManager = carPackageManager
+ internalItems.forEach { mapEntry ->
+ val item = mapEntry.value
+ internalItems[mapEntry.key] = item.copy(
+ isDistractionOptimized = item.isMediaApp ||
+ carPackageManager.isActivityDistractionOptimized(
+ item.component.packageName,
+ item.component.className
+ )
+ )
+ }
+ currentItems.value = createDockList()
+ }
+
+ @VisibleForTesting
+ fun createDockList(): List<DockAppItem> {
+ if (DEBUG) Log.d(TAG, "createDockList called")
+ // todo(b/312718542): hidden api(ActivityTaskManager.getTasks) usage
+ val runningTaskList = getRunningTasks().filter { it.userId == userId }
+
+ for (index in 0..<maxItemsInDock) {
+ if (internalItems.contains(index)) continue
+
+ var isItemFound = false
+ for (component in runningTaskList.mapNotNull { TaskUtils.getComponentName(it) }) {
+ if (!isItemExcluded(component) && !isItemInDock(component)) {
+ createDockItem(
+ component,
+ DockAppItem.Type.DYNAMIC,
+ isMediaApp(component)
+ )?.let { dockItem ->
+ if (DEBUG) {
+ Log.d(TAG, "Adding recent item(${dockItem.component}) at $index")
+ }
+ internalItems[index] = dockItem
+ isItemFound = true
+ }
+ }
+ if (isItemFound) break
+ }
+
+ if (isItemFound) continue
+
+ for (component in launcherActivities.shuffled()) {
+ if (!isItemExcluded(component) && !isItemInDock(component)) {
+ createDockItem(
+ componentName = component,
+ DockAppItem.Type.DYNAMIC,
+ isMediaApp(component)
+ )?.let { dockItem ->
+ if (DEBUG) {
+ Log.d(TAG, "Adding recommended item(${dockItem.component}) at $index")
+ }
+ internalItems[index] = dockItem
+ isItemFound = true
+ }
+ }
+ if (isItemFound) break
+ }
+
+ if (!isItemFound) {
+ throw IllegalStateException("Cannot find enough apps to place in the dock")
+ }
+ }
+ return convertMapToList(internalItems)
+ }
+
+ private fun savePinnedItemsToProto() {
+ dockProtoDataController.savePinnedItemsToFile(
+ internalItems.filter { entry -> entry.value.type == DockAppItem.Type.STATIC }
+ .mapValues { entry -> entry.value.component }
+ )
+ }
+
+ /** Use the mapping index->item to create the ordered list of Dock items */
+ private fun convertMapToList(map: Map<Int, DockAppItem>): List<DockAppItem> =
+ List(maxItemsInDock) { index -> map[index] }.filterNotNull()
+ // TODO b/314409899: use a default DockItem when a position is empty
+
+ private fun findIndexToPin(): Int? {
+ var index: Int? = null
+ for (i in 0..<maxItemsInDock) {
+ if (!internalItems.contains(i)) {
+ index = i
+ break
+ }
+ if (internalItems[i]?.type == DockAppItem.Type.DYNAMIC) {
+ index = i
+ break
+ }
+ }
+ return index
+ }
+
private fun indexOfLeastRecentDynamicItemInDock(): Int? {
- // edge case - if there is no apps being shown, update first position
- if (internalItems.size == 0) return 0
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "internalItems.size = ${internalItems.size}, maxItemsInDock= $maxItemsInDock"
+ )
+ }
+ if (internalItems.size < maxItemsInDock) return internalItems.size
// since map is ordered from least recent to most recent, return first dynamic entry found
internalItems.forEach { appItemEntry ->
if (appItemEntry.value.type == DockAppItem.Type.DYNAMIC) return appItemEntry.key
@@ -96,8 +376,103 @@
return null
}
- /** Use the mapping index->item to create the ordered list of Dock items */
- private fun convertMapToList(map: Map<Int, DockAppItem>) =
- List(numItems) { index -> map[index] }
- // TODO b/314409899: use a default DockItem when a position is empty
+ private fun isItemExcluded(component: ComponentName): Boolean =
+ (isPackageExcluded(component.packageName) || isComponentExcluded(component))
+
+ private fun isItemInDock(component: ComponentName, ofType: DockAppItem.Type? = null): Boolean {
+ return internalItems.values
+ .filter { (ofType == null) || (it.type == ofType) }
+ .map { it.component.packageName }
+ .contains(component.packageName)
+ }
+
+ /* Creates Dock item from a ComponentName. */
+ private fun createDockItem(
+ componentName: ComponentName,
+ itemType: DockAppItem.Type,
+ isMediaApp: Boolean,
+ ): DockAppItem? {
+ // TODO: Compare the component against LauncherApps to make sure the component
+ // is launchable, similar to what app grid has
+
+ val ai = getPackageItemInfo(componentName) ?: return null
+ // todo(b/315210225): handle getting icon lazily
+ val icon = ai.loadIcon(packageManager)
+ val iconColor = getIconColor(icon)
+ return DockAppItem(
+ id = getUniqueDockItemId(),
+ type = itemType,
+ component = componentName,
+ name = ai.loadLabel(packageManager).toString(),
+ icon = icon,
+ iconColor = iconColor,
+ isDistractionOptimized =
+ isMediaApp || (carPackageManager?.isActivityDistractionOptimized(
+ componentName.packageName,
+ componentName.className
+ ) ?: false),
+ isMediaApp = isMediaApp
+ )
+ }
+
+ private fun getPackageItemInfo(componentName: ComponentName): PackageItemInfo? {
+ try {
+ val isMediaApp = isMediaApp(componentName)
+ val pkgInfo = packageManager.getPackageInfo(
+ componentName.packageName,
+ PackageManager.PackageInfoFlags.of(
+ (if (isMediaApp) PackageManager.GET_SERVICES else PackageManager.GET_ACTIVITIES)
+ .toLong()
+ )
+ )
+ return if (isMediaApp) {
+ pkgInfo.services?.find { it.componentName == componentName }
+ } else {
+ pkgInfo.activities?.find { it.componentName == componentName }
+ }
+ } catch (e: PackageManager.NameNotFoundException) {
+ if (DEBUG) {
+ // don't need to crash for this failure, log error instead
+ Log.e(TAG, "Component $componentName not found", e)
+ }
+ }
+ return null
+ }
+
+ private fun getIconColor(componentName: ComponentName): Int {
+ val ai = getPackageItemInfo(componentName) ?: return defaultIconColor
+ return getIconColor(ai.loadIcon(packageManager))
+ }
+
+ private fun getIconColor(icon: Drawable) = colorExtractor.findDominantColorByHue(
+ iconFactory.createScaledBitmap(icon, BaseIconFactory.MODE_DEFAULT)
+ )
+
+ private fun getUniqueDockItemId(): @DockItemId UUID {
+ val existingKeys = internalItems.values.map { it.id }.toSet()
+ for (i in 0..MAX_UNIQUE_ID_TRIES) {
+ val id = UUID.randomUUID()
+ if (!existingKeys.contains(id)) return id
+ }
+ return UUID.randomUUID()
+ }
+
+ private fun isMediaApp(component: ComponentName) = mediaServiceComponents.contains(component)
+
+ /** To be disabled for tests since [Toast] cannot be shown on that process */
+ @VisibleForTesting
+ fun showToast(message: String) {
+ Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
+ }
+
+ /** To be overridden in tests to pass mock values for RunningTasks */
+ @VisibleForTesting
+ fun getRunningTasks(): List<ActivityManager.RunningTaskInfo> {
+ return ActivityTaskManager.getInstance().getTasks(
+ MAX_TASKS_TO_FETCH,
+ false, // filterOnlyVisibleRecents
+ false, // keepIntentExtra
+ Display.DEFAULT_DISPLAY // displayId
+ )
+ }
}
diff --git a/docklib/src/com/android/car/docklib/ExcludedItemsProvider.kt b/docklib/src/com/android/car/docklib/ExcludedItemsProvider.kt
new file mode 100644
index 0000000..35b983c
--- /dev/null
+++ b/docklib/src/com/android/car/docklib/ExcludedItemsProvider.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 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.docklib
+
+import android.content.ComponentName
+
+/**
+ * Interface to provide packages or components to be excluded from the dock.
+ */
+interface ExcludedItemsProvider {
+ /**
+ * @return if the components in the [pkg] are excluded from Dock
+ */
+ fun isPackageExcluded(pkg: String): Boolean
+
+ /**
+ * @return if the [component] is excluded from Dock
+ */
+ fun isComponentExcluded(component: ComponentName): Boolean
+}
diff --git a/docklib/src/com/android/car/docklib/ResourceExcludedItemsProvider.kt b/docklib/src/com/android/car/docklib/ResourceExcludedItemsProvider.kt
new file mode 100644
index 0000000..d8998cd
--- /dev/null
+++ b/docklib/src/com/android/car/docklib/ResourceExcludedItemsProvider.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 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.docklib
+
+import android.content.ComponentName
+import android.content.Context
+
+/**
+ * [ExcludedItemsProvider] that reads from resources and excludes given packages and components.
+ */
+class ResourceExcludedItemsProvider(context: Context) : ExcludedItemsProvider {
+ private val excludedPackages = context.resources
+ .getStringArray(R.array.config_packagesExcludedFromDock).toHashSet()
+ private val excludedComponents = context.resources
+ .getStringArray(R.array.config_componentsExcludedFromDock)
+ .mapNotNull(ComponentName::unflattenFromString).toHashSet()
+
+ override fun isPackageExcluded(pkg: String) = excludedPackages.contains(pkg)
+
+ override fun isComponentExcluded(component: ComponentName) =
+ excludedComponents.contains(component)
+}
diff --git a/docklib/src/com/android/car/docklib/data/DockAppItem.kt b/docklib/src/com/android/car/docklib/data/DockAppItem.kt
index 3e67950..fdf2121 100644
--- a/docklib/src/com/android/car/docklib/data/DockAppItem.kt
+++ b/docklib/src/com/android/car/docklib/data/DockAppItem.kt
@@ -17,17 +17,54 @@
package com.android.car.docklib.data
import android.content.ComponentName
+import android.graphics.Color
import android.graphics.drawable.Drawable
+import androidx.annotation.ColorInt
+import com.android.internal.graphics.ColorUtils
+import com.android.launcher3.icons.FastBitmapDrawable
+import com.android.launcher3.icons.GraphicsUtils
+import java.util.UUID
-/** Data class that describes an app being showed on Dock */
+/**
+ * Data class that describes an app being showed on Dock.
+ *
+ * @param iconColor dominant color to be used with the [icon].
+ * @param iconColorScrim color to be used to create a slightly dull/bright color variation of
+ * [iconColor]. Uses the default scrim if not provided.
+ */
data class DockAppItem(
- val type: Type,
- val component: ComponentName,
- val name: String,
- val icon: Drawable,
- val isDistractionOptimized: Boolean,
+ val id: @DockItemId UUID = UUID.randomUUID(),
+ val type: Type,
+ val component: ComponentName,
+ val name: String,
+ val icon: Drawable,
+ @ColorInt val iconColor: Int,
+ @ColorInt private val iconColorScrim: Int = defaultIconColorScrim,
+ val isDistractionOptimized: Boolean,
+ val isMediaApp: Boolean,
) {
- // todo(b/315210225): handle getting icon lazily
+ companion object{
+ private val defaultIconColorScrim = GraphicsUtils.setColorAlphaBound(
+ Color.WHITE,
+ FastBitmapDrawable.WHITE_SCRIM_ALPHA
+ )
+
+ /**
+ * Composes the [iconColor] with the [iconColorScrim].
+ *
+ * @param iconColorScrim color to be used to create a slightly dull/bright color variation
+ * of [iconColor]. Uses the default scrim if not provided.
+ */
+ fun getIconColorWithScrim(
+ iconColor: Int,
+ iconColorScrim: Int = defaultIconColorScrim
+ ): Int {
+ return ColorUtils.compositeColors(iconColorScrim, iconColor)
+ }
+ }
+
+ @ColorInt val iconColorWithScrim = getIconColorWithScrim(iconColor, iconColorScrim)
+
enum class Type(val value: String) {
DYNAMIC("DYNAMIC"),
STATIC("STATIC");
@@ -38,20 +75,23 @@
}
override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is DockAppItem) return false
-
- if (this.type != other.type) return false
- if (this.name != other.name) return false
- if (this.component != other.component) return false
- if (this.icon.constantState != other.icon.constantState) return false
- if (this.isDistractionOptimized != other.isDistractionOptimized) return false
-
- return true
+ return (this === other) ||
+ (other is DockAppItem &&
+ this.id == other.id &&
+ this.name == other.name &&
+ this.type == other.type &&
+ this.component == other.component &&
+ this.icon.constantState == other.icon.constantState &&
+ this.iconColor == other.iconColor &&
+ this.iconColorWithScrim == other.iconColorWithScrim &&
+ this.isDistractionOptimized == other.isDistractionOptimized &&
+ this.isMediaApp == other.isMediaApp)
}
override fun toString(): String {
- return ("DockAppItem#${hashCode()}{name: $name, component: $component, type: $type, " +
- "isDistractionOptimized: $isDistractionOptimized, icon: $icon}")
+ return ("DockAppItem#${hashCode()}{id: $id, name: $name, component: $component, " +
+ "type: $type, isDistractionOptimized: $isDistractionOptimized, icon: $icon, " +
+ "iconColor: $iconColor, iconColorScrim: $iconColorScrim, " +
+ "iconColorWithScrim: $iconColorWithScrim}")
}
}
diff --git a/app/src/com/android/car/carlauncher/LaunchRootCarTaskViewCallbacks.java b/docklib/src/com/android/car/docklib/data/DockItemId.kt
similarity index 67%
copy from app/src/com/android/car/carlauncher/LaunchRootCarTaskViewCallbacks.java
copy to docklib/src/com/android/car/docklib/data/DockItemId.kt
index e30ffe5..0713039 100644
--- a/app/src/com/android/car/carlauncher/LaunchRootCarTaskViewCallbacks.java
+++ b/docklib/src/com/android/car/docklib/data/DockItemId.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 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.
@@ -14,10 +14,12 @@
* limitations under the License.
*/
-package com.android.car.carlauncher;
+package com.android.car.docklib.data
-/**
- * A callbacks interface for {@link LaunchRootCarTaskView}.
- */
-public interface LaunchRootCarTaskViewCallbacks extends
- CarTaskViewCallbacks {}
+@Target(
+ AnnotationTarget.VALUE_PARAMETER,
+ AnnotationTarget.TYPE
+)
+@Retention(AnnotationRetention.SOURCE)
+@MustBeDocumented
+annotation class DockItemId
diff --git a/docklib/src/com/android/car/docklib/data/DockProtoDataController.kt b/docklib/src/com/android/car/docklib/data/DockProtoDataController.kt
new file mode 100644
index 0000000..5695b1a
--- /dev/null
+++ b/docklib/src/com/android/car/docklib/data/DockProtoDataController.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2024 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.docklib.data
+
+import android.content.ComponentName
+import android.os.Build
+import android.util.Log
+import com.android.car.docklib.DockItemProto.DockAppItemListMessage
+import com.android.car.docklib.DockItemProto.DockAppItemMessage
+import java.io.File
+
+/**
+ * Proto file controller to read and write to store dock data
+ * @param dataFile a file that the current user has read and write permission
+ */
+class DockProtoDataController(dataFile: File) {
+ companion object {
+ private const val TAG = "DockProtoDataController"
+ private val DEBUG = Build.isDebuggable()
+ const val FILE_NAME = "dock_item_data"
+ }
+ private val dataSource = DockProtoDataSource(dataFile)
+
+ /**
+ * Load data from storage file
+ * @return mapping of a position to the component pinned to that position,
+ * or an empty mapping if the storage file doesn't exist
+ */
+ fun loadFromFile(): Map<Int, ComponentName>? {
+ if (DEBUG) Log.d(TAG, "Loading dock from file $dataSource")
+ return dataSource.readFromFile()?.let { dockAppItemListMessage ->
+ val items = HashMap<Int, ComponentName>()
+ dockAppItemListMessage.dockAppItemMessageList.forEach {
+ val componentName = ComponentName(it.packageName, it.className)
+ items[it.relativePosition] = componentName
+ }
+ if (DEBUG) Log.d(TAG, "Loaded dock from file $dataSource")
+ items
+ }
+ }
+
+ /**
+ * Create and write pinned dock items to file
+ * @param pinnedDockItems mapping of position to the component pinned to that position
+ */
+ fun savePinnedItemsToFile(pinnedDockItems: Map<Int, ComponentName>) {
+ if (DEBUG) Log.d(TAG, "Save dock to file $dataSource")
+ val data = DockAppItemListMessage.newBuilder()
+ pinnedDockItems.forEach() {
+ data.addDockAppItemMessage(
+ DockAppItemMessage.newBuilder()
+ .setPackageName(it.value.packageName)
+ .setClassName(it.value.className)
+ .setRelativePosition(it.key)
+ .build()
+ )
+ }
+ dataSource.writeToFile(data.build())
+ }
+}
diff --git a/docklib/src/com/android/car/docklib/data/DockProtoDataSource.kt b/docklib/src/com/android/car/docklib/data/DockProtoDataSource.kt
new file mode 100644
index 0000000..e9d5e6e
--- /dev/null
+++ b/docklib/src/com/android/car/docklib/data/DockProtoDataSource.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 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.docklib.data
+
+import com.android.car.carlaunchercommon.proto.ProtoDataSource
+import com.android.car.docklib.DockItemProto.DockAppItemListMessage
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+
+/**
+ * Proto file wrapper with helper methods
+ * @param dataFile a file that the current user has read and write permission
+ */
+class DockProtoDataSource(dataFile: File) : ProtoDataSource<DockAppItemListMessage>(dataFile) {
+ override fun parseDelimitedFrom(inputStream: InputStream?): DockAppItemListMessage {
+ return DockAppItemListMessage.parseDelimitedFrom(inputStream)
+ }
+
+ override fun writeDelimitedTo(outputData: DockAppItemListMessage, outputStream: OutputStream?) {
+ return outputData.writeDelimitedTo(outputStream)
+ }
+}
diff --git a/docklib/src/com/android/car/docklib/data/proto/dock_item.proto b/docklib/src/com/android/car/docklib/data/proto/dock_item.proto
new file mode 100644
index 0000000..43b0317
--- /dev/null
+++ b/docklib/src/com/android/car/docklib/data/proto/dock_item.proto
@@ -0,0 +1,16 @@
+syntax = "proto2";
+
+package com.android.car.docklib.data;
+
+option java_package = "com.android.car.docklib";
+option java_outer_classname = "DockItemProto";
+
+message DockAppItemMessage {
+ required int32 relativePosition = 1;
+ required string package_name = 2;
+ required string class_name = 3;
+}
+
+message DockAppItemListMessage {
+ repeated DockAppItemMessage dockAppItemMessage = 1;
+}
diff --git a/docklib/src/com/android/car/docklib/events/DockEventsReceiver.java b/docklib/src/com/android/car/docklib/events/DockEventsReceiver.java
index 8442a9d..101bd73 100644
--- a/docklib/src/com/android/car/docklib/events/DockEventsReceiver.java
+++ b/docklib/src/com/android/car/docklib/events/DockEventsReceiver.java
@@ -40,7 +40,6 @@
public class DockEventsReceiver extends BroadcastReceiver {
private static final String TAG = "DockEventsReceiver";
private static final boolean DEBUG = Build.isDebuggable();
- // Extras key for the ComponentName associated with the Event
private final DockInterface mDockController;
public DockEventsReceiver(DockInterface dockController) {
@@ -81,6 +80,7 @@
* @param context the context through which the DockEventsReceiver is registered
* @return successfully registered DockEventsReceiver.
*/
+ @NonNull
public static DockEventsReceiver registerDockReceiver(
@NonNull Context context,
@NonNull DockInterface dockController
diff --git a/docklib/src/com/android/car/docklib/events/DockPackageChangeReceiver.kt b/docklib/src/com/android/car/docklib/events/DockPackageChangeReceiver.kt
new file mode 100644
index 0000000..2a4164b
--- /dev/null
+++ b/docklib/src/com/android/car/docklib/events/DockPackageChangeReceiver.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2023 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.docklib.events
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
+import android.os.Build
+import android.util.Log
+import com.android.car.docklib.DockInterface
+
+class DockPackageChangeReceiver(
+ private val dockController: DockInterface
+) : BroadcastReceiver() {
+ companion object {
+ private val DEBUG = Build.isDebuggable()
+ private const val TAG = "DockPackageChangeReceiver"
+
+ /**
+ * Helper method to register [DockPackageChangeReceiver] through context and listen to
+ * changes to packages in the system.
+ *
+ * @param context the context through which the [DockPackageChangeReceiver] is registered
+ * @return successfully registered [DockPackageChangeReceiver].
+ */
+ fun registerReceiver(
+ context: Context,
+ dockController: DockInterface
+ ): DockPackageChangeReceiver {
+ val receiver = DockPackageChangeReceiver(dockController)
+ val filter = IntentFilter()
+ filter.addAction(Intent.ACTION_PACKAGE_ADDED)
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED)
+ filter.addAction(Intent.ACTION_PACKAGE_CHANGED)
+ filter.addDataScheme("package")
+ context.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED)
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "DockPackageChangeReceiver registered from package: " +
+ "${context.packageName}, for user ${context.userId}"
+ )
+ }
+ return receiver
+ }
+ }
+
+ override fun onReceive(context: Context?, intent: Intent?) {
+ intent?.data?.schemeSpecificPart?.let { packageName ->
+ if (DEBUG) Log.d(TAG, "package name: $packageName")
+ when (intent.action) {
+ Intent.ACTION_PACKAGE_ADDED -> {
+ if (intent.getBooleanExtra(
+ Intent.EXTRA_REPLACING,
+ false // defaultValue
+ )
+ ) {
+ return
+ }
+ if (DEBUG) Log.d(TAG, "ACTION_PACKAGE_ADDED")
+ dockController.packageAdded(packageName)
+ }
+
+ Intent.ACTION_PACKAGE_REMOVED -> {
+ if (intent.getBooleanExtra(
+ Intent.EXTRA_REPLACING,
+ false // defaultValue
+ )
+ ) {
+ return
+ }
+ if (DEBUG) Log.d(TAG, "ACTION_PACKAGE_REMOVED")
+ dockController.packageRemoved(packageName)
+ }
+
+ Intent.ACTION_PACKAGE_CHANGED -> {
+ if (DEBUG) Log.d(TAG, "ACTION_PACKAGE_CHANGED")
+ when (context?.packageManager?.getApplicationEnabledSetting(packageName)) {
+ COMPONENT_ENABLED_STATE_DISABLED, COMPONENT_ENABLED_STATE_DISABLED_USER -> {
+ if (DEBUG) Log.d(TAG, "package disabled")
+ dockController.packageRemoved(packageName)
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/docklib/src/com/android/car/docklib/media/MediaUtils.kt b/docklib/src/com/android/car/docklib/media/MediaUtils.kt
new file mode 100644
index 0000000..99fc15f
--- /dev/null
+++ b/docklib/src/com/android/car/docklib/media/MediaUtils.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2024 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.docklib.media
+
+import android.app.ActivityManager
+import android.car.media.CarMediaIntents
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.os.Build
+import android.service.media.MediaBrowserService
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import java.util.stream.Collectors
+
+class MediaUtils {
+ companion object {
+ private const val TAG = "MediaUtils"
+ private val DEBUG = Build.isDebuggable()
+
+ @VisibleForTesting
+ val CAR_MEDIA_ACTIVITY = ComponentName(
+ "com.android.car.media",
+ "com.android.car.media.MediaActivity"
+ )
+
+ @VisibleForTesting
+ const val CAR_MEDIA_DATA_SCHEME = "custom"
+
+ fun getMediaComponentName(taskInfo: ActivityManager.RunningTaskInfo): ComponentName? {
+ val data = taskInfo.baseIntent.data
+ if (data == null) {
+ if (DEBUG) Log.d(TAG, "No data attached to the base intent")
+ return null
+ }
+ if (CAR_MEDIA_DATA_SCHEME != data.scheme) {
+ if (DEBUG) Log.d(TAG, "Data scheme doesn't match")
+ return null
+ }
+ // should drop the first backslash that is part of the schemeSpecificPart
+ val ssp = data.schemeSpecificPart
+ val mediaComponentString = if (ssp.startsWith("/")) ssp.drop(1) else ssp
+ val mediaComponent = ComponentName.unflattenFromString(mediaComponentString)
+ if (DEBUG) Log.d(TAG, "Media component found: $mediaComponent")
+ return mediaComponent
+ }
+
+ fun isMediaComponent(component: ComponentName?) = component == CAR_MEDIA_ACTIVITY
+
+ fun createLaunchIntent(componentName: ComponentName) =
+ Intent(CarMediaIntents.ACTION_MEDIA_TEMPLATE)
+ .putExtra(CarMediaIntents.EXTRA_MEDIA_COMPONENT, componentName.flattenToString())
+
+ fun fetchMediaServiceComponents(
+ packageManager: PackageManager,
+ packageName: String? = null
+ ): MutableSet<ComponentName> {
+ val intent = Intent(MediaBrowserService.SERVICE_INTERFACE)
+ if (packageName != null) intent.setPackage(packageName)
+ return packageManager.queryIntentServices(
+ intent,
+ PackageManager.GET_RESOLVED_FILTER
+ ).stream()
+ .map { resolveInfo: ResolveInfo -> resolveInfo.serviceInfo.componentName }
+ .collect(Collectors.toSet())
+ }
+ }
+}
diff --git a/docklib/src/com/android/car/docklib/task/DockTaskStackChangeListener.java b/docklib/src/com/android/car/docklib/task/DockTaskStackChangeListener.java
index d23a98c..86903fa 100644
--- a/docklib/src/com/android/car/docklib/task/DockTaskStackChangeListener.java
+++ b/docklib/src/com/android/car/docklib/task/DockTaskStackChangeListener.java
@@ -17,34 +17,40 @@
import android.app.ActivityManager;
import android.content.ComponentName;
+import android.os.Build;
+import android.util.Log;
+import android.view.Display;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import com.android.car.docklib.DockInterface;
import com.android.systemui.shared.system.TaskStackChangeListener;
-import java.util.function.Consumer;
-
public class DockTaskStackChangeListener implements TaskStackChangeListener {
- Consumer<ComponentName> mTaskLaunchDelegate;
- public DockTaskStackChangeListener(Consumer<ComponentName> taskLaunchDelegate) {
- mTaskLaunchDelegate = taskLaunchDelegate;
+ private static final String TAG = "DockTaskStackChangeListener";
+ private static final boolean DEBUG = Build.isDebuggable();
+
+ private final DockInterface mDockController;
+ private final int mCurrentUserId;
+
+ public DockTaskStackChangeListener(int currentUserId, @NonNull DockInterface dockController) {
+ mDockController = dockController;
+ mCurrentUserId = currentUserId;
}
@Override
public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
- ComponentName component = getComponentName(taskInfo);
- mTaskLaunchDelegate.accept(component);
- }
-
- @Nullable
- private ComponentName getComponentName(@NonNull ActivityManager.RunningTaskInfo taskInfo) {
- if (taskInfo.baseActivity == null && taskInfo.baseIntent.getComponent() == null) {
- return null;
+ if (taskInfo.displayId != Display.DEFAULT_DISPLAY || taskInfo.userId != mCurrentUserId) {
+ if (DEBUG) {
+ Log.d(TAG, "New task on display " + taskInfo.displayId
+ + " and for user " + taskInfo.userId + " is not added to the dock");
+ }
+ return;
}
- return taskInfo.baseActivity != null ? taskInfo.baseActivity
- : taskInfo.baseIntent.getComponent();
+
+ ComponentName component = TaskUtils.Companion.getComponentName(taskInfo);
+ if (component != null) {
+ mDockController.appLaunched(component);
+ }
}
-
-
}
diff --git a/docklib/src/com/android/car/docklib/task/TaskUtils.kt b/docklib/src/com/android/car/docklib/task/TaskUtils.kt
new file mode 100644
index 0000000..bc074d1
--- /dev/null
+++ b/docklib/src/com/android/car/docklib/task/TaskUtils.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 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.docklib.task
+
+import android.app.ActivityManager
+import android.content.ComponentName
+import com.android.car.docklib.media.MediaUtils.Companion.getMediaComponentName
+import com.android.car.docklib.media.MediaUtils.Companion.isMediaComponent
+
+class TaskUtils {
+ companion object {
+ fun getComponentName(taskInfo: ActivityManager.RunningTaskInfo): ComponentName? {
+ if (taskInfo.baseActivity == null && taskInfo.baseIntent.component == null) {
+ return null
+ }
+ val component = if (taskInfo.baseActivity != null) {
+ taskInfo.baseActivity
+ } else {
+ taskInfo.baseIntent.component
+ }
+
+ return if (isMediaComponent(component)) getMediaComponentName(taskInfo) else component
+ }
+ }
+}
diff --git a/docklib/src/com/android/car/docklib/view/DockAdapter.kt b/docklib/src/com/android/car/docklib/view/DockAdapter.kt
index 5a5d746..7b9a9eb 100644
--- a/docklib/src/com/android/car/docklib/view/DockAdapter.kt
+++ b/docklib/src/com/android/car/docklib/view/DockAdapter.kt
@@ -1,150 +1,159 @@
+/*
+ * Copyright (C) 2023 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.docklib.view
-import android.car.content.pm.CarPackageManager
-import android.content.ComponentName
+import android.car.media.CarMediaManager
import android.content.Context
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.content.pm.PackageManager.NameNotFoundException
import android.os.Build
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
+import com.android.car.docklib.DockInterface
import com.android.car.docklib.R
import com.android.car.docklib.data.DockAppItem
-import java.util.function.Consumer
-/**
- * [RecyclerView.Adapter] used to bind Dock items
- * @param numItems maximum num of items present in the dock
- * @param items initial list of items in the Dock
- */
-class DockAdapter(
- private val numItems: Int,
- private val intentDelegate: Consumer<Intent>,
- private val userContext: Context,
- private val items: Array<DockAppItem?> = arrayOfNulls(numItems)
-) : RecyclerView.Adapter<DockItemViewHolder>() {
+/** [RecyclerView.Adapter] used to bind Dock items */
+class DockAdapter(private val dockController: DockInterface, private val userContext: Context) :
+ ListAdapter<DockAppItem, DockItemViewHolder>(DIFF_CALLBACK) {
companion object {
private val DEBUG = Build.isDebuggable()
private const val TAG = "DockAdapter"
}
- private var carPackageManager: CarPackageManager? = null
+ private var carMediaManager: CarMediaManager? = null
enum class PayloadType {
- CHANGE_SAME_ITEM_TYPE,
+ CHANGE_ITEM_TYPE,
+ CHANGE_UX_RESTRICTION_STATE,
+ CHANGE_ACTIVE_MEDIA_SESSION,
+ }
+
+ private val positionToCallbackMap = HashMap<Int, Runnable>()
+ private var isUxRestrictionEnabled = false
+ private var activeMediaSessions: List<String> = emptyList()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DockItemViewHolder {
+ val view = LayoutInflater.from(parent.context).inflate(
+ R.layout.dock_app_item_view, // resource
+ parent,
+ false // attachToRoot
+ )
+ return DockItemViewHolder(
+ dockController,
+ view,
+ userContext,
+ carMediaManager
+ )
}
override fun onBindViewHolder(
- viewHolder: DockItemViewHolder,
- position: Int,
- payloads: MutableList<Any>
+ viewHolder: DockItemViewHolder,
+ position: Int,
+ payloads: MutableList<Any>
) {
- if (payloads.isEmpty() ||
- payloads.getOrNull(0) == null ||
- payloads[0] !is PayloadType
- ) {
+ if (payloads.isEmpty()) {
return super.onBindViewHolder(viewHolder, position, payloads)
}
- when (payloads[0]) {
- PayloadType.CHANGE_SAME_ITEM_TYPE ->
- items[position]?.let {
- viewHolder.itemTypeChanged(it)
+ if (DEBUG) Log.d(TAG, "Binding at position $position with payloads")
+
+ payloads.forEach { payload ->
+ when (payload) {
+ PayloadType.CHANGE_ITEM_TYPE -> {
+ if (DEBUG) Log.d(TAG, "Type changed for position $position")
+ viewHolder.itemTypeChanged(currentList[position])
}
+ PayloadType.CHANGE_UX_RESTRICTION_STATE -> {
+ if (DEBUG) Log.d(TAG, "UX restriction changed for position $position")
+ viewHolder.setUxRestrictions(currentList[position], isUxRestrictionEnabled)
+ }
+ PayloadType.CHANGE_ACTIVE_MEDIA_SESSION -> {
+ if (DEBUG) Log.d(TAG, "Active MediaSession changed for position $position")
+ viewHolder.setHasActiveMediaSession(
+ activeMediaSessions.contains(currentList[position].component.packageName)
+ )
+ }
+ }
}
}
- override fun onCreateViewHolder(parent: ViewGroup, p1: Int): DockItemViewHolder {
- val view = LayoutInflater.from(parent.context).inflate(
- R.layout.dock_app_item_view, // resource
- parent,
- false // attachToRoot
- )
- return DockItemViewHolder(view, intentDelegate)
- }
-
- override fun getItemCount() = numItems
-
override fun onBindViewHolder(viewHolder: DockItemViewHolder, position: Int) {
- viewHolder.bind(items[position])
+ if (DEBUG) Log.d(TAG, "Binding at position $position without payloads")
+ val cleanupCallback = positionToCallbackMap.getOrDefault(
+ position,
+ null // defaultValue
+ )
+ if (DEBUG) Log.d(TAG, "Is callback set for $position: ${cleanupCallback != null}")
+ positionToCallbackMap.remove(position)
+ viewHolder.bind(
+ currentList[position],
+ isUxRestrictionEnabled,
+ cleanupCallback,
+ activeMediaSessions.contains(currentList[position].component.packageName)
+ )
}
- fun setItems(items: List<DockAppItem?>) {
- for (i in 0..<numItems) {
- if (this.items[i] != items.getOrNull(i)) {
- this.items[i] = items.getOrNull(i)
- notifyItemChanged(i)
- }
- }
+ /** Used to set a callback for the [position] to be passed to the ViewHolder on the next bind. */
+ fun setCallback(position: Int, callback: Runnable?) {
+ callback?.let { positionToCallbackMap[position] = it }
}
/**
- * Pin new app to the given position
+ * Setter for CarMediaManager
*/
- fun pinItemAt(position: Int, componentName: ComponentName) {
- // todo(b/315222570): move to controller
- if (!isValidPosition(position)) {
- return
- }
- try {
- val ai = userContext.packageManager
- .getActivityInfo(componentName, PackageManager.ComponentInfoFlags.of(0L))
- items[position] = DockAppItem(
- DockAppItem.Type.STATIC,
- componentName,
- ai.name,
- ai.loadIcon(userContext.packageManager),
- carPackageManager?.isActivityDistractionOptimized(
- componentName.packageName,
- componentName.className
- ) ?: false
- )
- notifyItemChanged(position)
- } catch (e: NameNotFoundException) {
- if (DEBUG) {
- // don't need to crash for a failed pin, log error instead
- Log.e(TAG, "Component $componentName not found, pinning failed $e")
- }
+ fun setCarMediaManager(carMediaManager: CarMediaManager) {
+ this.carMediaManager = carMediaManager
+ }
+
+ /** Set if the Ux restrictions are enabled */
+ fun setUxRestrictions(isUxRestrictionEnabled: Boolean) {
+ if (this.isUxRestrictionEnabled != isUxRestrictionEnabled) {
+ this.isUxRestrictionEnabled = isUxRestrictionEnabled
+ notifyItemRangeChanged(0, itemCount, PayloadType.CHANGE_UX_RESTRICTION_STATE)
}
}
- /**
- * Pin the DockItem at the given position. If the app is already pinned this call is a no-op.
- */
- fun pinItemAt(position: Int) {
- // todo(b/315222570): move to controller
- changeItemType(position, DockAppItem.Type.STATIC)
- }
-
- /**
- * Unpin the DockItem at the given position. If the app is already unpinned this call is a
- * no-op.
- */
- fun unpinItemAt(position: Int) {
- // todo(b/315222570): move to controller
- changeItemType(position, DockAppItem.Type.DYNAMIC)
- }
-
- private fun changeItemType(position: Int, newItemType: DockAppItem.Type) {
- if (!isValidPosition(position) || items[position]?.type == newItemType) {
- return
- }
- items[position]?.let {
- items[position] = it.copy(type = newItemType)
- notifyItemChanged(position, PayloadType.CHANGE_SAME_ITEM_TYPE)
+ /** Be notified that active media sessions have been changed */
+ fun onMediaSessionChange(activeMediaSessions: List<String>) {
+ if (this.activeMediaSessions != activeMediaSessions) {
+ this.activeMediaSessions = activeMediaSessions
+ notifyItemRangeChanged(0, itemCount, PayloadType.CHANGE_ACTIVE_MEDIA_SESSION)
}
}
+}
- /**
- * Setter for CarPackageManager
- */
- fun setCarPackageManager(carPackageManager: CarPackageManager) {
- this.carPackageManager = carPackageManager
+private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<DockAppItem>() {
+ override fun areItemsTheSame(p0: DockAppItem, p1: DockAppItem): Boolean {
+ return p0.id == p1.id
}
- private fun isValidPosition(position: Int): Boolean {
- return position >= 0 && position < items.size
+ override fun areContentsTheSame(p0: DockAppItem, p1: DockAppItem): Boolean {
+ return p0 == p1
+ }
+
+ override fun getChangePayload(
+ oldItem: DockAppItem,
+ newItem: DockAppItem
+ ): Any? {
+ if (oldItem.type != newItem.type) {
+ return DockAdapter.PayloadType.CHANGE_ITEM_TYPE
+ }
+ return super.getChangePayload(oldItem, newItem)
}
}
diff --git a/docklib/src/com/android/car/docklib/view/DockDragListener.kt b/docklib/src/com/android/car/docklib/view/DockDragListener.kt
index 7563bd4..fae4579 100644
--- a/docklib/src/com/android/car/docklib/view/DockDragListener.kt
+++ b/docklib/src/com/android/car/docklib/view/DockDragListener.kt
@@ -1,7 +1,24 @@
+/*
+ * Copyright (C) 2023 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.docklib.view
import android.content.ClipData
import android.content.ComponentName
+import android.content.res.Resources
import android.graphics.Point
import android.os.Build
import android.util.Log
@@ -9,22 +26,23 @@
import android.view.SurfaceControl
import android.view.SurfaceControl.Transaction
import android.view.View
+import androidx.annotation.OpenForTesting
import androidx.annotation.VisibleForTesting
import androidx.core.animation.Animator
+import androidx.core.animation.PathInterpolator
import androidx.core.animation.PropertyValuesHolder
import androidx.core.animation.ValueAnimator
-import androidx.recyclerview.widget.RecyclerView
import com.android.car.docklib.R
-import java.lang.Exception
-import java.lang.IndexOutOfBoundsException
+import java.util.function.Consumer
/**
- * {@link View.OnDragListener} for Dock. Receives a drop and moves it to correct location,
+ * [View.OnDragListener] for Dock. Receives a drop and moves it to correct location,
* transformed to the given size. This should be applied to all individual items in the dock that
* wants to receive a drop.
*/
+@OpenForTesting
open class DockDragListener(
- private val viewHolder: RecyclerView.ViewHolder,
+ resources: Resources,
private val callback: Callback
) : View.OnDragListener {
companion object {
@@ -47,12 +65,13 @@
private val DEBUG = Build.isDebuggable()
}
- private val animateInDuration: Int
-
- init {
- val resources = viewHolder.itemView.context.resources
- animateInDuration = resources.getInteger(R.integer.drag_drop_animate_in_duration)
- }
+ private val scaleDownDuration =
+ resources.getInteger(R.integer.drop_animation_scale_down_duration_ms).toLong()
+ private val scaleUpDuration =
+ resources.getInteger(R.integer.drop_animation_scale_up_duration_ms).toLong()
+ private val scaleDownWidth =
+ resources.getDimension(R.dimen.drop_animation_scale_down_width)
+ private var surfaceControl: SurfaceControl? = null
override fun onDrag(view: View, dragEvent: DragEvent): Boolean {
when (dragEvent.action) {
@@ -70,13 +89,6 @@
}
DragEvent.ACTION_DROP -> {
- if (viewHolder.bindingAdapterPosition == RecyclerView.NO_POSITION) {
- if (DEBUG) Log.d(TAG, "Drop at invalid position")
- callback.resetView()
- return false
- }
- if (DEBUG) Log.d(TAG, "Drop at position: " + viewHolder.bindingAdapterPosition)
-
val item: ClipData.Item
try {
item = dragEvent.clipData.getItemAt(0)
@@ -108,24 +120,16 @@
// todo(b/312718542): hidden api(dragEvent.dragSurface) usage
dragEvent.dragSurface?.let {
- callback.dragAccepted(component)
- animateSurfaceIn(it, dragEvent)
+ surfaceControl = it
+ animateSurfaceIn(it, dragEvent, component)
return true
} ?: run {
if (DEBUG) Log.d(TAG, "Could not retrieve the drag surface")
// drag is success but animation is not possible since there is no dragSurface
- callback.dragAccepted(component)
+ callback.dropSuccessful(component)
return false
}
}
-
- DragEvent.ACTION_DRAG_ENDED -> {
- if (!dragEvent.result) {
- // if drop was accepted the drop action should handle resetting
- callback.resetView()
- }
- return true
- }
}
return false
}
@@ -137,26 +141,76 @@
private fun animateSurfaceIn(
surfaceControl: SurfaceControl,
dragEvent: DragEvent,
+ component: ComponentName
) {
+ callback.dropAnimationsStarting(component)
val dropContainerLocation = callback.getDropContainerLocation()
// todo(b/312718542): hidden api(offsetX and offsetY) usage
val fromX: Float = dropContainerLocation.x + (dragEvent.x - dragEvent.offsetX)
val fromY: Float = dropContainerLocation.y + (dragEvent.y - dragEvent.offsetY)
val dropLocation = callback.getDropLocation()
- val toX: Float = dropLocation.x.toFloat()
- val toY: Float = dropLocation.y.toFloat()
+ val toFinalX: Float = dropLocation.x.toFloat()
+ val toFinalY: Float = dropLocation.y.toFloat()
+ val toScaleDownLocationX = toFinalX + scaleDownWidth
+ val toScaleDownLocationY = toFinalY + scaleDownWidth
- val toScaleX: Float = callback.getDropWidth() / surfaceControl.width
- val toScaleY: Float = callback.getDropHeight() / surfaceControl.height
+ val toFinalWidth: Float = callback.getDropWidth()
+ val toFinalHeight: Float = callback.getDropHeight()
+ val toFinalScaleX: Float = toFinalWidth / surfaceControl.width
+ val toFinalScaleY: Float = toFinalHeight / surfaceControl.height
+ val toScaleDownX: Float =
+ ((toFinalWidth - (scaleDownWidth * 2)) / surfaceControl.width).coerceAtLeast(0f)
+ val toScaleDownY: Float =
+ ((toFinalHeight - (scaleDownWidth * 2)) / surfaceControl.height).coerceAtLeast(0f)
+ if (DEBUG && (toScaleDownX <= 0 || toScaleDownY <= 0)) {
+ Log.w(
+ TAG,
+ "Reached negative/zero scale, decrease the value of " +
+ "drop_animation_scale_down_width"
+ )
+ }
- getAnimator(surfaceControl, fromX, fromY, toX, toY, toScaleX, toScaleY).start()
+ val scaleDownAnimator = getAnimator(
+ surfaceControl,
+ fromX = fromX,
+ fromY = fromY,
+ toX = toScaleDownLocationX,
+ toY = toScaleDownLocationY,
+ toScaleX = toScaleDownX,
+ toScaleY = toScaleDownY,
+ animationDuration = scaleDownDuration,
+ )
+ val scaleUpAnimator = getAnimator(
+ surfaceControl,
+ fromX = toScaleDownLocationX,
+ fromY = toScaleDownLocationY,
+ toX = toFinalX,
+ toY = toFinalY,
+ fromScaleX = toScaleDownX,
+ fromScaleY = toScaleDownY,
+ toScaleX = toFinalScaleX,
+ toScaleY = toFinalScaleY,
+ animationDuration = scaleUpDuration,
+ )
+
+ scaleDownAnimator.addListener(getAnimatorListener(onAnimationEnd = { isCancelled ->
+ if (!isCancelled) {
+ callback.dropAnimationScaleDownComplete(component)
+ scaleUpAnimator.start()
+ }
+ }))
+ scaleUpAnimator.addListener(getAnimatorListener(onAnimationEnd = { isCancelled ->
+ callback.dropAnimationComplete(component)
+ if (!isCancelled) callback.dropSuccessful(component, getCleanUpCallback(surfaceControl))
+ }))
+
+ scaleDownAnimator.start()
}
/**
- * Get the animator responsible for animating the {@code surfaceControl} from
- * {@code fromX, fromY} to its final position {@code toX, toY} with correct scale
- * {@code toScaleX, toScaleY}.
+ * Get the animator responsible for animating the [surfaceControl] from [fromX], [fromY]
+ * to its final position [toX], [toY] with correct scale [toScaleX], [toScaleY].
* Default values are added to make this method easier to test. Generally all parameters are
* expected to be sent by the caller.
*/
@@ -167,17 +221,33 @@
fromY: Float = 0f,
toX: Float = 0f,
toY: Float = 0f,
+ fromScaleX: Float = 1f,
+ fromScaleY: Float = 1f,
toScaleX: Float = 1f,
- toScaleY: Float = 1f
+ toScaleY: Float = 1f,
+ animationDuration: Long = 0L,
): ValueAnimator {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "getAnimator{ " +
+ "surfaceControl: $surfaceControl, " +
+ "(fromX: $fromX, fromY: $fromY), " +
+ "(toX: $toX, toY: $toY), " +
+ "(fromScaleX: $fromScaleX, fromScaleY: $fromScaleY), " +
+ "(toScaleX: $toScaleX, toScaleY: $toScaleY) " +
+ "}"
+ )
+ }
+
val pvhX: PropertyValuesHolder =
PropertyValuesHolder.ofFloat(PVH_POSITION_X, fromX, toX)
val pvhY: PropertyValuesHolder =
PropertyValuesHolder.ofFloat(PVH_POSITION_Y, fromY, toY)
val pvhScaleX =
- PropertyValuesHolder.ofFloat(PVH_SCALE_X, 1f, toScaleX)
+ PropertyValuesHolder.ofFloat(PVH_SCALE_X, fromScaleX, toScaleX)
val pvhScaleY =
- PropertyValuesHolder.ofFloat(PVH_SCALE_Y, 1f, toScaleY)
+ PropertyValuesHolder.ofFloat(PVH_SCALE_Y, fromScaleY, toScaleY)
val animator: ValueAnimator =
ValueAnimator.ofPropertyValuesHolder(
@@ -186,27 +256,25 @@
pvhScaleX,
pvhScaleY
)
- animator.setDuration(animateInDuration.toLong())
+ animator.setDuration(animationDuration)
+ animator.interpolator = PathInterpolator(0f, 0f, 0f, 1f)
val trx = Transaction()
animator.addUpdateListener(getAnimatorUpdateListener(surfaceControl, trx))
- animator.addListener(
- getAnimatorListener(surfaceControl, trx, cleanupTrx = Transaction())
- )
+ animator.addListener(getAnimatorListener { trx.close() })
return animator
}
/**
* Not expected to be used directly or overridden.
*
- * @param trx Transaction used to animate the {@code SurfaceControl} in place.
+ * @param trx Transaction used to animate the [surfaceControl] in place.
*/
@VisibleForTesting
fun getAnimatorUpdateListener(
surfaceControl: SurfaceControl,
trx: Transaction
): Animator.AnimatorUpdateListener {
- return Animator.AnimatorUpdateListener {
- updatedAnimation ->
+ return Animator.AnimatorUpdateListener { updatedAnimation ->
if (updatedAnimation is ValueAnimator) {
trx.setPosition(
surfaceControl,
@@ -222,17 +290,12 @@
}
/**
- * Not expected to be used directly or overridden.
- * {@code trx} and {@code cleanupTrx} will be closed by this listener.
- *
- * @param trx Transaction used to animate the {@code SurfaceControl} in place.
- * @param cleanupTrx Transaction used to animate out and hide {@code SurfaceControl}
+ * @param onAnimationEnd called with boolean(isCancelled) set to false when animation is ended
+ * and to true when cancelled.
*/
@VisibleForTesting
fun getAnimatorListener(
- surfaceControl: SurfaceControl,
- trx: Transaction,
- cleanupTrx: Transaction
+ onAnimationEnd: Consumer<Boolean>
): Animator.AnimatorListener {
return object : Animator.AnimatorListener {
private var isCancelled = false
@@ -242,41 +305,61 @@
override fun onAnimationEnd(var1: Animator) {
if (!isCancelled) {
- cleanup()
+ onAnimationEnd.accept(isCancelled)
}
}
override fun onAnimationCancel(var1: Animator) {
isCancelled = true
- cleanup()
+ onAnimationEnd.accept(isCancelled)
}
override fun onAnimationRepeat(var1: Animator) {
// no-op
}
+ }
+ }
- fun cleanup() {
- trx.close()
- if (surfaceControl.isValid) {
- // todo(b/312737692): add animations
- cleanupTrx.hide(surfaceControl)
- cleanupTrx.remove(surfaceControl)
- cleanupTrx.apply()
- cleanupTrx.close()
- }
+ private fun getCleanUpCallback(surfaceControl: SurfaceControl): () -> Unit {
+ return {
+ if (DEBUG) Log.d(TAG, "cleanup callback called")
+ if (surfaceControl.isValid) {
+ if (DEBUG) Log.d(TAG, "Surface is valid")
+ val cleanupTrx = Transaction()
+ cleanupTrx.hide(surfaceControl)
+ cleanupTrx.remove(surfaceControl)
+ cleanupTrx.apply()
+ cleanupTrx.close()
}
}
}
/**
- * {@link DockDragListener} communicates events back and requests data from the caller using
+ * [DockDragListener] communicates events back and requests data from the caller using
* this callback.
*/
interface Callback {
/**
- * Drag is accepted/successful for the {@code componentName}
+ * Drop is accepted/successful for the [componentName]
+ *
+ * @param cleanupCallback [Runnable] to be called when the dropped item is ready/drawn.
*/
- fun dragAccepted(componentName: ComponentName) {}
+ fun dropSuccessful(componentName: ComponentName, cleanupCallback: Runnable? = null) {}
+
+ /**
+ * Drop animations about to start.
+ */
+ fun dropAnimationsStarting(componentName: ComponentName) {}
+
+ /**
+ * Drop animation scale down completed.
+ */
+ fun dropAnimationScaleDownComplete(componentName: ComponentName) {}
+
+ /**
+ * Drop animation completed.
+ */
+ fun dropAnimationComplete(componentName: ComponentName) {}
/**
* Excite the view to indicate the item can be dropped in this position when dragged inside
diff --git a/docklib/src/com/android/car/docklib/view/DockItemClickListener.kt b/docklib/src/com/android/car/docklib/view/DockItemClickListener.kt
new file mode 100644
index 0000000..7e11ef8
--- /dev/null
+++ b/docklib/src/com/android/car/docklib/view/DockItemClickListener.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 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.docklib.view
+
+import android.view.View
+import com.android.car.carlaunchercommon.toasts.NonDrivingOptimizedLaunchFailedToast.Companion.showToast
+import com.android.car.docklib.DockInterface
+import com.android.car.docklib.data.DockAppItem
+
+/**
+ * [View.OnClickListener] for handling clicks on dock item.
+ *
+ * @property isRestricted if the item is restricted
+ */
+class DockItemClickListener(
+ private val dockController: DockInterface,
+ private val dockAppItem: DockAppItem,
+ private var isRestricted: Boolean,
+) : View.OnClickListener {
+ override fun onClick(v: View?) {
+ if (isRestricted) {
+ v?.context?.let { showToast(it, dockAppItem.name) }
+ return
+ }
+ dockController.launchApp(dockAppItem.component, dockAppItem.isMediaApp)
+ }
+
+ /**
+ * Set if the item is restricted.
+ */
+ fun setIsRestricted(isRestricted: Boolean) {
+ this.isRestricted = isRestricted
+ }
+}
diff --git a/docklib/src/com/android/car/docklib/view/DockItemLongClickListener.kt b/docklib/src/com/android/car/docklib/view/DockItemLongClickListener.kt
index 2e65b74..11a1eac 100644
--- a/docklib/src/com/android/car/docklib/view/DockItemLongClickListener.kt
+++ b/docklib/src/com/android/car/docklib/view/DockItemLongClickListener.kt
@@ -1,31 +1,68 @@
+/*
+ * Copyright (C) 2023 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.docklib.view
+import android.car.media.CarMediaManager
+import android.content.ComponentName
+import android.content.Context
import android.content.res.Resources
import android.view.View
import androidx.annotation.OpenForTesting
import androidx.annotation.VisibleForTesting
+import com.android.car.carlaunchercommon.shortcuts.AppInfoShortcutItem
+import com.android.car.carlaunchercommon.shortcuts.ForceStopShortcutItem
+import com.android.car.carlaunchercommon.shortcuts.PinShortcutItem
import com.android.car.docklib.data.DockAppItem
-import com.android.car.dockutil.shortcuts.PinShortcutItem
import com.android.car.ui.shortcutspopup.CarUiShortcutsPopup
/**
- * {@link View.OnLongClickListener} for handling long clicks on dock item.
+ * [View.OnLongClickListener] for handling long clicks on dock item.
* It is responsible to create and show th popup window
*
- * @param dockAppItem the {@link DockAppItem} to be used on long click.
- * @param pinItemClickDelegate called when item should be pinned at that position
- * @param unpinItemClickDelegate called when item should be unpinned at that position
+ * @property dockAppItem the [DockAppItem] to be used on long click.
+ * @property pinItemClickDelegate called when item should be pinned at that position
+ * @property unpinItemClickDelegate called when item should be unpinned at that position
+ * @property component [ComponentName] of the item at this position
+ * @property userContext [Context] for the current running user
+ * @property mediaServiceComponents list of [ComponentName] of the services the adhere to the media
+ * service interface
*/
@OpenForTesting
open class DockItemLongClickListener(
private var dockAppItem: DockAppItem,
private val pinItemClickDelegate: Runnable,
- private val unpinItemClickDelegate: Runnable
+ private val unpinItemClickDelegate: Runnable,
+ private val component: ComponentName,
+ private val userContext: Context,
+ private val carMediaManager: CarMediaManager?,
+ private val mediaServiceComponents: Set<ComponentName>
) : View.OnLongClickListener {
override fun onLongClick(view: View?): Boolean {
if (view == null) return false
createCarUiShortcutsPopupBuilder()
+ .addShortcut(ForceStopShortcutItem(
+ userContext,
+ component.packageName,
+ dockAppItem.name,
+ carMediaManager,
+ mediaServiceComponents
+ ))
+ .addShortcut(AppInfoShortcutItem(userContext, component.packageName, userContext.user))
.addShortcut(
createPinShortcutItem(
view.context.resources,
@@ -40,7 +77,7 @@
}
/**
- * Set the {@link DockAppItem} to be used on long click.
+ * Set the [DockAppItem] to be used on long click.
*/
fun setDockAppItem(dockAppItem: DockAppItem) {
this.dockAppItem = dockAppItem
diff --git a/docklib/src/com/android/car/docklib/view/DockItemViewController.kt b/docklib/src/com/android/car/docklib/view/DockItemViewController.kt
new file mode 100644
index 0000000..ad7aa52
--- /dev/null
+++ b/docklib/src/com/android/car/docklib/view/DockItemViewController.kt
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2024 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.docklib.view
+
+import android.content.res.ColorStateList
+import android.graphics.ColorFilter
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.os.Build
+import android.util.Log
+import androidx.core.animation.Animator
+import com.android.car.docklib.data.DockAppItem
+import com.android.car.docklib.view.animation.ExcitementAnimationHelper
+import com.google.android.material.imageview.ShapeableImageView
+import java.util.EnumSet
+import kotlin.math.floor
+
+/**
+ * Controller to help manage states for individual DockItemViews.
+ */
+class DockItemViewController(
+ private val staticIconStrokeWidth: Float,
+ private val dynamicIconStrokeWidth: Float,
+ private val excitedIconStrokeWidth: Float,
+ private val staticIconStrokeColor: Int,
+ private val excitedIconStrokeColor: Int,
+ private val restrictedIconStrokeColor: Int,
+ private val defaultIconColor: Int,
+ private val excitedColorFilter: ColorFilter,
+ private val restrictedColorFilter: ColorFilter,
+ private val excitedIconColorFilterAlpha: Float,
+ private val exciteAnimationDuration: Int,
+) {
+
+ companion object {
+ private val TAG = DockItemViewController::class.simpleName
+ private val DEBUG = Build.isDebuggable()
+ private const val DEFAULT_STROKE_WIDTH = 0f
+ private const val INITIAL_COLOR_FILTER_ALPHA = 0f
+ }
+
+ private enum class TypeStates {
+ DYNAMIC, STATIC
+ }
+
+ private enum class OptionalStates {
+ EXCITED, UPDATING, RESTRICTED, ACTIVE_MEDIA
+ }
+
+ private var dynamicIconStrokeColor: Int = defaultIconColor
+ private var updatingColor: Int = defaultIconColor
+ private var exciteAnimator: Animator? = null
+
+ private var typeState: Enum<TypeStates> = TypeStates.STATIC
+ private val optionalState: EnumSet<OptionalStates> = EnumSet.noneOf(OptionalStates::class.java)
+
+ /**
+ * Setter to set if the DockItem is dynamic.
+ */
+ fun setDynamic(dynamicIconStrokeColor: Int) {
+ typeState = TypeStates.DYNAMIC
+ this.dynamicIconStrokeColor = dynamicIconStrokeColor
+ }
+
+ /**
+ * Setter to set if the DockItem is static.
+ */
+ fun setStatic() {
+ typeState = TypeStates.STATIC
+ this.dynamicIconStrokeColor = defaultIconColor
+ }
+
+ /**
+ * Setter to set if the DockItem is excited. Returns true if the state was changed.
+ */
+ fun setExcited(isExcited: Boolean): Boolean {
+ if ((isExcited && optionalState.contains(OptionalStates.EXCITED)) ||
+ (!isExcited && !optionalState.contains(OptionalStates.EXCITED))
+ ) {
+ return false
+ }
+
+ if (isExcited) {
+ optionalState.add(OptionalStates.EXCITED)
+ } else {
+ optionalState.remove(OptionalStates.EXCITED)
+ }
+ return true
+ }
+
+ /**
+ * Setter to set if the DockItem is updating to another [DockAppItem]
+ * @param updatingColor color to use when app is updating. Generally the icon color of the next
+ * [DockAppItem]
+ * @return Returns true if the state was changed, false otherwise
+ */
+ fun setUpdating(isUpdating: Boolean, updatingColor: Int?): Boolean {
+ if ((isUpdating && optionalState.contains(OptionalStates.UPDATING)) ||
+ (!isUpdating && !optionalState.contains(OptionalStates.UPDATING))
+ ) {
+ return false
+ }
+
+ if (isUpdating) {
+ optionalState.add(OptionalStates.UPDATING)
+ } else {
+ optionalState.remove(OptionalStates.UPDATING)
+ }
+ this.updatingColor = updatingColor ?: defaultIconColor
+ return true
+ }
+
+ /**
+ * Setter to set if the DockItem is restricted. Returns true if the state was changed.
+ */
+ fun setRestricted(isRestricted: Boolean): Boolean {
+ if ((isRestricted && optionalState.contains(OptionalStates.RESTRICTED)) ||
+ (!isRestricted && !optionalState.contains(OptionalStates.RESTRICTED))
+ ) {
+ return false
+ }
+
+ if (isRestricted) {
+ optionalState.add(OptionalStates.RESTRICTED)
+ } else {
+ optionalState.remove(OptionalStates.RESTRICTED)
+ }
+ return true
+ }
+
+ /** Tracks whether the app item has an active media session or not */
+ fun setHasActiveMediaSession(
+ hasMediaSession: Boolean
+ ): Boolean {
+ if ((hasMediaSession && optionalState.contains(OptionalStates.ACTIVE_MEDIA)) ||
+ (!hasMediaSession && !optionalState.contains(OptionalStates.ACTIVE_MEDIA))
+ ) {
+ return false
+ }
+
+ if (hasMediaSession) {
+ optionalState.add(OptionalStates.ACTIVE_MEDIA)
+ } else {
+ optionalState.remove(OptionalStates.ACTIVE_MEDIA)
+ }
+ return true
+ }
+
+ /** @return whether the view should be restricted or not */
+ fun shouldBeRestricted(): Boolean {
+ return optionalState.contains(OptionalStates.RESTRICTED) &&
+ !optionalState.contains(OptionalStates.ACTIVE_MEDIA)
+ }
+
+ /**
+ * Updates the [appIcon] based on the current state
+ */
+ fun updateViewBasedOnState(appIcon: ShapeableImageView) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "updateViewBasedOnState, typeState: $typeState, optionalState: $optionalState"
+ )
+ }
+ if (exciteAnimator != null) {
+ exciteAnimator?.cancel()
+ exciteAnimator = null
+ }
+ appIcon.strokeColor = ColorStateList.valueOf(getStrokeColor())
+ appIcon.strokeWidth = getStrokeWidth()
+ appIcon.colorFilter = getColorFilter()
+ val cp = getContentPadding()
+ // ContentPadding should not be set before the measure phase of the view otherwise it might
+ // set incorrect padding values on the view.
+ appIcon.post { appIcon.setContentPadding(cp, cp, cp, cp) }
+ }
+
+ /**
+ * Animate the [appIcon] to be excited or reset after being excited.
+ */
+ fun animateAppIconExcited(appIcon: ShapeableImageView) {
+ val isAnimationOngoing = exciteAnimator?.isRunning ?: false
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Excite animation{ " +
+ "isExciting: ${optionalState.contains(OptionalStates.EXCITED)}, " +
+ "isAnimationOngoing: $isAnimationOngoing }"
+ )
+ }
+ exciteAnimator?.cancel()
+
+ val toStrokeWidth: Float = getStrokeWidth()
+ val toContentPadding: Int = getContentPadding()
+ val toStrokeColor: Int = getStrokeColor()
+ val toColorFilterAlpha: Float = if (optionalState.contains(OptionalStates.EXCITED)) {
+ excitedIconColorFilterAlpha
+ } else {
+ INITIAL_COLOR_FILTER_ALPHA
+ }
+
+ val successCallback = {
+ exciteAnimator = null
+ updateViewBasedOnState(appIcon)
+ }
+
+ val failureCallback = {
+ exciteAnimator = null
+ updateViewBasedOnState(appIcon)
+ }
+
+ exciteAnimator = ExcitementAnimationHelper.getExcitementAnimator(
+ appIcon,
+ exciteAnimationDuration.toLong(),
+ toStrokeWidth,
+ toStrokeColor,
+ toContentPadding,
+ toColorFilterAlpha,
+ successCallback,
+ failureCallback
+ )
+ exciteAnimator?.start()
+ }
+
+ private fun getStrokeColor(): Int {
+ if (optionalState.contains(OptionalStates.UPDATING)) {
+ return updatingColor
+ } else if (shouldBeRestricted()) {
+ return restrictedIconStrokeColor
+ } else if (optionalState.contains(OptionalStates.EXCITED)) {
+ return excitedIconStrokeColor
+ } else if (typeState == TypeStates.STATIC) {
+ return staticIconStrokeColor
+ } else if (typeState == TypeStates.DYNAMIC) {
+ return dynamicIconStrokeColor
+ }
+ return defaultIconColor
+ }
+
+ private fun getStrokeWidth(): Float {
+ if (optionalState.contains(OptionalStates.EXCITED)) {
+ return excitedIconStrokeWidth
+ } else if (typeState == TypeStates.STATIC) {
+ return staticIconStrokeWidth
+ } else if (typeState == TypeStates.DYNAMIC) {
+ return dynamicIconStrokeWidth
+ }
+ return DEFAULT_STROKE_WIDTH
+ }
+
+ private fun getContentPadding(): Int {
+ return getContentPaddingFromStrokeWidth(getStrokeWidth())
+ }
+
+ private fun getColorFilter(): ColorFilter? {
+ if (optionalState.contains(OptionalStates.UPDATING)) {
+ return PorterDuffColorFilter(updatingColor, PorterDuff.Mode.SRC_OVER)
+ } else if (shouldBeRestricted()){
+ return restrictedColorFilter
+ } else if (optionalState.contains(OptionalStates.EXCITED)) {
+ return excitedColorFilter
+ }
+ return null
+ }
+
+ private fun getContentPaddingFromStrokeWidth(strokeWidth: Float): Int =
+ floor(strokeWidth / 2).toInt()
+}
diff --git a/docklib/src/com/android/car/docklib/view/DockItemViewHolder.kt b/docklib/src/com/android/car/docklib/view/DockItemViewHolder.kt
index 9750f36..a718265 100644
--- a/docklib/src/com/android/car/docklib/view/DockItemViewHolder.kt
+++ b/docklib/src/com/android/car/docklib/view/DockItemViewHolder.kt
@@ -1,92 +1,170 @@
+/*
+ * Copyright (C) 2023 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.docklib.view
+import android.car.media.CarMediaManager
import android.content.ComponentName
-import android.content.Intent
-import android.content.res.ColorStateList
+import android.content.Context
import android.graphics.Color
+import android.graphics.ColorMatrix
+import android.graphics.ColorMatrixColorFilter
import android.graphics.Point
import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.os.Build
+import android.util.TypedValue
import android.view.View
-import androidx.core.view.setPadding
import androidx.recyclerview.widget.RecyclerView
+import com.android.car.docklib.DockInterface
import com.android.car.docklib.R
import com.android.car.docklib.data.DockAppItem
import com.google.android.material.imageview.ShapeableImageView
-import java.util.function.Consumer
+import java.util.concurrent.Callable
+import java.util.concurrent.Executors
+import java.util.concurrent.Future
+import java.util.concurrent.TimeUnit
+/**
+ * ViewHolder of {@link DockAppItem}
+ */
class DockItemViewHolder(
- itemView: View,
- private val intentDelegate: Consumer<Intent>,
+ private val dockController: DockInterface,
+ itemView: View,
+ private val userContext: Context,
+ private val carMediaManager: CarMediaManager?
) : RecyclerView.ViewHolder(itemView) {
companion object {
- private const val DEFAULT_STROKE_WIDTH = 0f
+ private const val TAG = "DockItemViewHolder"
+ private val DEBUG = Build.isDebuggable()
+
+ /**
+ * Cleanup callback is used to reset/remove any pending views so it should be called after
+ * the new item is ready to be shown. This delay ensures new view is ready before the
+ * cleanup.
+ *
+ * todo(b/319285942): Remove fixed timer
+ */
+ private const val CLEANUP_DELAY = 500L
+ private const val MAX_WAIT_TO_COMPUTE_ICON_COLOR_MS = 500L
}
- private val staticIconStrokeWidth: Float
- private val dynamicIconStrokeWidth: Float
- private val excitedIconStrokeWidth: Float
- private val iconStrokeColor: Int
- private val excitedIconStrokeColor: Int
- private val appIcon: ShapeableImageView
+ private val staticIconStrokeWidth = itemView.resources
+ .getDimension(R.dimen.icon_stroke_width_static)
+ private val defaultIconColor = itemView.resources.getColor(
+ R.color.icon_default_color,
+ null // theme
+ )
+ private val appIcon: ShapeableImageView = itemView.requireViewById(R.id.dock_app_icon)
+ private val iconColorExecutor = Executors.newSingleThreadExecutor()
+ private val dockDragListener: DockDragListener
+ private val dockItemViewController: DockItemViewController
+ private var dockItemClickListener: DockItemClickListener? = null
private var dockItemLongClickListener: DockItemLongClickListener? = null
- private var iconStrokeWidth: Float = DEFAULT_STROKE_WIDTH
+ private var droppedIconColor: Int = defaultIconColor
+ private var iconColorFuture: Future<Int>? = null
init {
- staticIconStrokeWidth = itemView.resources.getDimension(R.dimen.static_icon_stroke_width)
- dynamicIconStrokeWidth = itemView.resources.getDimension(R.dimen.dynamic_icon_stroke_width)
- excitedIconStrokeWidth = itemView.resources.getDimension(R.dimen.icon_stroke_width_excited)
- // todo(b/314859977): iconStrokeColor should be decided by the app primary color
- iconStrokeColor = itemView.resources.getColor(
- R.color.icon_default_stroke_color,
- null // theme
+ val typedValue = TypedValue()
+ itemView.resources.getValue(
+ R.dimen.icon_colorFilter_alpha_excited,
+ typedValue,
+ true // resolveRefs
)
- excitedIconStrokeColor = itemView.resources.getColor(
- R.color.icon_excited_stroke_color,
- null // theme
+ val excitedIconColorFilterAlpha = typedValue.float
+
+ dockItemViewController = DockItemViewController(
+ staticIconStrokeWidth,
+ dynamicIconStrokeWidth = itemView.resources
+ .getDimension(R.dimen.icon_stroke_width_dynamic),
+ excitedIconStrokeWidth = itemView.resources
+ .getDimension(R.dimen.icon_stroke_width_excited),
+ staticIconStrokeColor = itemView.resources.getColor(
+ R.color.icon_static_stroke_color,
+ null // theme
+ ),
+ excitedIconStrokeColor = itemView.resources.getColor(
+ R.color.icon_excited_stroke_color,
+ null // theme
+ ),
+ restrictedIconStrokeColor = itemView.resources.getColor(
+ R.color.icon_restricted_stroke_color,
+ null // theme
+ ),
+ defaultIconColor,
+ excitedColorFilter = PorterDuffColorFilter(
+ Color.argb(excitedIconColorFilterAlpha, 0f, 0f, 0f),
+ PorterDuff.Mode.DARKEN
+ ),
+ restrictedColorFilter = ColorMatrixColorFilter(
+ ColorMatrix().apply { setSaturation(0f) }
+ ),
+ excitedIconColorFilterAlpha,
+ exciteAnimationDuration = itemView.resources
+ .getInteger(R.integer.excite_icon_animation_duration_ms)
)
- appIcon = itemView.requireViewById(R.id.dock_app_icon)
- }
- fun bind(dockAppItem: DockAppItem?) {
- reset()
- if (dockAppItem == null) return
-
- itemTypeChanged(dockAppItem)
-
- appIcon.contentDescription = dockAppItem.name
- appIcon.setImageDrawable(dockAppItem.icon)
- appIcon.setOnClickListener {
- val intent =
- Intent(Intent.ACTION_MAIN)
- .setComponent(dockAppItem.component)
- .addCategory(Intent.CATEGORY_LAUNCHER)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- intentDelegate.accept(intent)
- }
- dockItemLongClickListener = DockItemLongClickListener(
- dockAppItem,
- pinItemClickDelegate =
- { (bindingAdapter as? DockAdapter)?.pinItemAt(bindingAdapterPosition) },
- unpinItemClickDelegate =
- { (bindingAdapter as? DockAdapter)?.unpinItemAt(bindingAdapterPosition) }
- )
- appIcon.onLongClickListener = dockItemLongClickListener
-
- itemView.setOnDragListener(
- DockDragListener(
- viewHolder = this,
+ dockDragListener = DockDragListener(
+ resources = this.itemView.resources,
object : DockDragListener.Callback {
- override fun dragAccepted(componentName: ComponentName) {
- pinNewItem(componentName)
+ override fun dropSuccessful(
+ componentName: ComponentName,
+ cleanupCallback: Runnable?
+ ) {
+ (bindingAdapter as? DockAdapter)
+ ?.setCallback(bindingAdapterPosition, cleanupCallback)
+ dockController.appPinned(componentName, bindingAdapterPosition)
+ }
+
+ override fun dropAnimationsStarting(componentName: ComponentName) {
+ dockItemViewController.setExcited(isExcited = false)
+ // todo(b/320543972): Increase efficiency of dropping
+ iconColorFuture = iconColorExecutor.submit(
+ Callable { dockController.getIconColorWithScrim(componentName) }
+ )
+ }
+
+ override fun dropAnimationScaleDownComplete(componentName: ComponentName) {
+ droppedIconColor = iconColorFuture?.get(
+ MAX_WAIT_TO_COMPUTE_ICON_COLOR_MS,
+ TimeUnit.MILLISECONDS
+ ) ?: defaultIconColor
+ if (dockItemViewController.setUpdating(
+ isUpdating = true,
+ updatingColor = droppedIconColor
+ )) {
+ dockItemViewController.updateViewBasedOnState(appIcon)
+ }
+ }
+
+ override fun dropAnimationComplete(componentName: ComponentName) {
+ dockItemViewController.setUpdating(isUpdating = false, updatingColor = null)
}
override fun exciteView() {
- exciteAppIcon()
+ if (dockItemViewController.setExcited(isExcited = true)) {
+ dockItemViewController.animateAppIconExcited(appIcon)
+ }
}
override fun resetView() {
- resetAppIcon()
+ if (dockItemViewController.setExcited(isExcited = false)) {
+ dockItemViewController.animateAppIconExcited(appIcon)
+ }
}
override fun getDropContainerLocation(): Point {
@@ -97,65 +175,83 @@
override fun getDropLocation(): Point {
val iconLocation = appIcon.locationOnScreen
return Point(
- (iconLocation[0] + iconStrokeWidth.toInt()),
- (iconLocation[1] + iconStrokeWidth.toInt())
+ (iconLocation[0] + staticIconStrokeWidth.toInt()),
+ (iconLocation[1] + staticIconStrokeWidth.toInt())
)
}
override fun getDropWidth(): Float {
- return (appIcon.width.toFloat() - iconStrokeWidth * 2)
+ return (appIcon.width.toFloat() - staticIconStrokeWidth * 2)
}
override fun getDropHeight(): Float {
- return (appIcon.height.toFloat() - iconStrokeWidth * 2)
+ return (appIcon.height.toFloat() - staticIconStrokeWidth * 2)
}
- }
- )
+ })
+ }
+
+ /**
+ * @param callback [Runnable] to be called after the new item is bound
+ */
+ fun bind(
+ dockAppItem: DockAppItem,
+ isUxRestrictionEnabled: Boolean,
+ callback: Runnable? = null,
+ hasActiveMediaSessions: Boolean
+ ) {
+ itemTypeChanged(dockAppItem)
+ appIcon.contentDescription = dockAppItem.name
+ appIcon.setImageDrawable(dockAppItem.icon)
+ appIcon.postDelayed({ callback?.run() }, CLEANUP_DELAY)
+ dockItemClickListener = DockItemClickListener(
+ dockController,
+ dockAppItem,
+ isRestricted = !dockAppItem.isDistractionOptimized && isUxRestrictionEnabled &&
+ !hasActiveMediaSessions
)
+ appIcon.setOnClickListener(dockItemClickListener)
+ setUxRestrictions(dockAppItem, isUxRestrictionEnabled)
+ setHasActiveMediaSession(hasActiveMediaSessions)
+ dockItemLongClickListener = DockItemLongClickListener(
+ dockAppItem,
+ pinItemClickDelegate = { dockController.appPinned(dockAppItem.id) },
+ unpinItemClickDelegate = { dockController.appUnpinned(dockAppItem.id) },
+ dockAppItem.component,
+ userContext,
+ carMediaManager,
+ dockController.getMediaServiceComponents()
+ )
+ appIcon.onLongClickListener = dockItemLongClickListener
+
+ itemView.setOnDragListener(dockDragListener)
}
fun itemTypeChanged(dockAppItem: DockAppItem) {
- iconStrokeWidth = when (dockAppItem.type) {
- DockAppItem.Type.STATIC -> staticIconStrokeWidth
- DockAppItem.Type.DYNAMIC -> dynamicIconStrokeWidth
- }
- appIcon.strokeWidth = iconStrokeWidth
+ when (dockAppItem.type) {
+ DockAppItem.Type.DYNAMIC ->
+ dockItemViewController.setDynamic(dockAppItem.iconColorWithScrim)
- appIcon.invalidate()
+ DockAppItem.Type.STATIC -> dockItemViewController.setStatic()
+ }
+ dockItemViewController.updateViewBasedOnState(appIcon)
dockItemLongClickListener?.setDockAppItem(dockAppItem)
}
- private fun pinNewItem(componentName: ComponentName) {
- (bindingAdapter as? DockAdapter)?.pinItemAt(bindingAdapterPosition, componentName)
+ /** Set if the Ux restrictions are enabled */
+ fun setUxRestrictions(dockAppItem: DockAppItem, isUxRestrictionEnabled: Boolean) {
+ val shouldBeRestricted = !dockAppItem.isDistractionOptimized && isUxRestrictionEnabled
+ if (dockItemViewController.setRestricted(shouldBeRestricted)) {
+ dockItemViewController.updateViewBasedOnState(appIcon)
+ dockItemClickListener?.setIsRestricted(dockItemViewController.shouldBeRestricted())
+ }
}
- private fun exciteAppIcon() {
- // todo(b/312737692): add animations
- appIcon.strokeColor = ColorStateList.valueOf(excitedIconStrokeColor)
- appIcon.setColorFilter(Color.argb(0.3f, 0f, 0f, 0f), PorterDuff.Mode.DARKEN)
- appIcon.strokeWidth = excitedIconStrokeWidth
- appIcon.setPadding(getPaddingFromStrokeWidth(excitedIconStrokeWidth))
- appIcon.invalidate()
+ /** Set if item has an active media session */
+ fun setHasActiveMediaSession(hasActiveMediaSession: Boolean) {
+ if (dockItemViewController.setHasActiveMediaSession(hasActiveMediaSession)) {
+ dockItemViewController.updateViewBasedOnState(appIcon)
+ dockItemClickListener?.setIsRestricted(dockItemViewController.shouldBeRestricted())
+ }
}
-
- private fun resetAppIcon() {
- appIcon.strokeColor = ColorStateList.valueOf(iconStrokeColor)
- appIcon.colorFilter = null
- appIcon.strokeWidth = iconStrokeWidth
- appIcon.setPadding(getPaddingFromStrokeWidth(iconStrokeWidth))
- appIcon.invalidate()
- }
-
- private fun reset() {
- iconStrokeWidth = DEFAULT_STROKE_WIDTH
- resetAppIcon()
- appIcon.contentDescription = null
- appIcon.setImageDrawable(null)
- appIcon.setOnClickListener(null)
- itemView.setOnDragListener(null)
- }
-
- private fun getPaddingFromStrokeWidth(strokeWidth: Float): Int = (strokeWidth / 2).toInt()
-
// TODO: b/301484526 Add animation when app icon is changed
}
diff --git a/docklib/src/com/android/car/docklib/view/DockView.kt b/docklib/src/com/android/car/docklib/view/DockView.kt
index c0b2564..858ce3d 100644
--- a/docklib/src/com/android/car/docklib/view/DockView.kt
+++ b/docklib/src/com/android/car/docklib/view/DockView.kt
@@ -1,12 +1,25 @@
+/*
+ * Copyright (C) 2023 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.docklib.view
import android.content.Context
-import android.graphics.Rect
import android.util.AttributeSet
-import android.view.View
import android.widget.FrameLayout
import androidx.recyclerview.widget.RecyclerView
-import androidx.recyclerview.widget.RecyclerView.ItemDecoration
import com.android.car.docklib.R
class DockView
@@ -23,23 +36,6 @@
init {
inflate(context, R.layout.dock_view, this)
recyclerView = requireViewById(R.id.recycler_view)
- recyclerView.addItemDecoration(
- object : ItemDecoration() {
- override fun getItemOffsets(
- outRect: Rect,
- view: View,
- parent: RecyclerView,
- state: RecyclerView.State
- ) {
- with(outRect) {
- if (parent.getChildAdapterPosition(view) != 0) {
- // TODO: b/301484526 set margins in case of RTL and vertical
- left = resources.getDimensionPixelSize(R.dimen.dock_item_spacing)
- }
- }
- }
- }
- )
}
fun getAdapter() = recyclerView.adapter as DockAdapter
diff --git a/docklib/src/com/android/car/docklib/view/animation/ExcitementAnimationHelper.kt b/docklib/src/com/android/car/docklib/view/animation/ExcitementAnimationHelper.kt
new file mode 100644
index 0000000..898300b
--- /dev/null
+++ b/docklib/src/com/android/car/docklib/view/animation/ExcitementAnimationHelper.kt
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2023 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.docklib.view.animation
+
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.os.Build
+import android.util.Log
+import android.util.Property
+import android.view.View
+import android.widget.ImageView
+import androidx.annotation.ColorInt
+import androidx.annotation.VisibleForTesting
+import androidx.core.animation.Animator
+import androidx.core.animation.ArgbEvaluator
+import androidx.core.animation.ObjectAnimator
+import androidx.core.animation.PathInterpolator
+import androidx.core.animation.PropertyValuesHolder
+import androidx.core.graphics.alpha
+import androidx.core.graphics.toColorLong
+import com.google.android.material.imageview.ShapeableImageView
+
+/**
+ * Helper class to build an [Animator] that can animate a view to be excited or reset. This is
+ * generally used to excite the dock item when something is hovered over it.
+ */
+class ExcitementAnimationHelper {
+ companion object {
+ private const val TAG = "ExcitementAnimator"
+ private val DEBUG = Build.isDebuggable()
+ private const val PVH_STROKE_WIDTH = "strokeWidth"
+ private const val PVH_STROKE_COLOR = "strokeColor"
+ private const val PVH_PADDING = "padding"
+ private const val PVH_COLOR_FILTER = "colorFilter"
+
+ /**
+ * Creates an [Animator] using the final values to be animated to. The initial values to
+ * start the animation from are taken from the View.
+ *
+ * Some assumptions:
+ * 1. strokeWidth, strokeColor and contentPadding will only be animated if the given [view]
+ * is of type [ShapeableImageView]
+ * 2. colorFilter will only be animated if the given [view] is of type [ImageView]
+ * 3. The colorFilter set on the [view] should be [PorterDuffColorFilter] to be able to get
+ * the initial value from.
+ *
+ * @param view [View] that should be animated and sourced the initial animation values
+ * from.
+ * @param animationDuration length of the animation
+ * @param toStrokeWidth final strokeWidth value
+ * @param toStrokeColor final strokeColor value
+ * @param toContentPadding final content padding value, applied to all sides
+ * @param toColorFilterAlpha final alpha value of the colorFilter
+ * @param successCallback called when the animation has completed successfully
+ * @param failureCallback called when the animation is unsuccessful/cancelled
+ */
+ fun getExcitementAnimator(
+ view: View,
+ animationDuration: Long,
+ toStrokeWidth: Float,
+ @ColorInt toStrokeColor: Int,
+ toContentPadding: Int,
+ toColorFilterAlpha: Float,
+ successCallback: Runnable,
+ failureCallback: Runnable
+ ): Animator {
+ // todo(b/312718542): hidden api(PorterDuffColorFilter.getAlpha) usage
+ return getExcitementAnimator(
+ view,
+ animationDuration,
+ fromStrokeWidth = getStrokeWidth(view, defaultWidth = 0f),
+ toStrokeWidth = toStrokeWidth,
+ fromStrokeColor = getStrokeColor(view, defaultColor = Color.rgb(0f, 0f, 0f)),
+ toStrokeColor = toStrokeColor,
+ fromPadding = getAverageContentPadding(view, defaultContentPadding = 0),
+ toContentPadding = toContentPadding,
+ fromColorFilterAlpha = getColorFilterAlpha(view, defaultColorFilterAlpha = 0f),
+ toColorFilterAlpha = toColorFilterAlpha,
+ successCallback,
+ failureCallback
+ )
+ }
+
+ private fun getExcitementAnimator(
+ view: View,
+ animationDuration: Long,
+ fromStrokeWidth: Float,
+ toStrokeWidth: Float,
+ @ColorInt fromStrokeColor: Int,
+ @ColorInt toStrokeColor: Int,
+ fromPadding: Int,
+ toContentPadding: Int,
+ fromColorFilterAlpha: Float,
+ toColorFilterAlpha: Float,
+ successCallback: Runnable,
+ failureCallback: Runnable
+ ): Animator {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "getExcitementAnimator{" +
+ "view: $view, " +
+ "animationDuration: $animationDuration, " +
+ "fromStrokeWidth: $fromStrokeWidth," +
+ "toStrokeWidth: $toStrokeWidth," +
+ "fromStrokeColor: $fromStrokeColor," +
+ "toStrokeColor: $toStrokeColor," +
+ "fromPadding: $fromPadding," +
+ "toContentPadding: $toContentPadding," +
+ "fromColorFilterAlpha: $fromColorFilterAlpha," +
+ "toColorFilterAlpha: $toColorFilterAlpha," +
+ "}"
+ )
+ }
+ var pvhStrokeWidth: PropertyValuesHolder? = null
+ val pvhPadding: PropertyValuesHolder?
+ var pvhColorFilter: PropertyValuesHolder? = null
+ var pvhStrokeColor: PropertyValuesHolder? = null
+
+ if (view is ShapeableImageView) {
+ pvhStrokeWidth = PropertyValuesHolder.ofFloat(
+ PVH_STROKE_WIDTH,
+ fromStrokeWidth,
+ toStrokeWidth
+ )
+
+ pvhStrokeColor =
+ PropertyValuesHolder.ofObject(
+ object : Property<View, Int>(Int::class.java, PVH_STROKE_COLOR) {
+ override fun get(view: View?): Int {
+ return getStrokeColor(view, defaultColor = fromStrokeColor)
+ }
+
+ override fun set(view: View?, value: Int?) {
+ if (view is ShapeableImageView && value != null) {
+ view.strokeColor = ColorStateList.valueOf(value)
+ }
+ }
+ },
+ ArgbEvaluator.getInstance(),
+ fromStrokeColor,
+ toStrokeColor
+ )
+ }
+
+ pvhPadding = PropertyValuesHolder.ofInt(
+ object : Property<View, Int>(Int::class.java, PVH_PADDING) {
+ override fun get(view: View?): Int {
+ return if (view != null) {
+ getAverageContentPadding(view, defaultContentPadding = 0)
+ } else {
+ 0
+ }
+ }
+
+ override fun set(view: View?, value: Int?) {
+ if (view != null && value != null) setContentPadding(view, value)
+ }
+ },
+ fromPadding,
+ toContentPadding
+ )
+
+ if (view is ImageView) {
+ pvhColorFilter =
+ PropertyValuesHolder.ofFloat(
+ object : Property<View, Float>(
+ Float::class.java,
+ PVH_COLOR_FILTER
+ ) {
+ override fun get(view: View?): Float {
+ return getColorFilterAlpha(view, defaultColorFilterAlpha = 0f)
+ }
+
+ override fun set(view: View?, value: Float?) {
+ if (view is ImageView && value != null) {
+ view.colorFilter = PorterDuffColorFilter(
+ Color.argb(value, 0f, 0f, 0f),
+ PorterDuff.Mode.DARKEN
+ )
+ }
+ }
+ },
+ fromColorFilterAlpha,
+ toColorFilterAlpha
+ )
+ }
+
+ val animator = ObjectAnimator.ofPropertyValuesHolder(
+ view,
+ pvhStrokeWidth,
+ pvhPadding,
+ pvhColorFilter,
+ pvhStrokeColor
+ )
+ animator.setDuration(animationDuration)
+ animator.interpolator = PathInterpolator(0f, 0f, 0f, 1f)
+ animator.addListener(getAnimatorListener(successCallback, failureCallback))
+ return animator
+ }
+
+ @VisibleForTesting
+ fun getAnimatorListener(
+ successCallback: Runnable,
+ failureCallback: Runnable
+ ): Animator.AnimatorListener {
+ return object : Animator.AnimatorListener {
+ private var isCancelled = false
+ override fun onAnimationStart(animator: Animator) {
+ isCancelled = false
+ }
+
+ override fun onAnimationEnd(animator: Animator) {
+ if (!isCancelled) successCallback.run()
+ }
+
+ override fun onAnimationCancel(animator: Animator) {
+ isCancelled = true
+ failureCallback.run()
+ }
+
+ override fun onAnimationRepeat(animator: Animator) {
+ // no-op
+ }
+ }
+ }
+
+ private fun setContentPadding(view: View, contentPadding: Int) {
+ (view as? ShapeableImageView)?.setContentPadding(
+ contentPadding,
+ contentPadding,
+ contentPadding,
+ contentPadding
+ )
+ }
+
+ private fun getAverageContentPadding(view: View, defaultContentPadding: Int): Int {
+ (view as? ShapeableImageView)?.let {
+ return (it.contentPaddingStart + it.contentPaddingEnd +
+ it.contentPaddingTop + it.contentPaddingBottom) / 4
+ }
+ return defaultContentPadding
+ }
+
+ private fun getColorFilterAlpha(view: View?, defaultColorFilterAlpha: Float): Float {
+ return ((view as? ImageView)
+ ?.colorFilter as? PorterDuffColorFilter)
+ ?.color?.toColorLong()?.alpha ?: defaultColorFilterAlpha
+ }
+
+ private fun getStrokeWidth(view: View?, defaultWidth: Float): Float {
+ return (view as? ShapeableImageView)?.strokeWidth ?: defaultWidth
+ }
+
+ private fun getStrokeColor(view: View?, @ColorInt defaultColor: Int): Int {
+ return (view as? ShapeableImageView)
+ ?.strokeColor?.getColorForState(null, defaultColor) ?: defaultColor
+ }
+ }
+}
diff --git a/docklib/tests/Android.bp b/docklib/tests/Android.bp
index dd6432e..49b2272 100644
--- a/docklib/tests/Android.bp
+++ b/docklib/tests/Android.bp
@@ -1,5 +1,5 @@
//
-// Copyright (C) 2023 Google Inc.
+// Copyright (C) 2024 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.
@@ -16,6 +16,7 @@
package {
default_applicable_licenses: ["Android-Apache-2.0"],
+ default_team: "trendy_team_system_experience",
}
android_test {
@@ -23,12 +24,12 @@
srcs: [
"src/**/*.java",
- "src/**/*.kt"
+ "src/**/*.kt",
],
libs: [
"android.car",
- "android.test.base",
+ "android.test.base.stubs.system",
],
optimize: {
diff --git a/docklib/tests/src/com/android/car/docklib/DockHelperTest.kt b/docklib/tests/src/com/android/car/docklib/DockHelperTest.kt
deleted file mode 100644
index 1e788f6..0000000
--- a/docklib/tests/src/com/android/car/docklib/DockHelperTest.kt
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * Copyright (C) 2023 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.docklib
-
-import android.car.content.pm.CarPackageManager
-import android.content.Context
-import android.content.pm.ActivityInfo
-import android.content.pm.PackageManager
-import android.content.res.Resources
-import android.graphics.drawable.Drawable
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.`when`
-
-@RunWith(AndroidJUnit4::class)
-class DockHelperTest {
-
- private val context = mock(Context::class.java)
- private val carPackageManager = mock(CarPackageManager::class.java)
- private val packageManager = mock(PackageManager::class.java)
-
- @Test
- fun defaultApps_getCorrectAppsFromConfig() {
- val resources = mock(Resources::class.java)
- val item1 =
- TestUtils.createAppItem(
- app = "item1",
- name = "item1",
- icon = mock(Drawable::class.java),
- isDrivingOptimized = true
- )
- val item2 =
- TestUtils.createAppItem(
- app = "item2",
- name = "item2",
- icon = mock(Drawable::class.java),
- isDrivingOptimized = false
- )
- `when`(context.resources).thenReturn(resources)
- `when`(context.packageManager).thenReturn(packageManager)
- `when`(resources.getStringArray(anyInt()))
- .thenReturn(
- arrayOf(item1.component.flattenToString(), item2.component.flattenToString())
- )
- `when`(packageManager.getApplicationIcon(item1.component.packageName))
- .thenReturn(item1.icon)
- `when`(packageManager.getApplicationIcon(item2.component.packageName))
- .thenReturn(item2.icon)
- val activityInfo1 = mock(ActivityInfo::class.java)
- activityInfo1.name = item1.name
- `when`(packageManager.getActivityInfo(item1.component, 0)).thenReturn(activityInfo1)
- val activityInfo2 = mock(ActivityInfo::class.java)
- activityInfo2.name = item2.name
- `when`(packageManager.getActivityInfo(item2.component, 0)).thenReturn(activityInfo2)
- `when`(
- carPackageManager.isActivityDistractionOptimized(
- item1.component.packageName,
- item1.component.className
- )
- )
- .thenReturn(item1.isDistractionOptimized)
- `when`(
- carPackageManager.isActivityDistractionOptimized(
- item2.component.packageName,
- item2.component.className
- )
- )
- .thenReturn(item2.isDistractionOptimized)
-
- val defaultApps = DockHelper(context, carPackageManager).defaultApps
-
- assertThat(defaultApps.size).isEqualTo(2)
- assertThat(defaultApps[0]).isEqualTo(item1)
- assertThat(defaultApps[1]).isEqualTo(item2)
- }
-
- @Test
- fun toDockAppItem_fetchCorrectAppName() {
- val item1 = TestUtils.createAppItem(app = "app", name = "name")
- `when`(context.packageManager).thenReturn(packageManager)
- `when`(packageManager.getApplicationIcon(any(String::class.java)))
- .thenReturn(mock(Drawable::class.java))
- val activityInfo1 = mock(ActivityInfo::class.java)
- activityInfo1.name = item1.name
- `when`(packageManager.getActivityInfo(item1.component, 0)).thenReturn(activityInfo1)
-
- val dockAppItem = DockHelper(context, carPackageManager).toDockAppItem(item1.component)
-
- assertThat(dockAppItem.name).isEqualTo(item1.name)
- }
-
- @Test
- fun toDockAppItem_fetchCorrectAppIcon() {
- val item1 = TestUtils.createAppItem(app = "app", icon = mock(Drawable::class.java))
- `when`(context.packageManager).thenReturn(packageManager)
- val activityInfo = mock(ActivityInfo::class.java)
- activityInfo.name = ""
- `when`(packageManager.getActivityInfo(any(), eq(0))).thenReturn(activityInfo)
- `when`(packageManager.getApplicationIcon(item1.component.packageName))
- .thenReturn(item1.icon)
-
- val dockAppItem = DockHelper(context, carPackageManager).toDockAppItem(item1.component)
-
- assertThat(dockAppItem.icon).isEqualTo(item1.icon)
- }
-
- @Test
- fun toDockAppItem_fetchCorrectAppDO() {
- val item1 = TestUtils.createAppItem(app = "app", isDrivingOptimized = true)
- `when`(context.packageManager).thenReturn(packageManager)
- val activityInfo = mock(ActivityInfo::class.java)
- activityInfo.name = ""
- `when`(packageManager.getActivityInfo(any(), eq(0))).thenReturn(activityInfo)
- `when`(packageManager.getApplicationIcon(any(String::class.java)))
- .thenReturn(mock(Drawable::class.java))
- `when`(
- carPackageManager.isActivityDistractionOptimized(
- item1.component.packageName,
- item1.component.className
- )
- )
- .thenReturn(item1.isDistractionOptimized)
-
- val dockAppItem = DockHelper(context, carPackageManager).toDockAppItem(item1.component)
-
- assertThat(dockAppItem.isDistractionOptimized).isEqualTo(item1.isDistractionOptimized)
- }
-
- @Test
- fun toDockAppItem_fetchCorrectAppInfo() {
- val item1 =
- TestUtils.createAppItem(
- app = "item1",
- name = "item1",
- icon = mock(Drawable::class.java),
- isDrivingOptimized = true
- )
- `when`(context.packageManager).thenReturn(packageManager)
- `when`(packageManager.getApplicationIcon(item1.component.packageName))
- .thenReturn(item1.icon)
- val activityInfo1 = mock(ActivityInfo::class.java)
- activityInfo1.name = item1.name
- `when`(packageManager.getActivityInfo(item1.component, 0)).thenReturn(activityInfo1)
- `when`(
- carPackageManager.isActivityDistractionOptimized(
- item1.component.packageName,
- item1.component.className
- )
- )
- .thenReturn(item1.isDistractionOptimized)
-
- val dockAppItem = DockHelper(context, carPackageManager).toDockAppItem(item1.component)
-
- assertThat(dockAppItem).isEqualTo(item1)
- }
-}
diff --git a/docklib/tests/src/com/android/car/docklib/DockViewModelTest.kt b/docklib/tests/src/com/android/car/docklib/DockViewModelTest.kt
index d7a53d1..464b918 100644
--- a/docklib/tests/src/com/android/car/docklib/DockViewModelTest.kt
+++ b/docklib/tests/src/com/android/car/docklib/DockViewModelTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -16,203 +16,668 @@
package com.android.car.docklib
+import android.app.ActivityManager
+import android.car.content.pm.CarPackageManager
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.content.pm.ServiceInfo
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.car.docklib.data.DockAppItem
+import com.android.car.docklib.data.DockProtoDataController
+import com.android.launcher3.icons.IconFactory
import com.google.common.truth.Truth.assertThat
+import java.util.UUID
+import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
class DockViewModelTest {
- @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
+ private companion object {
+ private const val CURRENT_USER_ID = 10
+ private const val MAX_ITEMS = 4
+ private const val TOAST_STR = "TOAST_STR"
+ }
- private lateinit var items: List<DockAppItem?>
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ private lateinit var items: List<DockAppItem>
private lateinit var model: DockViewModel
- private val apps =
- listOf(
- TestUtils.createAppItem(app = "a"),
- TestUtils.createAppItem(app = "b"),
- TestUtils.createAppItem(app = "c"),
- TestUtils.createAppItem(app = "d"),
- )
+ private val resourcesMock = mock<Resources> {}
+ private val contextMock = mock<Context> {
+ on { getString(eq(R.string.pin_failed_no_spots)) } doReturn TOAST_STR
+ on { resources } doReturn resourcesMock
+ }
+ private val packageManagerMock = mock<PackageManager> {
+ on {
+ queryIntentServices(any<Intent>(), any<PackageManager.ResolveInfoFlags>())
+ } doReturn listOf()
+ }
+ private var carPackageManagerMock = mock<CarPackageManager> {}
+ private var bitmapMock = mock<Bitmap> {}
+ private var iconFactoryMock = mock<IconFactory> {
+ on { createScaledBitmap(any<Drawable>(), anyInt()) } doReturn bitmapMock
+ }
+ private val dataController = mock<DockProtoDataController>()
@Before
fun setUp() {
- model = DockViewModel(4) { items = it }
+ // explicitly use default items unless saved items are set
+ whenever(dataController.loadFromFile()).thenReturn(null)
+ model = createSpyDockViewModel()
+ doNothing().whenever(model).showToast(any())
}
@Test
- fun setDefaultApps_listSetInOrder() {
- model.updateDefaultApps(apps)
+ fun init_defaultPinnedItems_addedToDock() {
+ val defaultPinnedItems =
+ createTestComponentList(pkgPrefix = "DEFAULT_PKG", classPrefix = "DEFAULT_CLASS")
- assertThat(items.size).isEqualTo(4)
- assertThat(items[0]).isEqualTo(apps[0])
- assertThat(items[1]).isEqualTo(apps[1])
- assertThat(items[2]).isEqualTo(apps[2])
- assertThat(items[3]).isEqualTo(apps[3])
+ model = createSpyDockViewModel(defaultPinnedItems = defaultPinnedItems)
+
+ assertThat(items.size).isEqualTo(MAX_ITEMS)
+ assertThat(items[0].component).isEqualTo(defaultPinnedItems[0])
+ assertThat(items[1].component).isEqualTo(defaultPinnedItems[1])
+ assertThat(items[2].component).isEqualTo(defaultPinnedItems[2])
+ assertThat(items[3].component).isEqualTo(defaultPinnedItems[3])
}
@Test
- fun addDynamicItem_beforeDefaultApps_index0Updated() {
- val dynamicItem = TestUtils.createAppItem(app = "da")
+ fun init_savedPinnedItems_noItemAddedToDock() {
+ val defaultPinnedItems =
+ createTestComponentList(pkgPrefix = "DEFAULT_PKG", classPrefix = "DEFAULT_CLASS")
+ val savedPinnedItems = mapOf<Int, ComponentName>()
+ whenever(dataController.loadFromFile()).thenReturn(savedPinnedItems)
- model.addDynamicItem(dynamicItem)
+ model = createSpyDockViewModel(defaultPinnedItems = defaultPinnedItems)
- assertThat(items[0]).isEqualTo(dynamicItem)
+ assertThat(items.size).isEqualTo(MAX_ITEMS)
+ assertThat(items[0].type).isEqualTo(DockAppItem.Type.DYNAMIC)
+ assertThat(items[1].type).isEqualTo(DockAppItem.Type.DYNAMIC)
+ assertThat(items[2].type).isEqualTo(DockAppItem.Type.DYNAMIC)
+ assertThat(items[3].type).isEqualTo(DockAppItem.Type.DYNAMIC)
}
@Test
- fun setDefaultApps_afterDynamicItem_fillRemainingPositions() {
- val dynamicItem = TestUtils.createAppItem(app = "da")
- model.addDynamicItem(dynamicItem)
+ fun init_savedPinnedItems_onlySavedItemsAddedToDock() {
+ val defaultPinnedItems =
+ createTestComponentList(pkgPrefix = "DEFAULT_PKG", classPrefix = "DEFAULT_CLASS")
+ val savedPinnedItems =
+ mapOf(
+ 0 to createNewComponent(pkg = "SAVED_PKG_0", clazz = "SAVED_CLASS_0", false),
+ 2 to createNewComponent(pkg = "SAVED_PKG_2", clazz = "SAVED_CLASS_2", false),
+ )
+ whenever(dataController.loadFromFile()).thenReturn(savedPinnedItems)
- model.updateDefaultApps(apps)
+ model = createSpyDockViewModel(defaultPinnedItems = defaultPinnedItems)
- assertThat(items.size).isEqualTo(4)
- assertThat(items[0]).isEqualTo(dynamicItem)
- assertThat(items[1]).isEqualTo(apps[1])
- assertThat(items[2]).isEqualTo(apps[2])
- assertThat(items[3]).isEqualTo(apps[3])
+ assertThat(items.size).isEqualTo(MAX_ITEMS)
+ assertThat(items[0].component).isEqualTo(savedPinnedItems[0])
+ assertThat(items[1].type).isEqualTo(DockAppItem.Type.DYNAMIC)
+ assertThat(items[2].component).isEqualTo(savedPinnedItems[2])
+ assertThat(items[3].type).isEqualTo(DockAppItem.Type.DYNAMIC)
}
@Test
- fun addDynamicItem_allItemsDefault_index0Updated() {
- model.updateDefaultApps(apps)
+ fun addDynamicItem_emptyDockList_index0Updated() {
+ val newComponent = createNewComponent(pkg = "newpkg", clazz = "newclass")
- val dynamicItem = TestUtils.createAppItem(app = "da")
- model.addDynamicItem(dynamicItem)
+ model.internalItems.clear()
+ model.addDynamicItem(newComponent)
- assertThat(items.size).isEqualTo(4)
- assertThat(items[0]).isEqualTo(dynamicItem)
- assertThat(items[1]).isEqualTo(apps[1])
- assertThat(items[2]).isEqualTo(apps[2])
- assertThat(items[3]).isEqualTo(apps[3])
- }
-
- @Test
- fun addDynamicItem_someItemsDefault_index1Updated() {
- model.updateDefaultApps(apps)
- val dynamicItem1 = TestUtils.createAppItem(app = "da1")
- model.addDynamicItem(dynamicItem1)
-
- val dynamicItem2 = TestUtils.createAppItem(app = "da2")
- model.addDynamicItem(dynamicItem2)
-
- assertThat(items.size).isEqualTo(4)
- assertThat(items[0]).isEqualTo(dynamicItem1)
- assertThat(items[1]).isEqualTo(dynamicItem2)
- assertThat(items[2]).isEqualTo(apps[2])
- assertThat(items[3]).isEqualTo(apps[3])
- }
-
- @Test
- fun addDynamicItem_someItemsDefault_index2Updated() {
- model.updateDefaultApps(apps)
- val dynamicItem1 = TestUtils.createAppItem(app = "da1")
- model.addDynamicItem(dynamicItem1)
- val dynamicItem2 = TestUtils.createAppItem(app = "da2")
- model.addDynamicItem(dynamicItem2)
-
- val dynamicItem3 = TestUtils.createAppItem(app = "da3")
- model.addDynamicItem(dynamicItem3)
-
- assertThat(items.size).isEqualTo(4)
- assertThat(items[0]).isEqualTo(dynamicItem1)
- assertThat(items[1]).isEqualTo(dynamicItem2)
- assertThat(items[2]).isEqualTo(dynamicItem3)
- assertThat(items[3]).isEqualTo(apps[3])
- }
-
- @Test
- fun addDynamicItem_oneItemDefault_index3Updated() {
- model.updateDefaultApps(apps)
- val dynamicItem1 = TestUtils.createAppItem(app = "da1")
- model.addDynamicItem(dynamicItem1)
- val dynamicItem2 = TestUtils.createAppItem(app = "da2")
- model.addDynamicItem(dynamicItem2)
- val dynamicItem3 = TestUtils.createAppItem(app = "da3")
- model.addDynamicItem(dynamicItem3)
-
- val dynamicItem4 = TestUtils.createAppItem(app = "da4")
- model.addDynamicItem(dynamicItem4)
-
- assertThat(items.size).isEqualTo(4)
- assertThat(items[0]).isEqualTo(dynamicItem1)
- assertThat(items[1]).isEqualTo(dynamicItem2)
- assertThat(items[2]).isEqualTo(dynamicItem3)
- assertThat(items[3]).isEqualTo(dynamicItem4)
+ assertThat(items[0].component).isEqualTo(newComponent)
}
@Test
fun addDynamicItem_allItemsDynamic_leastRecentItemUpdated() {
- model.updateDefaultApps(apps)
- val dynamicItem1 = TestUtils.createAppItem(app = "da1")
- model.addDynamicItem(dynamicItem1)
- val dynamicItem2 = TestUtils.createAppItem(app = "da2")
- model.addDynamicItem(dynamicItem2)
- val dynamicItem3 = TestUtils.createAppItem(app = "da3")
- model.addDynamicItem(dynamicItem3)
- val dynamicItem4 = TestUtils.createAppItem(app = "da4")
- model.addDynamicItem(dynamicItem4)
+ val newComponent = createNewComponent(pkg = "newpkg", clazz = "newclass")
+ model.internalItems.clear()
+ for (i in 0..<MAX_ITEMS) {
+ model.internalItems[i] = TestUtils.createAppItem(
+ app = "da$i",
+ type = DockAppItem.Type.DYNAMIC)
+ }
- val dynamicItem5 = TestUtils.createAppItem(app = "da5")
- model.addDynamicItem(dynamicItem5)
+ model.addDynamicItem(newComponent)
- assertThat(items.size).isEqualTo(4)
- assertThat(items[0]).isEqualTo(dynamicItem5)
- assertThat(items[1]).isEqualTo(dynamicItem2)
- assertThat(items[2]).isEqualTo(dynamicItem3)
- assertThat(items[3]).isEqualTo(dynamicItem4)
+ assertThat(items.size).isEqualTo(MAX_ITEMS)
+ assertThat(items[0].component).isEqualTo(newComponent)
}
@Test
fun addDynamicItem_appInDock_itemsNotChanged() {
- model.updateDefaultApps(apps)
- val dynamicItem1 = TestUtils.createAppItem(app = "da1")
- model.addDynamicItem(dynamicItem1)
- val dynamicItem2 = TestUtils.createAppItem(app = "da2")
- model.addDynamicItem(dynamicItem2)
- val dynamicItem3 = TestUtils.createAppItem(app = "da3")
- model.addDynamicItem(dynamicItem3)
- val dynamicItem4 = TestUtils.createAppItem(app = "da4")
- model.addDynamicItem(dynamicItem4)
+ val existingComponent = createNewComponent(pkg = "da0", clazz = "da0")
+ for (i in 0..<MAX_ITEMS) {
+ model.internalItems[i] = TestUtils.createAppItem(
+ app = "da$i",
+ type = DockAppItem.Type.DYNAMIC
+ )
+ }
- val dynamicItem1B = TestUtils.createAppItem(app = "da1")
- model.addDynamicItem(dynamicItem1B)
+ model.addDynamicItem(existingComponent)
- assertThat(items.size).isEqualTo(4)
- assertThat(items[0]).isEqualTo(dynamicItem1)
- assertThat(items[1]).isEqualTo(dynamicItem2)
- assertThat(items[2]).isEqualTo(dynamicItem3)
- assertThat(items[3]).isEqualTo(dynamicItem4)
+ assertThat(items.size).isEqualTo(MAX_ITEMS)
+ assertThat(items.filter { it.component == existingComponent }.size).isEqualTo(1)
}
@Test
fun addDynamicItem_appInDock_recencyRefreshed() {
- model.updateDefaultApps(apps)
- val dynamicItem1 = TestUtils.createAppItem(app = "da1")
- model.addDynamicItem(dynamicItem1)
- val dynamicItem2 = TestUtils.createAppItem(app = "da2")
- model.addDynamicItem(dynamicItem2)
- val dynamicItem3 = TestUtils.createAppItem(app = "da3")
- model.addDynamicItem(dynamicItem3)
- val dynamicItem4 = TestUtils.createAppItem(app = "da4")
- model.addDynamicItem(dynamicItem4)
+ val existingComponent = createNewComponent(pkg = "da0", clazz = "da0")
+ val newComponent = createNewComponent(pkg = "newpkg", clazz = "newclass")
+ for (i in 0..<MAX_ITEMS) {
+ model.internalItems[i] = TestUtils.createAppItem(
+ app = "da$i",
+ type = DockAppItem.Type.DYNAMIC
+ )
+ }
- val dynamicItem2B = TestUtils.createAppItem(app = "da2")
- model.addDynamicItem(dynamicItem2B)
- val dynamicItem5 = TestUtils.createAppItem(app = "da5")
- model.addDynamicItem(dynamicItem5)
- val dynamicItem6 = TestUtils.createAppItem(app = "da6")
- model.addDynamicItem(dynamicItem6)
+ model.addDynamicItem(existingComponent)
+ model.addDynamicItem(newComponent)
- assertThat(items.size).isEqualTo(4)
- assertThat(items[0]).isEqualTo(dynamicItem5)
- assertThat(items[1]).isEqualTo(dynamicItem2B)
- assertThat(items[2]).isEqualTo(dynamicItem6)
- assertThat(items[3]).isEqualTo(dynamicItem4)
+ assertThat(items.size).isEqualTo(MAX_ITEMS)
+ assertThat(items[1].component).isEqualTo(newComponent)
+ }
+
+ @Test
+ fun pinItem_itemWithIdNotInDock_itemNotPinned() {
+ val idNotInDock = UUID.nameUUIDFromBytes("idNotInDock".toByteArray())
+ for (i in 0..<MAX_ITEMS) {
+ model.internalItems[i] = TestUtils.createAppItem(
+ id = UUID.nameUUIDFromBytes("id$i".toByteArray()),
+ app = "da$i",
+ type = DockAppItem.Type.DYNAMIC
+ )
+ }
+
+ model.pinItem(idNotInDock)
+
+ items.forEach { assertThat(it.type).isEqualTo(DockAppItem.Type.DYNAMIC) }
+ }
+
+ @Test
+ fun pinItem_itemWithIdInDock_itemTypeStatic_itemIdUnchanged() {
+ val idInDock = UUID.nameUUIDFromBytes("id0".toByteArray())
+ for (i in 0..<MAX_ITEMS) {
+ model.internalItems[i] = TestUtils.createAppItem(
+ id = UUID.nameUUIDFromBytes("id$i".toByteArray()),
+ app = "da$i",
+ type = DockAppItem.Type.DYNAMIC
+ )
+ }
+
+ model.pinItem(idInDock)
+
+ val dockItem = items.firstOrNull { it.id == idInDock }
+ assertThat(dockItem).isNotNull()
+ assertThat(dockItem?.type).isEqualTo(DockAppItem.Type.STATIC)
+ }
+
+ @Test
+ fun pinItem_indexLessThanZero_itemNotPinned() {
+ val newComponent = createNewComponent(pkg = "newpkg", clazz = "newclass")
+ for (i in 0..<MAX_ITEMS) {
+ model.internalItems[i] = TestUtils.createAppItem(
+ app = "da$i",
+ type = DockAppItem.Type.DYNAMIC
+ )
+ }
+
+ model.pinItem(newComponent, indexToPin = -1)
+
+ items.forEach {
+ assertThat(it.component).isNotEqualTo(newComponent)
+ }
+ }
+
+ @Test
+ fun pinItem_indexGreaterThanMaxItems_itemNotPinned() {
+ val newComponent = createNewComponent(pkg = "newpkg", clazz = "newclass")
+ for (i in 0..<MAX_ITEMS) {
+ model.internalItems[i] = TestUtils.createAppItem(
+ app = "da$i",
+ type = DockAppItem.Type.DYNAMIC
+ )
+ }
+
+ model.pinItem(newComponent, indexToPin = MAX_ITEMS + 1)
+
+ items.forEach {
+ assertThat(it.component).isNotEqualTo(newComponent)
+ }
+ }
+
+ @Test
+ fun pinItem_indexProvided_itemPinnedToIndex() {
+ val indexToPin = 2
+ val newComponent = createNewComponent(pkg = "newpkg", clazz = "newclass")
+ for (i in 0..<MAX_ITEMS) {
+ model.internalItems[i] = TestUtils.createAppItem(
+ app = "da$i",
+ type = DockAppItem.Type.DYNAMIC
+ )
+ }
+
+ model.pinItem(newComponent, indexToPin)
+
+ assertThat(items[
+ indexToPin].component).isEqualTo(newComponent)
+ assertThat(items[indexToPin].type).isEqualTo(DockAppItem.Type.STATIC)
+ }
+
+ @Test
+ fun pinItem_indexNotProvided_noDynamicItemOrEmptyIndex_itemNotPinned() {
+ val newComponent = createNewComponent(pkg = "newpkg", clazz = "newclass")
+ for (i in 0..<MAX_ITEMS) {
+ model.internalItems[i] = TestUtils.createAppItem(
+ app = "da$i",
+ type = DockAppItem.Type.STATIC
+ )
+ }
+
+ model.pinItem(newComponent, indexToPin = null)
+
+ items.forEach { assertThat(it.component).isNotEqualTo(newComponent) }
+ }
+
+ @Test
+ fun pinItem_indexNotProvided_noDynamicItemOrEmptyIndex_toastShown() {
+ val newComponent = createNewComponent(pkg = "newpkg", clazz = "newclass")
+ for (i in 0..<MAX_ITEMS) {
+ model.internalItems[i] = TestUtils.createAppItem(
+ app = "da$i",
+ type = DockAppItem.Type.STATIC
+ )
+ }
+
+ model.pinItem(newComponent, indexToPin = null)
+
+ verify(model).showToast(eq(TOAST_STR))
+ }
+
+ @Test
+ fun pinItem_indexNotProvided_dynamicItemsPresent_itemPinnedToFirstDynamicItemIndex() {
+ val newComponent = createNewComponent(pkg = "newpkg", clazz = "newclass")
+ model.internalItems.compute(0) { _, item -> item?.copy(type = DockAppItem.Type.STATIC) }
+ model.internalItems.compute(1) { _, item -> item?.copy(type = DockAppItem.Type.DYNAMIC) }
+ model.internalItems.compute(2) { _, item -> item?.copy(type = DockAppItem.Type.DYNAMIC) }
+ model.internalItems.compute(3) { _, item -> item?.copy(type = DockAppItem.Type.STATIC) }
+
+ model.pinItem(newComponent, indexToPin = null)
+
+ val index = items.indexOfFirst { it.component == newComponent }
+ assertThat(index).isEqualTo(1)
+ assertThat(items[index].type).isEqualTo(DockAppItem.Type.STATIC)
+ }
+
+ @Test
+ fun pinItem_indexNotProvided_emptyIndexesPresent_itemPinnedToFirstEmptyIndex() {
+ val newComponent = createNewComponent(pkg = "newpkg", clazz = "newclass")
+ model.internalItems.replaceAll { _, item -> item.copy(type = DockAppItem.Type.STATIC) }
+ model.internalItems.remove(1)
+
+ model.pinItem(newComponent, indexToPin = null)
+
+ val index = items.indexOfFirst { it.component == newComponent }
+ assertThat(index).isEqualTo(1)
+ assertThat(items[index].type).isEqualTo(DockAppItem.Type.STATIC)
+ }
+
+ @Test
+ fun removeItem_itemWithIdNotInDock_itemNotRemoved() {
+ val idNotInDock = UUID.nameUUIDFromBytes("idNotInDock".toByteArray())
+ for (i in 0..<MAX_ITEMS) {
+ model.internalItems[i] = TestUtils.createAppItem(
+ id = UUID.nameUUIDFromBytes("id$i".toByteArray()),
+ app = "da$i",
+ type = DockAppItem.Type.STATIC
+ )
+ }
+ val listBeforeRemove = model.internalItems.values.toList()
+
+ model.removeItem(idNotInDock)
+
+ listBeforeRemove.forEachIndexed { key, item -> assertThat(items[key]).isEqualTo(item) }
+ }
+
+ @Test
+ fun removeItem_itemWithIdInDock_itemRemoved() {
+ val idInDock = UUID.nameUUIDFromBytes("id2".toByteArray())
+ for (i in 0..<MAX_ITEMS) {
+ model.internalItems[i] = TestUtils.createAppItem(
+ id = UUID.nameUUIDFromBytes("id$i".toByteArray()),
+ app = "da$i",
+ type = DockAppItem.Type.STATIC
+ )
+ }
+
+ model.removeItem(idInDock)
+
+ items.forEach { item -> assertThat(item.id).isNotEqualTo(idInDock) }
+ }
+
+ @Test
+ fun createDockList_indexNotFilled_noRecentTasks_noLauncherApp_errorThrown() {
+ model = createSpyDockViewModel(launcherActivities = mutableSetOf())
+ doReturn(List<ActivityManager.RunningTaskInfo>(0) { return })
+ .whenever(model).getRunningTasks()
+ model.internalItems.remove(2)
+
+ assertThrows(IllegalStateException::class.java) { model.createDockList() }
+ }
+
+ @Test
+ fun createDockList_indexNotFilled_noRecentTasks_launcherActivitiesExcluded_errorThrown() {
+ val cmpList = createTestComponentList(pkgPrefix = "testPkg", classPrefix = "testClass")
+ model = createSpyDockViewModel(
+ launcherActivities = cmpList.toMutableSet(),
+ excludedComponents = cmpList.toSet(),
+ )
+ doReturn(List<ActivityManager.RunningTaskInfo>(0) { return })
+ .whenever(model).getRunningTasks()
+ model.internalItems.remove(2)
+
+ assertThrows(IllegalStateException::class.java) { model.createDockList() }
+ }
+
+ @Test
+ fun createDockList_indexNotFilled_noRecentTasks_launcherActivitiesAlreadyInDock_errorThrown() {
+ val launcherAppComponent = createNewComponent(pkg = "testPkg", clazz = "testClass")
+ model = createSpyDockViewModel(launcherActivities = mutableSetOf(launcherAppComponent))
+ doReturn(List<ActivityManager.RunningTaskInfo>(0) { return })
+ .whenever(model).getRunningTasks()
+ model.internalItems[2] = TestUtils.createAppItem(component = launcherAppComponent)
+ model.internalItems.remove(3)
+
+ assertThrows(IllegalStateException::class.java) { model.createDockList() }
+ }
+
+ @Test
+ fun createDockList_indexNotFilled_noRecentTasks_randomLauncherAppAdded() {
+ val launcherActivities = createTestComponentList(
+ pkgPrefix = "testPkg",
+ classPrefix = "testClass"
+ )
+ model = createSpyDockViewModel(launcherActivities = launcherActivities.toMutableSet())
+ doReturn(List<ActivityManager.RunningTaskInfo>(0) { return })
+ .whenever(model).getRunningTasks()
+ model.internalItems.remove(2)
+
+ val dockList = model.createDockList()
+
+ assertThat(launcherActivities.contains(dockList[2].component)).isTrue()
+ }
+
+ @Test
+ fun createDockList_indexNotFilled_noRecentTasksForCurrentUser_randomLauncherAppAdded() {
+ val launcherActivities = createTestComponentList(
+ pkgPrefix = "testPkg",
+ classPrefix = "testClass"
+ )
+ model = createSpyDockViewModel(launcherActivities = launcherActivities.toMutableSet())
+ doReturn(createTestRunningTaskInfoList(userId = CURRENT_USER_ID + 1))
+ .whenever(model)
+ .getRunningTasks()
+ model.internalItems.remove(2)
+
+ val dockList = model.createDockList()
+
+ assertThat(launcherActivities.contains(dockList[2].component)).isTrue()
+ }
+
+ @Test
+ fun createDockList_indexNotFilled_recentTasksExcluded_randomLauncherAppAdded() {
+ val launcherActivities = createTestComponentList(
+ pkgPrefix = "testPkg",
+ classPrefix = "testClass"
+ )
+ val excludedComponent = createNewComponent(pkg = "excludedPkg", clazz = "excludedClass")
+ model = createSpyDockViewModel(
+ launcherActivities = launcherActivities.toMutableSet(),
+ excludedComponents = setOf(excludedComponent),
+ )
+ doReturn(createTestRunningTaskInfoList(component = excludedComponent))
+ .whenever(model)
+ .getRunningTasks()
+ model.internalItems.remove(2)
+
+ val dockList = model.createDockList()
+
+ assertThat(launcherActivities.contains(dockList[2].component)).isTrue()
+ }
+
+ @Test
+ fun createDockList_indexNotFilled_recentTasksAlreadyInDock_randomLauncherAppAdded() {
+ val launcherActivities = createTestComponentList(
+ pkgPrefix = "testPkg",
+ classPrefix = "testClass"
+ )
+ val recentTaskComponent =
+ createNewComponent(pkg = "recentTaskPkg", clazz = "recentTaskClass")
+ model = createSpyDockViewModel(launcherActivities = launcherActivities.toMutableSet())
+ doReturn(createTestRunningTaskInfoList(component = recentTaskComponent))
+ .whenever(model)
+ .getRunningTasks()
+ model.internalItems[2] = TestUtils.createAppItem(component = recentTaskComponent)
+ model.internalItems.remove(3)
+
+ val dockList = model.createDockList()
+
+ assertThat(launcherActivities.contains(dockList[3].component)).isTrue()
+ }
+
+ @Test
+ fun createDockList_indexNotFilled_recentTasksAdded() {
+ val recentTaskComponent =
+ createNewComponent(pkg = "recentTaskPkg", clazz = "recentTaskClass")
+ val recentTaskList = createTestRunningTaskInfoList(component = recentTaskComponent)
+ doReturn(recentTaskList).whenever(model).getRunningTasks()
+ model.internalItems.remove(2)
+
+ val dockList = model.createDockList()
+
+ assertThat(dockList[2].component).isEqualTo(recentTaskComponent)
+ }
+
+ @Test
+ fun removeItems_itemsWithPackageNameInDock_itemsRemoved() {
+ val pkgToBeRemoved = "pkgToBeRemoved"
+ val pkgOther = "pkgOther"
+ model.internalItems[0] =
+ TestUtils.createAppItem(type = DockAppItem.Type.DYNAMIC, pkg = pkgToBeRemoved)
+ model.internalItems[1] =
+ TestUtils.createAppItem(type = DockAppItem.Type.STATIC, pkg = pkgOther)
+ model.internalItems[2] =
+ TestUtils.createAppItem(type = DockAppItem.Type.STATIC, pkg = pkgToBeRemoved)
+ model.internalItems[3] =
+ TestUtils.createAppItem(type = DockAppItem.Type.DYNAMIC, pkg = pkgOther)
+
+ model.removeItems(pkgToBeRemoved)
+
+ items.forEach { assertThat(it.component.packageName).isNotEqualTo(pkgToBeRemoved) }
+ }
+
+ @Test
+ fun removeItems_itemsWithPackageNameNotInDock_itemsNotRemoved() {
+ val pkgToBeRemoved = "pkgToBeRemoved"
+ val pkgOther = "pkgOther"
+ model.internalItems[0] =
+ TestUtils.createAppItem(type = DockAppItem.Type.DYNAMIC, pkg = pkgOther)
+ model.internalItems[1] =
+ TestUtils.createAppItem(type = DockAppItem.Type.STATIC, pkg = pkgOther)
+ model.internalItems[2] =
+ TestUtils.createAppItem(type = DockAppItem.Type.STATIC, pkg = pkgOther)
+ model.internalItems[3] =
+ TestUtils.createAppItem(type = DockAppItem.Type.DYNAMIC, pkg = pkgOther)
+
+ model.removeItems(pkgToBeRemoved)
+
+ items.forEach { assertThat(it.component.packageName).isNotEqualTo(pkgToBeRemoved) }
+ }
+
+ @Test
+ fun setCarPackageManager_distractionValuesUpdated() {
+ model = createSpyDockViewModel(shouldSetCarPackageManager = false)
+ for (i in 0..<4) {
+ model.internalItems[i] = TestUtils.createAppItem(
+ pkg = "pkg$i",
+ clazz = "class$i",
+ isDrivingOptimized = false
+ )
+ }
+ whenever(carPackageManagerMock.isActivityDistractionOptimized(
+ eq("pkg0"),
+ eq("class0")
+ )).thenReturn(true)
+ whenever(carPackageManagerMock.isActivityDistractionOptimized(
+ eq("pkg1"),
+ eq("class1")
+ )).thenReturn(false)
+ whenever(carPackageManagerMock.isActivityDistractionOptimized(
+ eq("pkg2"),
+ eq("class2")
+ )).thenReturn(true)
+ whenever(carPackageManagerMock.isActivityDistractionOptimized(
+ eq("pkg3"),
+ eq("class3")
+ )).thenReturn(false)
+
+ model.setCarPackageManager(carPackageManagerMock)
+
+ items.forEach {
+ when (it.component.packageName) {
+ "pkg0" -> assertThat(it.isDistractionOptimized).isEqualTo(true)
+ "pkg1" -> assertThat(it.isDistractionOptimized).isEqualTo(false)
+ "pkg2" -> assertThat(it.isDistractionOptimized).isEqualTo(true)
+ "pkg3" -> assertThat(it.isDistractionOptimized).isEqualTo(false)
+ }
+ }
+ }
+
+ private fun createSpyDockViewModel(
+ shouldSetCarPackageManager: Boolean = true,
+ launcherActivities: MutableSet<ComponentName> = createTestComponentList(
+ pkgPrefix = "LAUNCHER_PKG",
+ classPrefix = "LAUNCHER_CLASS"
+ ).toMutableSet(),
+ defaultPinnedItems: List<ComponentName> = createTestComponentList(
+ pkgPrefix = "DEFAULT_PKG",
+ classPrefix = "DEFAULT_CLASS"
+ ),
+ excludedComponents: Set<ComponentName> = setOf(),
+ ): DockViewModel {
+ return spy(DockViewModel(
+ maxItemsInDock = MAX_ITEMS,
+ context = contextMock,
+ packageManager = packageManagerMock,
+ if (shouldSetCarPackageManager) carPackageManagerMock else null,
+ userId = CURRENT_USER_ID,
+ launcherActivities,
+ defaultPinnedItems,
+ isPackageExcluded = { false },
+ isComponentExcluded = { excludedComponents.contains(it) },
+ iconFactory = iconFactoryMock,
+ dockProtoDataController = dataController,
+ observer = { items = it },
+ ))
+ }
+
+ private fun createTestRunningTaskInfoList(
+ pkgPrefix: String = "testRunningTaskInfo_PKG",
+ classPrefix: String = "testRunningTaskInfo_CLASS",
+ component: ComponentName? = null,
+ userId: Int = CURRENT_USER_ID
+ ): List<ActivityManager.RunningTaskInfo> {
+ val recentTasks = mutableListOf<ActivityManager.RunningTaskInfo>()
+ for (i in 1..10) {
+ val t = mock<ActivityManager.RunningTaskInfo> {}
+ t.userId = userId
+ val cmp = component ?: createNewComponent(pkg = "$pkgPrefix$i", "$classPrefix$i")
+ t.baseActivity = cmp
+ val intent = mock<Intent> {}
+ intent.component = cmp
+ t.baseIntent = intent
+ recentTasks.add(t)
+ }
+ return recentTasks
+ }
+
+ private fun createTestComponentList(
+ pkgPrefix: String,
+ classPrefix: String,
+ isService: Boolean = false,
+ ): List<ComponentName> {
+ val launcherActivities = mutableListOf<ComponentName>()
+ for (i in 1..10) {
+ launcherActivities.add(
+ createNewComponent(pkg = "$pkgPrefix$i", "$classPrefix$i", isService)
+ )
+ }
+ return launcherActivities
+ }
+
+ private fun createNewComponent(
+ pkg: String,
+ clazz: String,
+ isService: Boolean = false
+ ): ComponentName {
+ val component = ComponentName(pkg, clazz)
+ val pi = createMockPackageInfo(component, isService)
+ whenever(packageManagerMock.getPackageInfo(eq(pkg), any<PackageManager.PackageInfoFlags>()))
+ .thenReturn(pi)
+ return component
+ }
+
+ private fun createMockPackageInfo(
+ component: ComponentName,
+ isService: Boolean = false
+ ): PackageInfo {
+ val pi = mock<PackageInfo> {}
+ if (isService) {
+ pi.services = arrayOf(createMockServiceInfo(component))
+ } else {
+ pi.activities = arrayOf(createMockActivityInfo(component))
+ }
+ return pi
+ }
+
+ private fun createMockActivityInfo(component: ComponentName): ActivityInfo {
+ val icon = mock<Drawable> {}
+ val ai = mock<ActivityInfo> {
+ on { loadIcon(any()) } doReturn icon
+ on { loadLabel(any()) } doReturn "${component.packageName}-${component.className}"
+ on { componentName } doReturn component
+ }
+ ai.name = "${component.packageName}-${component.className}"
+ return ai
+ }
+
+ private fun createMockServiceInfo(component: ComponentName): ServiceInfo {
+ val icon = mock<Drawable> {}
+ val si = mock<ServiceInfo> {
+ on { loadIcon(any()) } doReturn icon
+ on { componentName } doReturn component
+ }
+ si.name = "${component.packageName}-${component.className}"
+ return si
}
}
diff --git a/docklib/tests/src/com/android/car/docklib/TestUtils.kt b/docklib/tests/src/com/android/car/docklib/TestUtils.kt
index c362590..0e940d0 100644
--- a/docklib/tests/src/com/android/car/docklib/TestUtils.kt
+++ b/docklib/tests/src/com/android/car/docklib/TestUtils.kt
@@ -1,22 +1,52 @@
package com.android.car.docklib
import android.content.ComponentName
+import android.graphics.Color
import android.graphics.drawable.Drawable
+import androidx.annotation.ColorInt
import com.android.car.docklib.data.DockAppItem
-import org.mockito.Mockito.mock
+import com.android.car.docklib.data.DockItemId
+import java.util.UUID
+import org.mockito.kotlin.mock
object TestUtils {
-
- private val icon = mock(Drawable::class.java)
-
/** Create a hardcoded dock item with optional fields */
fun createAppItem(
- type: DockAppItem.Type = DockAppItem.Type.DYNAMIC,
- app: String = "app",
- name: String = "app",
- icon: Drawable = this.icon,
- isDrivingOptimized: Boolean = true
+ id: @DockItemId UUID = UUID.randomUUID(),
+ type: DockAppItem.Type = DockAppItem.Type.DYNAMIC,
+ app: String = "app",
+ pkg: String = app,
+ clazz: String = app,
+ name: String = "app",
+ component: ComponentName = ComponentName(pkg, clazz),
+ icon: Drawable = mock<Drawable>(),
+ @ColorInt iconColor: Int = Color.WHITE,
+ @ColorInt iconColorScrim: Int? = null,
+ isDrivingOptimized: Boolean = true,
+ isMediaApp: Boolean = false,
): DockAppItem {
- return DockAppItem(type, ComponentName(app, app), name, icon, isDrivingOptimized)
+ if (iconColorScrim != null) {
+ return DockAppItem(
+ id = id,
+ type = type,
+ component = component,
+ name = name,
+ icon = icon,
+ iconColor = iconColor,
+ iconColorScrim = iconColorScrim,
+ isDistractionOptimized = isDrivingOptimized,
+ isMediaApp = isMediaApp,
+ )
+ }
+ return DockAppItem(
+ id = id,
+ type = type,
+ component = component,
+ name = name,
+ icon = icon,
+ iconColor = iconColor,
+ isDistractionOptimized = isDrivingOptimized,
+ isMediaApp = isMediaApp,
+ )
}
}
diff --git a/docklib/tests/src/com/android/car/docklib/data/DockAppItemTest.kt b/docklib/tests/src/com/android/car/docklib/data/DockAppItemTest.kt
index 2da0f3e..56aa35f 100644
--- a/docklib/tests/src/com/android/car/docklib/data/DockAppItemTest.kt
+++ b/docklib/tests/src/com/android/car/docklib/data/DockAppItemTest.kt
@@ -16,10 +16,12 @@
package com.android.car.docklib.data
+import android.graphics.Color
import android.graphics.drawable.Drawable
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.car.docklib.TestUtils
import com.google.common.truth.Truth.assertThat
+import java.util.UUID
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
@@ -29,32 +31,36 @@
class DockAppItemTest {
@Test
fun compareAppItems_equal() {
- val item1: DockAppItem = TestUtils.createAppItem()
- val item2: DockAppItem = TestUtils.createAppItem()
+ val id = UUID.randomUUID()
+ val item1: DockAppItem = TestUtils.createAppItem(id = id)
+ val item2: DockAppItem = TestUtils.createAppItem(id = id)
assertThat(item1).isEqualTo(item2)
}
@Test
fun compareAppItems_notEqual_differentApps() {
- val item1: DockAppItem = TestUtils.createAppItem(app = "1")
- val item2: DockAppItem = TestUtils.createAppItem(app = "2")
+ val id = UUID.randomUUID()
+ val item1: DockAppItem = TestUtils.createAppItem(id = id, app = "1")
+ val item2: DockAppItem = TestUtils.createAppItem(id = id, app = "2")
assertThat(item1).isNotEqualTo(item2)
}
@Test
fun compareAppItems_notEqual_differentNames() {
- val item1: DockAppItem = TestUtils.createAppItem(name = "1")
- val item2: DockAppItem = TestUtils.createAppItem(name = "2")
+ val id = UUID.randomUUID()
+ val item1: DockAppItem = TestUtils.createAppItem(id = id, name = "1")
+ val item2: DockAppItem = TestUtils.createAppItem(id = id, name = "2")
assertThat(item1).isNotEqualTo(item2)
}
@Test
fun compareAppItems_notEqual_differentStates() {
- val item1: DockAppItem = TestUtils.createAppItem(type = DockAppItem.Type.DYNAMIC)
- val item2: DockAppItem = TestUtils.createAppItem(type = DockAppItem.Type.STATIC)
+ val id = UUID.randomUUID()
+ val item1: DockAppItem = TestUtils.createAppItem(id = id, type = DockAppItem.Type.DYNAMIC)
+ val item2: DockAppItem = TestUtils.createAppItem(id = id, type = DockAppItem.Type.STATIC)
assertThat(item1).isNotEqualTo(item2)
}
@@ -65,17 +71,63 @@
whenever(icon1.constantState).thenReturn(null)
val icon2 = mock<Drawable>()
whenever(icon2.constantState).thenReturn(mock<Drawable.ConstantState>())
-
- val item1: DockAppItem = TestUtils.createAppItem(icon = icon1)
- val item2: DockAppItem = TestUtils.createAppItem(icon = icon2)
+ val id = UUID.randomUUID()
+ val item1: DockAppItem = TestUtils.createAppItem(id = id, icon = icon1)
+ val item2: DockAppItem = TestUtils.createAppItem(id = id, icon = icon2)
assertThat(item1).isNotEqualTo(item2)
}
@Test
fun compareAppItems_notEqual_differentDrivingOptimized() {
- val item1: DockAppItem = TestUtils.createAppItem(isDrivingOptimized = true)
- val item2: DockAppItem = TestUtils.createAppItem(isDrivingOptimized = false)
+ val id = UUID.randomUUID()
+ val item1: DockAppItem = TestUtils.createAppItem(id = id, isDrivingOptimized = true)
+ val item2: DockAppItem = TestUtils.createAppItem(id = id, isDrivingOptimized = false)
+
+ assertThat(item1).isNotEqualTo(item2)
+ }
+
+ @Test
+ fun compareAppItems_notEqual_differentMediaType() {
+ val id = UUID.randomUUID()
+ val item1: DockAppItem = TestUtils.createAppItem(id = id, isMediaApp = true)
+ val item2: DockAppItem = TestUtils.createAppItem(id = id, isMediaApp = false)
+
+ assertThat(item1).isNotEqualTo(item2)
+ }
+
+ @Test
+ fun compareAppItems_notEqual_differentIconColor() {
+ val id = UUID.randomUUID()
+ val item1: DockAppItem = TestUtils.createAppItem(id = id, iconColor = Color.WHITE)
+ val item2: DockAppItem = TestUtils.createAppItem(id = id, iconColor = Color.BLACK)
+
+ assertThat(item1).isNotEqualTo(item2)
+ }
+
+ @Test
+ fun compareAppItems_notEqual_differentIconColorScrim() {
+ val id = UUID.randomUUID()
+ val item1: DockAppItem = TestUtils.createAppItem(
+ id = id,
+ iconColor = Color.WHITE,
+ iconColorScrim = Color.argb(
+ 100, // alpha
+ 255, // red
+ 0, // green
+ 0 // blue
+ )
+ )
+ val item2: DockAppItem = TestUtils.createAppItem(
+ id = id,
+ iconColor = Color.WHITE,
+ iconColorScrim = Color.argb(
+ 150, // alpha
+ 0, // red
+ 255, // green
+ 0 // blue
+ )
+ )
assertThat(item1).isNotEqualTo(item2)
}
diff --git a/docklib/tests/src/com/android/car/docklib/data/DockProtoDataControllerTest.kt b/docklib/tests/src/com/android/car/docklib/data/DockProtoDataControllerTest.kt
new file mode 100644
index 0000000..1f1e756
--- /dev/null
+++ b/docklib/tests/src/com/android/car/docklib/data/DockProtoDataControllerTest.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2024 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.docklib.data
+
+import android.content.ComponentName
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.car.docklib.DockItemProto.DockAppItemListMessage
+import com.android.car.docklib.DockItemProto.DockAppItemMessage
+import com.google.common.truth.Truth.assertThat
+import java.io.File
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class DockProtoDataControllerTest {
+
+ private lateinit var dataController: DockProtoDataController
+ private val dataSource = mock<DockProtoDataSource>()
+
+ private val pinnedItemA = ComponentName("packageA", "classA")
+ private val pinnedItemC = ComponentName("packageC", "classC")
+
+ @Before
+ fun setup() {
+ dataController = DockProtoDataController(mock<File>())
+ val field = DockProtoDataController::class.java.getDeclaredField("dataSource")
+ field.isAccessible = true
+ field.set(dataController, dataSource)
+ }
+
+ @Test
+ fun testSaveToFile_OneItem_DataCorrect() {
+ val dockData = mapOf(1 to pinnedItemA)
+ val captor = argumentCaptor<DockAppItemListMessage>()
+
+ dataController.savePinnedItemsToFile(dockData)
+
+ verify(dataSource).writeToFile(captor.capture())
+ assertThat(captor.allValues.size).isEqualTo(1)
+ val dockProtoData = captor.allValues[0].dockAppItemMessageList
+ assertThat(dockProtoData.size).isEqualTo(dockData.size)
+ assertThat(dockProtoData[0].packageName).isEqualTo(pinnedItemA.packageName)
+ assertThat(dockProtoData[0].className).isEqualTo(pinnedItemA.className)
+ assertThat(dockProtoData[0].relativePosition).isEqualTo(1)
+ }
+
+ @Test
+ fun testSaveToFile_MultipleItems_DataCorrect() {
+ val dockData = mapOf(
+ 0 to pinnedItemA,
+ // Intentionally skip index 1
+ 2 to pinnedItemC,
+ )
+ val captor = argumentCaptor<DockAppItemListMessage>()
+
+ dataController.savePinnedItemsToFile(dockData)
+
+ verify(dataSource).writeToFile(captor.capture())
+ assertThat(captor.allValues.size).isEqualTo(1)
+ val dockProtoData = captor.allValues[0].dockAppItemMessageList
+ assertThat(dockProtoData.size).isEqualTo(dockData.size)
+ assertThat(dockProtoData[0].packageName).isEqualTo(pinnedItemA.packageName)
+ assertThat(dockProtoData[0].className).isEqualTo(pinnedItemA.className)
+ assertThat(dockProtoData[0].relativePosition).isEqualTo(0)
+ assertThat(dockProtoData[1].packageName).isEqualTo(pinnedItemC.packageName)
+ assertThat(dockProtoData[1].className).isEqualTo(pinnedItemC.className)
+ assertThat(dockProtoData[1].relativePosition).isEqualTo(2)
+ }
+
+ @Test
+ fun testLoadFromFile_OneItem_DataCorrect() {
+ val dockProtoData = DockAppItemListMessage.newBuilder()
+ .addDockAppItemMessage(convertDockItemToProto(pinnedItemA, 1))
+ .build()
+ whenever(dataSource.readFromFile()).thenReturn(dockProtoData)
+
+ val loadedDockData = dataController.loadFromFile()
+
+ assertThat(loadedDockData?.size).isEqualTo(1)
+ assertThat(loadedDockData?.get(1)).isEqualTo(pinnedItemA)
+ }
+
+ @Test
+ fun testLoadFromFile_MultipleItems_DataCorrect() {
+ val dockProtoData = DockAppItemListMessage.newBuilder()
+ .addDockAppItemMessage(convertDockItemToProto(pinnedItemA, 0))
+ .addDockAppItemMessage(convertDockItemToProto(pinnedItemC, 2))
+ .build()
+ whenever(dataSource.readFromFile()).thenReturn(dockProtoData)
+
+ val loadedDockData = dataController.loadFromFile()
+
+ assertThat(loadedDockData?.size).isEqualTo(2)
+ assertThat(loadedDockData?.get(0)).isEqualTo(pinnedItemA)
+ assertThat(loadedDockData?.get(2)).isEqualTo(pinnedItemC)
+ }
+
+ private fun convertDockItemToProto(componentName: ComponentName, position: Int) =
+ DockAppItemMessage.newBuilder()
+ .setPackageName(componentName.packageName)
+ .setClassName(componentName.className)
+ .setRelativePosition(position)
+ .build()
+}
diff --git a/docklib/tests/src/com/android/car/docklib/events/DockPackageChangeReceiverTest.kt b/docklib/tests/src/com/android/car/docklib/events/DockPackageChangeReceiverTest.kt
new file mode 100644
index 0000000..3c20c56
--- /dev/null
+++ b/docklib/tests/src/com/android/car/docklib/events/DockPackageChangeReceiverTest.kt
@@ -0,0 +1,152 @@
+package com.android.car.docklib.events
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
+import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.car.docklib.DockInterface
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoMoreInteractions
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class DockPackageChangeReceiverTest {
+ private companion object {
+ private const val TEST_UID = 10
+ private const val TEST_PKG_NAME = "TEST_PKG_NAME"
+ }
+
+ private val dockInterfaceMock = mock<DockInterface> {}
+ private val packageManagerMock = mock<PackageManager> {}
+ private val contextMock = mock<Context> {
+ on { packageManager } doReturn packageManagerMock
+ }
+ private val uriMock = mock<Uri> {}
+ private val intentMock = mock<Intent> {
+ on { data } doReturn uriMock
+ }
+ private val dockPackageChangeReceiver = DockPackageChangeReceiver(dockInterfaceMock)
+
+ @Test
+ fun onReceive_uidNotSent_noop() {
+ // since getIntExtra cannot return null value, return the default value passed in as arg
+ whenever(intentMock.getIntExtra(eq(Intent.EXTRA_UID), anyInt()))
+ .then { it.getArgument<Int>(1) }
+
+ dockPackageChangeReceiver.onReceive(contextMock, intentMock)
+
+ verifyNoMoreInteractions(dockInterfaceMock)
+ }
+
+ @Test
+ fun onReceive_pkgNotSend_noop() {
+ whenever(intentMock.getIntExtra(eq(Intent.EXTRA_UID), anyInt())).thenReturn(TEST_UID)
+ whenever(uriMock.schemeSpecificPart).thenReturn(null)
+
+ dockPackageChangeReceiver.onReceive(contextMock, intentMock)
+
+ verifyNoMoreInteractions(dockInterfaceMock)
+ }
+
+ @Test
+ fun onReceive_actionPackageRemoved_replacing_noop() {
+ whenever(intentMock.action).thenReturn(Intent.ACTION_PACKAGE_REMOVED)
+ whenever(intentMock.getBooleanExtra(eq(Intent.EXTRA_REPLACING), anyBoolean()))
+ .thenReturn(true)
+ whenever(intentMock.getIntExtra(eq(Intent.EXTRA_UID), anyInt())).thenReturn(TEST_UID)
+ whenever(uriMock.schemeSpecificPart).thenReturn(TEST_PKG_NAME)
+
+ dockPackageChangeReceiver.onReceive(contextMock, intentMock)
+
+ verifyNoMoreInteractions(dockInterfaceMock)
+ }
+
+ @Test
+ fun onReceive_actionPackageRemoved_notReplacing_packageRemovedCalled() {
+ whenever(intentMock.action).thenReturn(Intent.ACTION_PACKAGE_REMOVED)
+ whenever(intentMock.getBooleanExtra(eq(Intent.EXTRA_REPLACING), anyBoolean()))
+ .thenReturn(false)
+ whenever(intentMock.getIntExtra(eq(Intent.EXTRA_UID), anyInt())).thenReturn(TEST_UID)
+ whenever(uriMock.schemeSpecificPart).thenReturn(TEST_PKG_NAME)
+
+ dockPackageChangeReceiver.onReceive(contextMock, intentMock)
+
+ verify(dockInterfaceMock).packageRemoved(eq(TEST_PKG_NAME))
+ }
+
+ @Test
+ fun onReceive_actionPackageAdded_replacing_noop() {
+ whenever(intentMock.action).thenReturn(Intent.ACTION_PACKAGE_ADDED)
+ whenever(intentMock.getBooleanExtra(eq(Intent.EXTRA_REPLACING), anyBoolean()))
+ .thenReturn(true)
+ whenever(intentMock.getIntExtra(eq(Intent.EXTRA_UID), anyInt())).thenReturn(TEST_UID)
+ whenever(uriMock.schemeSpecificPart).thenReturn(TEST_PKG_NAME)
+
+ dockPackageChangeReceiver.onReceive(contextMock, intentMock)
+
+ verifyNoMoreInteractions(dockInterfaceMock)
+ }
+
+ @Test
+ fun onReceive_actionPackageAdded_notReplacing_packageAddedCalled() {
+ whenever(intentMock.action).thenReturn(Intent.ACTION_PACKAGE_ADDED)
+ whenever(intentMock.getBooleanExtra(eq(Intent.EXTRA_REPLACING), anyBoolean()))
+ .thenReturn(false)
+ whenever(intentMock.getIntExtra(eq(Intent.EXTRA_UID), anyInt())).thenReturn(TEST_UID)
+ whenever(uriMock.schemeSpecificPart).thenReturn(TEST_PKG_NAME)
+
+ dockPackageChangeReceiver.onReceive(contextMock, intentMock)
+
+ verify(dockInterfaceMock).packageAdded(eq(TEST_PKG_NAME))
+ }
+
+ @Test
+ fun onReceive_actionPackageChanged_isEnabled_noop() {
+ whenever(intentMock.action).thenReturn(Intent.ACTION_PACKAGE_CHANGED)
+ whenever(intentMock.getIntExtra(eq(Intent.EXTRA_UID), anyInt())).thenReturn(TEST_UID)
+ whenever(uriMock.schemeSpecificPart).thenReturn(TEST_PKG_NAME)
+ whenever(packageManagerMock.getApplicationEnabledSetting(eq(TEST_PKG_NAME)))
+ .thenReturn(COMPONENT_ENABLED_STATE_ENABLED)
+
+ dockPackageChangeReceiver.onReceive(contextMock, intentMock)
+
+ verifyNoMoreInteractions(dockInterfaceMock)
+ }
+
+ @Test
+ fun onReceive_actionPackageChanged_isDisabled_packageRemovedCalled() {
+ whenever(intentMock.action).thenReturn(Intent.ACTION_PACKAGE_CHANGED)
+ whenever(intentMock.getIntExtra(eq(Intent.EXTRA_UID), anyInt())).thenReturn(TEST_UID)
+ whenever(uriMock.schemeSpecificPart).thenReturn(TEST_PKG_NAME)
+ whenever(packageManagerMock.getApplicationEnabledSetting(eq(TEST_PKG_NAME)))
+ .thenReturn(COMPONENT_ENABLED_STATE_DISABLED)
+
+ dockPackageChangeReceiver.onReceive(contextMock, intentMock)
+
+ verify(dockInterfaceMock).packageRemoved(eq(TEST_PKG_NAME))
+ }
+
+ @Test
+ fun onReceive_actionPackageChanged_isDisabledForUser_packageRemovedCalled() {
+ whenever(intentMock.action).thenReturn(Intent.ACTION_PACKAGE_CHANGED)
+ whenever(intentMock.getIntExtra(eq(Intent.EXTRA_UID), anyInt())).thenReturn(TEST_UID)
+ whenever(uriMock.schemeSpecificPart).thenReturn(TEST_PKG_NAME)
+ whenever(packageManagerMock.getApplicationEnabledSetting(eq(TEST_PKG_NAME)))
+ .thenReturn(COMPONENT_ENABLED_STATE_DISABLED_USER)
+
+ dockPackageChangeReceiver.onReceive(contextMock, intentMock)
+
+ verify(dockInterfaceMock).packageRemoved(eq(TEST_PKG_NAME))
+ }
+}
diff --git a/docklib/tests/src/com/android/car/docklib/media/MediaUtilsTest.kt b/docklib/tests/src/com/android/car/docklib/media/MediaUtilsTest.kt
new file mode 100644
index 0000000..07cb25b
--- /dev/null
+++ b/docklib/tests/src/com/android/car/docklib/media/MediaUtilsTest.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2024 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.docklib.media
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.car.docklib.media.MediaUtils.Companion.CAR_MEDIA_DATA_SCHEME
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class MediaUtilsTest {
+ private val uriMock = mock<Uri> {}
+ private val intentMock = mock<Intent> {}
+ private val runningTaskInfoMock = mock<RunningTaskInfo> {}
+ private val packageManagerMock = mock<PackageManager> {}
+ private val intentCaptor = argumentCaptor<Intent>()
+
+ @Before
+ fun setup() {
+ runningTaskInfoMock.baseIntent = intentMock
+ }
+
+ @Test
+ fun getMediaComponentName_noData_returnsNull() {
+ whenever(intentMock.data).doReturn(null)
+
+ val ret = MediaUtils.getMediaComponentName(runningTaskInfoMock)
+
+ assertThat(ret).isNull()
+ }
+
+ @Test
+ fun getMediaComponentName_incorrectDataScheme_returnsNull() {
+ whenever(intentMock.data).doReturn(uriMock)
+ whenever(uriMock.scheme).doReturn("MediaUtilsTestScheme")
+
+ val ret = MediaUtils.getMediaComponentName(runningTaskInfoMock)
+
+ assertThat(ret).isNull()
+ }
+
+ @Test
+ fun getMediaComponentName_returnsCorrectComponent() {
+ val componentName = ComponentName("testPkg", "testClass")
+ whenever(intentMock.data).doReturn(uriMock)
+ whenever(uriMock.scheme).doReturn(CAR_MEDIA_DATA_SCHEME)
+ whenever(uriMock.schemeSpecificPart).doReturn("/" + componentName.flattenToString())
+
+ val ret = MediaUtils.getMediaComponentName(runningTaskInfoMock)
+
+ assertThat(ret).isEqualTo(componentName)
+ }
+
+ @Test
+ fun fetchMediaServiceComponents_packageNotNull_queriesOnlyForThatPackage() {
+ val pkg = "testPkg"
+
+ MediaUtils.fetchMediaServiceComponents(packageManagerMock, pkg)
+
+ verify(packageManagerMock).queryIntentServices(intentCaptor.capture(), anyInt())
+ assertThat(intentCaptor.firstValue.getPackage()).isEqualTo(pkg)
+ }
+}
diff --git a/docklib/tests/src/com/android/car/docklib/task/DockTaskStackChangeListenerTest.kt b/docklib/tests/src/com/android/car/docklib/task/DockTaskStackChangeListenerTest.kt
new file mode 100644
index 0000000..0a480ad
--- /dev/null
+++ b/docklib/tests/src/com/android/car/docklib/task/DockTaskStackChangeListenerTest.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2023 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.docklib.task
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.ComponentName
+import android.content.Intent
+import android.view.Display
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.car.docklib.DockInterface
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoMoreInteractions
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class DockTaskStackChangeListenerTest {
+ private val dockInterfaceMock = mock<DockInterface> {}
+ private val runningTaskInfoMock = mock<RunningTaskInfo> {}
+ private val intentMock = mock<Intent> {}
+
+ @Test
+ fun onTaskMovedToFront_taskNotForCurrentUser_appLaunchedNotCalled() {
+ val currentUserId = 10
+ val taskUserId = 11
+ val taskComponentName = ComponentName("testPkgName", "testClassName")
+ runningTaskInfoMock.userId = taskUserId
+ runningTaskInfoMock.displayId = Display.DEFAULT_DISPLAY
+ runningTaskInfoMock.baseActivity = taskComponentName
+
+ val dockTaskStackChangeListener =
+ DockTaskStackChangeListener(currentUserId, dockInterfaceMock)
+ dockTaskStackChangeListener.onTaskMovedToFront(runningTaskInfoMock)
+
+ verifyNoMoreInteractions(dockInterfaceMock)
+ }
+
+ @Test
+ fun onTaskMovedToFront_taskNotForDefaultDisplay_appLaunchedNotCalled() {
+ val currentUserId = 10
+ val nonDefaultDisplay = Display.DEFAULT_DISPLAY + 2
+ val taskComponentName = ComponentName("testPkgName", "testClassName")
+ runningTaskInfoMock.userId = currentUserId
+ runningTaskInfoMock.displayId = nonDefaultDisplay
+ runningTaskInfoMock.baseActivity = taskComponentName
+
+ val dockTaskStackChangeListener =
+ DockTaskStackChangeListener(currentUserId, dockInterfaceMock)
+ dockTaskStackChangeListener.onTaskMovedToFront(runningTaskInfoMock)
+
+ verifyNoMoreInteractions(dockInterfaceMock)
+ }
+
+ @Test
+ fun onTaskMovedToFront_taskMissingBaseActivityAndBaseIntentComponent_appLaunchedNotCalled() {
+ val currentUserId = 10
+ runningTaskInfoMock.userId = currentUserId
+ runningTaskInfoMock.displayId = Display.DEFAULT_DISPLAY
+ runningTaskInfoMock.baseActivity = null
+ whenever(intentMock.component).thenReturn(null)
+ runningTaskInfoMock.baseIntent = intentMock
+
+ val dockTaskStackChangeListener =
+ DockTaskStackChangeListener(currentUserId, dockInterfaceMock)
+ dockTaskStackChangeListener.onTaskMovedToFront(runningTaskInfoMock)
+
+ verifyNoMoreInteractions(dockInterfaceMock)
+ }
+
+ @Test
+ fun onTaskMovedToFront_appLaunchedCalled() {
+ val currentUserId = 10
+ val taskComponentName = ComponentName("testPkgName", "testClassName")
+ runningTaskInfoMock.userId = currentUserId
+ runningTaskInfoMock.displayId = Display.DEFAULT_DISPLAY
+ runningTaskInfoMock.baseActivity = taskComponentName
+
+ val dockTaskStackChangeListener =
+ DockTaskStackChangeListener(currentUserId, dockInterfaceMock)
+ dockTaskStackChangeListener.onTaskMovedToFront(runningTaskInfoMock)
+
+ verify(dockInterfaceMock).appLaunched(eq(taskComponentName))
+ }
+}
diff --git a/docklib/tests/src/com/android/car/docklib/task/TaskUtilsTest.kt b/docklib/tests/src/com/android/car/docklib/task/TaskUtilsTest.kt
new file mode 100644
index 0000000..f90e0a3
--- /dev/null
+++ b/docklib/tests/src/com/android/car/docklib/task/TaskUtilsTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 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.docklib.task
+
+import android.app.ActivityManager
+import android.content.ComponentName
+import android.content.Intent
+import android.net.Uri
+import android.view.Display
+import com.android.car.docklib.media.MediaUtils
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class TaskUtilsTest {
+ private val runningTaskInfoMock = mock<ActivityManager.RunningTaskInfo> {}
+ private val intentMock = mock<Intent> {}
+ private val uriMock = mock<Uri> {}
+
+ @Test
+ fun getComponentName_taskMissingBaseActivityAndBaseIntentComponent_returnsNull() {
+ val currentUserId = 10
+ runningTaskInfoMock.userId = currentUserId
+ runningTaskInfoMock.displayId = Display.DEFAULT_DISPLAY
+ runningTaskInfoMock.baseActivity = null
+ whenever(intentMock.component).thenReturn(null)
+ runningTaskInfoMock.baseIntent = intentMock
+
+ val ret = TaskUtils.getComponentName(runningTaskInfoMock)
+
+ assertThat(ret).isNull()
+ }
+
+ @Test
+ fun getComponentName_returnsComponent() {
+ val currentUserId = 10
+ val taskComponentName = ComponentName("testPkgName", "testClassName")
+ runningTaskInfoMock.userId = currentUserId
+ runningTaskInfoMock.displayId = Display.DEFAULT_DISPLAY
+ runningTaskInfoMock.baseActivity = taskComponentName
+
+ val ret = TaskUtils.getComponentName(runningTaskInfoMock)
+
+ assertThat(ret).isEqualTo(taskComponentName)
+ }
+
+ @Test
+ fun getComponentName_mediaComponent_returnsMediaComponent() {
+ val mediaComponentName = ComponentName("testPkg", "testClass")
+ val currentUserId = 10
+ runningTaskInfoMock.userId = currentUserId
+ runningTaskInfoMock.displayId = Display.DEFAULT_DISPLAY
+ whenever(intentMock.component).doReturn(MediaUtils.CAR_MEDIA_ACTIVITY)
+ whenever(intentMock.data).doReturn(uriMock)
+ whenever(uriMock.scheme).doReturn(MediaUtils.CAR_MEDIA_DATA_SCHEME)
+ whenever(uriMock.schemeSpecificPart).doReturn("/" + mediaComponentName.flattenToString())
+ runningTaskInfoMock.baseIntent = intentMock
+
+ val ret = TaskUtils.getComponentName(runningTaskInfoMock)
+
+ assertThat(ret).isEqualTo(mediaComponentName)
+ }
+}
diff --git a/docklib/tests/src/com/android/car/docklib/view/DockAdapterTest.kt b/docklib/tests/src/com/android/car/docklib/view/DockAdapterTest.kt
index d39ed0f..19541ba 100644
--- a/docklib/tests/src/com/android/car/docklib/view/DockAdapterTest.kt
+++ b/docklib/tests/src/com/android/car/docklib/view/DockAdapterTest.kt
@@ -1,122 +1,138 @@
package com.android.car.docklib.view
+import android.content.ComponentName
import android.content.Context
-import android.content.Intent
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.car.docklib.TestUtils
+import com.android.car.docklib.DockInterface
import com.android.car.docklib.data.DockAppItem
-import com.google.common.truth.Truth.assertThat
-import java.util.function.Consumer
+import org.junit.After
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
+import org.mockito.kotlin.verifyNoMoreInteractions
+import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
class DockAdapterTest {
- private val contextMock = mock<Context> {}
- private val intentConsumerMock = mock<Consumer<Intent>> {}
+
+ private companion object {
+ private const val PACKAGE = "package"
+ }
+
+ private val dockInterfaceMock = mock<DockInterface> {}
private val dockItemViewHolderMock = mock<DockItemViewHolder> {}
+ private val componentName = mock<ComponentName> {
+ on { packageName } doReturn PACKAGE
+ }
+ private val runnableMock = mock<Runnable> {}
+ private val contextMock = mock<Context> {}
+ private val dockAdapter: DockAdapter = spy(DockAdapter(dockInterfaceMock, contextMock))
+ private val dockItemList: MutableList<DockAppItem> = mutableListOf()
- @Test
- fun setItems_dockSizeEqualToListSize_adapterHasDockSize() {
- val defaultApps =
- listOf(TestUtils.createAppItem(app = "a"), TestUtils.createAppItem(app = "b"))
- val adapter = spy(DockAdapter(defaultApps.size, intentConsumerMock, contextMock))
-
- adapter.setItems(defaultApps)
-
- assertThat(adapter.itemCount).isEqualTo(defaultApps.size)
- verify(adapter).notifyItemChanged(0)
- verify(adapter).notifyItemChanged(1)
+ @Before
+ fun setup() {
+ for (i in 0..5) {
+ dockItemList.add(mock<DockAppItem> {})
+ }
+ dockAdapter.submitList(dockItemList)
+ doReturn(componentName).whenever(dockItemList[1]).component
}
- @Test
- fun setItems_dockSizeLessThanListSize_adapterHasDockSize() {
- val defaultApps =
- listOf(
- TestUtils.createAppItem(app = "a"),
- TestUtils.createAppItem(app = "b"),
- TestUtils.createAppItem(app = "c")
- )
- val dockSize = 2
- val adapter = spy(DockAdapter(dockSize, intentConsumerMock, contextMock))
-
- adapter.setItems(defaultApps)
-
- assertThat(adapter.itemCount).isEqualTo(dockSize)
- verify(adapter).notifyItemChanged(0)
- verify(adapter).notifyItemChanged(1)
- }
-
- @Test
- fun setItems_dockSizeGreaterThanListSize_adapterHasDockSize() {
- val defaultApps =
- listOf(TestUtils.createAppItem(app = "a"), TestUtils.createAppItem(app = "b"))
- val dockSize = 3
- val adapter = spy(DockAdapter(dockSize, intentConsumerMock, contextMock))
-
- adapter.setItems(defaultApps)
-
- assertThat(adapter.itemCount).isEqualTo(dockSize)
- verify(adapter).notifyItemChanged(0)
- verify(adapter).notifyItemChanged(1)
- verify(adapter, times(0)).notifyItemChanged(2)
+ @After
+ fun tearDown() {
+ dockItemList.clear()
}
@Test
fun onBindViewHolder_emptyPayload_onBindViewHolderWithoutPayloadCalled() {
- val adapter = spy(DockAdapter(3, intentConsumerMock, contextMock))
+ dockAdapter.onBindViewHolder(dockItemViewHolderMock, 1, MutableList(0) {})
- adapter.onBindViewHolder(dockItemViewHolderMock, 1, MutableList(0) {})
-
- verify(adapter).onBindViewHolder(eq(dockItemViewHolderMock), eq(1))
+ verify(dockAdapter).onBindViewHolder(eq(dockItemViewHolderMock), eq(1))
}
@Test
- fun onBindViewHolder_nullPayload_onBindViewHolderWithoutPayloadCalled() {
- val adapter = spy(DockAdapter(3, intentConsumerMock, contextMock))
+ fun onBindViewHolder_nullPayload_noop() {
+ dockAdapter.onBindViewHolder(dockItemViewHolderMock, 1, MutableList(1) {})
- adapter.onBindViewHolder(dockItemViewHolderMock, 1, MutableList(1) {})
-
- verify(adapter).onBindViewHolder(eq(dockItemViewHolderMock), eq(1))
+ verifyNoMoreInteractions(dockItemViewHolderMock)
}
@Test
- fun onBindViewHolder_payloadOfIncorrectType_onBindViewHolderWithoutPayloadCalled() {
+ fun onBindViewHolder_payloadOfIncorrectType_noop() {
class DummyPayload
- val adapter = spy(DockAdapter(3, intentConsumerMock, contextMock))
- adapter.onBindViewHolder(dockItemViewHolderMock, 1, MutableList(1) {
- DummyPayload()
- })
+ dockAdapter.onBindViewHolder(dockItemViewHolderMock, 1, MutableList(1) { DummyPayload() })
- verify(adapter).onBindViewHolder(eq(dockItemViewHolderMock), eq(1))
+ verifyNoMoreInteractions(dockItemViewHolderMock)
}
@Test
- fun onBindViewHolder_payload_CHANGE_SAME_ITEM_TYPE_itemTypeChangedCalled() {
- val dockAppItem0 = mock<DockAppItem> {}
- val dockAppItem1 = mock<DockAppItem> {}
- val dockAppItem2 = mock<DockAppItem> {}
- val adapter = spy(
- DockAdapter(
- numItems = 3,
- intentConsumerMock,
- contextMock,
- items = arrayOf(dockAppItem0, dockAppItem1, dockAppItem2)
- )
+ fun onBindViewHolder_payloadChangeItemType_itemTypeChangedCalled() {
+ dockAdapter.onBindViewHolder(
+ dockItemViewHolderMock,
+ 1,
+ MutableList(1) { DockAdapter.PayloadType.CHANGE_ITEM_TYPE }
)
- adapter.onBindViewHolder(dockItemViewHolderMock, 1, MutableList(1) {
- DockAdapter.PayloadType.CHANGE_SAME_ITEM_TYPE
- })
+ verify(dockAdapter, never()).onBindViewHolder(eq(dockItemViewHolderMock), eq(1))
+ verify(dockItemViewHolderMock).itemTypeChanged(eq(dockItemList[1]))
+ }
- verify(adapter, never()).onBindViewHolder(eq(dockItemViewHolderMock), eq(1))
- verify(dockItemViewHolderMock).itemTypeChanged(eq(dockAppItem1))
+ @Test
+ fun onBindViewHolder_payloadMediaItemType_hasMediaSession_hasActiveMediaSessionCalled() {
+ dockAdapter.onMediaSessionChange(listOf(PACKAGE))
+ dockAdapter.onBindViewHolder(
+ dockItemViewHolderMock,
+ 1,
+ MutableList(1) { DockAdapter.PayloadType.CHANGE_ACTIVE_MEDIA_SESSION }
+ )
+
+ verify(dockAdapter, never()).onBindViewHolder(eq(dockItemViewHolderMock), eq(1))
+ verify(dockItemViewHolderMock).setHasActiveMediaSession(true)
+ }
+
+ @Test
+ fun onBindViewHolder_payloadMediaItemType_noMediaSession_hasActiveMediaSessionCalled() {
+ dockAdapter.onBindViewHolder(
+ dockItemViewHolderMock,
+ 1,
+ MutableList(1) { DockAdapter.PayloadType.CHANGE_ACTIVE_MEDIA_SESSION }
+ )
+
+ verify(dockAdapter, never()).onBindViewHolder(eq(dockItemViewHolderMock), eq(1))
+ verify(dockItemViewHolderMock).setHasActiveMediaSession(false)
+ }
+
+ @Test
+ fun onBindViewHolder_multiplePayloadChangeItemType_itemTypeChangedCalledMultipleTimes() {
+ dockAdapter.onBindViewHolder(
+ dockItemViewHolderMock,
+ 1,
+ MutableList(3) { DockAdapter.PayloadType.CHANGE_ITEM_TYPE }
+ )
+
+ verify(dockAdapter, never()).onBindViewHolder(eq(dockItemViewHolderMock), eq(1))
+ verify(dockItemViewHolderMock, times(3)).itemTypeChanged(eq(dockItemList[1]))
+ }
+
+ @Test
+ fun onBindViewHolder_setCallback_bindWithRunnable() {
+ dockAdapter.setCallback(1, runnableMock)
+ dockAdapter.onBindViewHolder(dockItemViewHolderMock, 1)
+
+ verify(dockItemViewHolderMock).bind(
+ eq(dockItemList[1]),
+ eq(false),
+ eq(runnableMock),
+ any()
+ )
}
}
diff --git a/docklib/tests/src/com/android/car/docklib/view/DockDragListenerTest.kt b/docklib/tests/src/com/android/car/docklib/view/DockDragListenerTest.kt
index 371f87e..cb3b05a 100644
--- a/docklib/tests/src/com/android/car/docklib/view/DockDragListenerTest.kt
+++ b/docklib/tests/src/com/android/car/docklib/view/DockDragListenerTest.kt
@@ -3,7 +3,6 @@
import android.content.ClipData
import android.content.ClipDescription
import android.content.ComponentName
-import android.content.Context
import android.content.res.Resources
import android.graphics.Point
import android.view.DragEvent
@@ -14,36 +13,29 @@
import android.view.SurfaceControl
import android.view.View
import androidx.core.animation.ValueAnimator
-import androidx.recyclerview.widget.RecyclerView
-import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.car.docklib.R
import com.android.car.docklib.view.DockDragListener.Companion.APP_ITEM_DRAG_TAG
import com.google.common.truth.Truth.assertThat
+import java.util.function.Consumer
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyFloat
+import org.mockito.ArgumentMatchers.isNull
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
-import org.mockito.kotlin.spy
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
class DockDragListenerTest {
private val resourcesMock = mock<Resources> {
- on { getInteger(eq(R.integer.drag_drop_animate_in_duration)) } doReturn 0
+ on { getInteger(eq(R.integer.drop_animation_scale_down_duration_ms)) } doReturn 0
+ on { getInteger(eq(R.integer.drop_animation_scale_up_duration_ms)) } doReturn 0
}
- private val contextMock = mock<Context> {
- on { resources } doReturn resourcesMock
- }
- private val itemViewMock = mock<View> {
- on { context } doReturn contextMock
- }
- private val viewHolderSpy = spy(object : ViewHolder(itemViewMock) {})
private val viewMock = mock<View> {}
private val dragEventMock = mock<DragEvent> {}
private val clipDescriptionMock = mock<ClipDescription> {}
@@ -51,18 +43,32 @@
private val clipDataItemMock = mock<ClipData.Item> {}
private val surfaceControlMock = mock<SurfaceControl> {}
private val surfaceControlTransactionMock = mock<SurfaceControl.Transaction> {}
- private val surfaceControlTransactionMock2 = mock<SurfaceControl.Transaction> {}
private val callbackMock = mock<DockDragListener.Callback> {}
private val valueAnimatorMock = mock<ValueAnimator> {}
+ private val booleanConsumerMock = mock<Consumer<Boolean>> {}
private val componentNameCaptor = argumentCaptor<ComponentName>()
- private var dockDragListener = DockDragListener(viewHolderSpy, callbackMock)
+ private var dockDragListener = object : DockDragListener(resourcesMock, callbackMock) {
+ override fun getAnimator(
+ surfaceControl: SurfaceControl,
+ fromX: Float,
+ fromY: Float,
+ toX: Float,
+ toY: Float,
+ fromScaleX: Float,
+ fromScaleY: Float,
+ toScaleX: Float,
+ toScaleY: Float,
+ animationDuration: Long
+ ): ValueAnimator {
+ return valueAnimatorMock
+ }
+ }
companion object {
private val VALID_COMPONENT_NAME = ComponentName(
"com.android.car.docklib.view",
DockDragListenerTest::javaClass.name
).flattenToString()
- private const val VALID_ADAPTER_POSITION = 0
}
@Test
@@ -108,29 +114,8 @@
}
@Test
- fun onDrag_ACTION_DROP_invalidPosition_returnFalse() {
- whenever(dragEventMock.action) doReturn ACTION_DROP
- whenever(viewHolderSpy.bindingAdapterPosition) doReturn RecyclerView.NO_POSITION
-
- val ret = dockDragListener.onDrag(viewMock, dragEventMock)
-
- assertThat(ret).isFalse()
- }
-
- @Test
- fun onDrag_ACTION_DROP_invalidPosition_resetViewCallbackTriggered() {
- whenever(dragEventMock.action) doReturn ACTION_DROP
- whenever(viewHolderSpy.bindingAdapterPosition) doReturn RecyclerView.NO_POSITION
-
- dockDragListener.onDrag(viewMock, dragEventMock)
-
- verify(callbackMock).resetView()
- }
-
- @Test
fun onDrag_ACTION_DROP_noClipData_returnFalse() {
whenever(dragEventMock.action) doReturn ACTION_DROP
- whenever(viewHolderSpy.bindingAdapterPosition) doReturn VALID_ADAPTER_POSITION
whenever(dragEventMock.clipData) doReturn clipDataMock
whenever(clipDataMock.getItemAt(any())) doReturn null
@@ -142,7 +127,6 @@
@Test
fun onDrag_ACTION_DROP_noClipData_resetViewCallbackTriggered() {
whenever(dragEventMock.action) doReturn ACTION_DROP
- whenever(viewHolderSpy.bindingAdapterPosition) doReturn VALID_ADAPTER_POSITION
whenever(dragEventMock.clipData) doReturn clipDataMock
whenever(clipDataMock.getItemAt(any())) doReturn null
@@ -154,7 +138,6 @@
@Test
fun onDrag_ACTION_DROP_clipDataWithNoText_returnFalse() {
whenever(dragEventMock.action) doReturn ACTION_DROP
- whenever(viewHolderSpy.bindingAdapterPosition) doReturn VALID_ADAPTER_POSITION
whenever(clipDataItemMock.text) doReturn null
whenever(clipDataMock.getItemAt(eq(0))) doReturn clipDataItemMock
whenever(dragEventMock.clipData) doReturn clipDataMock
@@ -167,7 +150,6 @@
@Test
fun onDrag_ACTION_DROP_clipDataWithNoText_resetViewCallbackTriggered() {
whenever(dragEventMock.action) doReturn ACTION_DROP
- whenever(viewHolderSpy.bindingAdapterPosition) doReturn VALID_ADAPTER_POSITION
whenever(clipDataItemMock.text) doReturn null
whenever(clipDataMock.getItemAt(eq(0))) doReturn clipDataItemMock
whenever(dragEventMock.clipData) doReturn clipDataMock
@@ -181,7 +163,6 @@
fun onDrag_ACTION_DROP_invalidComponentName_returnFalse() {
val invalidComponentName = "invalidComponentName"
whenever(dragEventMock.action) doReturn ACTION_DROP
- whenever(viewHolderSpy.bindingAdapterPosition) doReturn VALID_ADAPTER_POSITION
whenever(clipDataItemMock.text) doReturn invalidComponentName
whenever(clipDataMock.getItemAt(eq(0))) doReturn clipDataItemMock
whenever(dragEventMock.clipData) doReturn clipDataMock
@@ -195,7 +176,6 @@
fun onDrag_ACTION_DROP_invalidComponentName_resetViewCallbackTriggered() {
val invalidComponentName = "invalidComponentName"
whenever(dragEventMock.action) doReturn ACTION_DROP
- whenever(viewHolderSpy.bindingAdapterPosition) doReturn VALID_ADAPTER_POSITION
whenever(clipDataItemMock.text) doReturn invalidComponentName
whenever(clipDataMock.getItemAt(eq(0))) doReturn clipDataItemMock
whenever(dragEventMock.clipData) doReturn clipDataMock
@@ -208,7 +188,6 @@
@Test
fun onDrag_ACTION_DROP_noDragSurface_returnFalse() {
whenever(dragEventMock.action) doReturn ACTION_DROP
- whenever(viewHolderSpy.bindingAdapterPosition) doReturn VALID_ADAPTER_POSITION
whenever(clipDataItemMock.text) doReturn VALID_COMPONENT_NAME
whenever(clipDataMock.getItemAt(eq(0))) doReturn clipDataItemMock
whenever(dragEventMock.clipData) doReturn clipDataMock
@@ -222,7 +201,6 @@
@Test
fun onDrag_ACTION_DROP_noDragSurface_dragAcceptedCallbackTriggered() {
whenever(dragEventMock.action) doReturn ACTION_DROP
- whenever(viewHolderSpy.bindingAdapterPosition) doReturn VALID_ADAPTER_POSITION
whenever(clipDataItemMock.text) doReturn VALID_COMPONENT_NAME
whenever(clipDataMock.getItemAt(eq(0))) doReturn clipDataItemMock
whenever(dragEventMock.clipData) doReturn clipDataMock
@@ -230,7 +208,7 @@
dockDragListener.onDrag(viewMock, dragEventMock)
- verify(callbackMock).dragAccepted(componentNameCaptor.capture())
+ verify(callbackMock).dropSuccessful(componentNameCaptor.capture(), isNull())
assertThat(componentNameCaptor.firstValue).isNotNull()
assertThat(componentNameCaptor.firstValue.flattenToString()).isEqualTo(VALID_COMPONENT_NAME)
}
@@ -238,7 +216,6 @@
@Test
fun onDrag_ACTION_DROP_validPositionComponentNameDragSurface_returnTrue() {
whenever(dragEventMock.action) doReturn ACTION_DROP
- whenever(viewHolderSpy.bindingAdapterPosition) doReturn VALID_ADAPTER_POSITION
whenever(clipDataItemMock.text) doReturn VALID_COMPONENT_NAME
whenever(clipDataMock.getItemAt(eq(0))) doReturn clipDataItemMock
whenever(dragEventMock.clipData) doReturn clipDataMock
@@ -247,19 +224,6 @@
whenever(callbackMock.getDropLocation()).thenReturn(Point(0, 0))
whenever(callbackMock.getDropHeight()).thenReturn(10f)
whenever(callbackMock.getDropWidth()).thenReturn(10f)
- dockDragListener = object : DockDragListener(viewHolderSpy, callbackMock) {
- override fun getAnimator(
- surfaceControl: SurfaceControl,
- fromX: Float,
- fromY: Float,
- toX: Float,
- toY: Float,
- toScaleX: Float,
- toScaleY: Float
- ): ValueAnimator {
- return valueAnimatorMock
- }
- }
val ret = dockDragListener.onDrag(viewMock, dragEventMock)
@@ -269,7 +233,6 @@
@Test
fun onDrag_ACTION_DROP_validPositionComponentNameDragSurface_animationStarted() {
whenever(dragEventMock.action) doReturn ACTION_DROP
- whenever(viewHolderSpy.bindingAdapterPosition) doReturn VALID_ADAPTER_POSITION
whenever(clipDataItemMock.text) doReturn VALID_COMPONENT_NAME
whenever(clipDataMock.getItemAt(eq(0))) doReturn clipDataItemMock
whenever(dragEventMock.clipData) doReturn clipDataMock
@@ -278,19 +241,6 @@
whenever(callbackMock.getDropLocation()).thenReturn(Point(0, 0))
whenever(callbackMock.getDropHeight()).thenReturn(10f)
whenever(callbackMock.getDropWidth()).thenReturn(10f)
- dockDragListener = object : DockDragListener(viewHolderSpy, callbackMock) {
- override fun getAnimator(
- surfaceControl: SurfaceControl,
- fromX: Float,
- fromY: Float,
- toX: Float,
- toY: Float,
- toScaleX: Float,
- toScaleY: Float
- ): ValueAnimator {
- return valueAnimatorMock
- }
- }
dockDragListener.onDrag(viewMock, dragEventMock)
@@ -370,108 +320,39 @@
}
@Test
- fun getAnimatorListener_onAnimationEnd_transactionsClosed() {
+ fun getAnimatorListener_onAnimationEnd_callbackTriggered() {
whenever(surfaceControlMock.isValid) doReturn true
- val listener = dockDragListener.getAnimatorListener(
- surfaceControlMock,
- surfaceControlTransactionMock,
- surfaceControlTransactionMock2
- )
+ val listener = dockDragListener.getAnimatorListener(booleanConsumerMock)
listener.onAnimationEnd(valueAnimatorMock)
- verify(surfaceControlTransactionMock).close()
- verify(surfaceControlTransactionMock2).close()
+ verify(booleanConsumerMock).accept(
+ eq(false) // isCancelled
+ )
}
@Test
- fun getAnimatorListener_onAnimationCancel_transactionsClosed() {
+ fun getAnimatorListener_onAnimationCancel_callbackTriggered() {
whenever(surfaceControlMock.isValid) doReturn true
- val listener = dockDragListener.getAnimatorListener(
- surfaceControlMock,
- surfaceControlTransactionMock,
- surfaceControlTransactionMock2
- )
+ val listener = dockDragListener.getAnimatorListener(booleanConsumerMock)
listener.onAnimationCancel(valueAnimatorMock)
- verify(surfaceControlTransactionMock).close()
- verify(surfaceControlTransactionMock2).close()
+ verify(booleanConsumerMock).accept(
+ eq(true) // isCancelled
+ )
}
@Test
- fun getAnimatorListener_onAnimationCancelAndonAnimationEnd_transactionsClosedOnce() {
+ fun getAnimatorListener_onAnimationCancelAndonAnimationEnd_callbackTriggeredOnce() {
whenever(surfaceControlMock.isValid) doReturn true
- val listener = dockDragListener.getAnimatorListener(
- surfaceControlMock,
- surfaceControlTransactionMock,
- surfaceControlTransactionMock2
- )
+ val listener = dockDragListener.getAnimatorListener(booleanConsumerMock)
listener.onAnimationCancel(valueAnimatorMock)
listener.onAnimationEnd(valueAnimatorMock)
- verify(surfaceControlTransactionMock).close()
- verify(surfaceControlTransactionMock2).close()
- }
-
- @Test
- fun getAnimatorListener_onAnimationEnd_surfaceControlHidden() {
- whenever(surfaceControlMock.isValid) doReturn true
-
- val listener = dockDragListener.getAnimatorListener(
- surfaceControlMock,
- surfaceControlTransactionMock,
- surfaceControlTransactionMock2
+ verify(booleanConsumerMock).accept(
+ eq(true) // isCancelled
)
- listener.onAnimationEnd(valueAnimatorMock)
-
- verify(surfaceControlTransactionMock2).hide(eq(surfaceControlMock))
- verify(surfaceControlTransactionMock2).apply()
- }
-
- @Test
- fun getAnimatorListener_onAnimationCancel_surfaceControlHidden() {
- whenever(surfaceControlMock.isValid) doReturn true
-
- val listener = dockDragListener.getAnimatorListener(
- surfaceControlMock,
- surfaceControlTransactionMock,
- surfaceControlTransactionMock2
- )
- listener.onAnimationCancel(valueAnimatorMock)
-
- verify(surfaceControlTransactionMock2).hide(eq(surfaceControlMock))
- verify(surfaceControlTransactionMock2).apply()
- }
-
- @Test
- fun getAnimatorListener_onAnimationEnd_surfaceControlRemoved() {
- whenever(surfaceControlMock.isValid) doReturn true
-
- val listener = dockDragListener.getAnimatorListener(
- surfaceControlMock,
- surfaceControlTransactionMock,
- surfaceControlTransactionMock2
- )
- listener.onAnimationEnd(valueAnimatorMock)
-
- verify(surfaceControlTransactionMock2).remove(eq(surfaceControlMock))
- verify(surfaceControlTransactionMock2).apply()
- }
-
- @Test
- fun getAnimatorListener_onAnimationCancel_surfaceControlRemoved() {
- whenever(surfaceControlMock.isValid) doReturn true
-
- val listener = dockDragListener.getAnimatorListener(
- surfaceControlMock,
- surfaceControlTransactionMock,
- surfaceControlTransactionMock2
- )
- listener.onAnimationCancel(valueAnimatorMock)
-
- verify(surfaceControlTransactionMock2).remove(eq(surfaceControlMock))
- verify(surfaceControlTransactionMock2).apply()
}
}
diff --git a/docklib/tests/src/com/android/car/docklib/view/DockItemLongClickListenerTest.kt b/docklib/tests/src/com/android/car/docklib/view/DockItemLongClickListenerTest.kt
index d8ba54b..b2fbee5 100644
--- a/docklib/tests/src/com/android/car/docklib/view/DockItemLongClickListenerTest.kt
+++ b/docklib/tests/src/com/android/car/docklib/view/DockItemLongClickListenerTest.kt
@@ -1,7 +1,10 @@
package com.android.car.docklib.view
+import android.car.media.CarMediaManager
+import android.content.ComponentName
import android.content.Context
import android.content.res.Resources
+import android.os.UserHandle
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.car.docklib.TestUtils
@@ -19,17 +22,22 @@
@RunWith(AndroidJUnit4::class)
class DockItemLongClickListenerTest {
- private val dockAppItemMock = mock<DockAppItem>()
private val resourcesMock = mock<Resources>()
- private val contextMock = mock<Context> { on { resources } doReturn resourcesMock }
+ private val userHandleMock = mock<UserHandle>()
+ private val contextMock = mock<Context> {
+ on { resources } doReturn resourcesMock
+ on { user } doReturn userHandleMock
+ }
private val viewMock = mock<View> { on { context } doReturn contextMock }
private val runnableMock1 = mock<Runnable>()
private val runnableMock2 = mock<Runnable>()
private val carUiShortcutsPopupMock = mock<CarUiShortcutsPopup>()
- private val carUiShortcutsPopupBuilderMock = mock<CarUiShortcutsPopup.Builder>() {
+ private val carUiShortcutsPopupBuilderMock = mock<CarUiShortcutsPopup.Builder> {
on { addShortcut(any<CarUiShortcutsPopup.ShortcutItem>()) } doReturn it
on { build(any<Context>(), any<View>()) } doReturn carUiShortcutsPopupMock
}
+ private val carMediaManagerMock = mock<CarMediaManager>()
+
private lateinit var dockItemLongClickListener: DockItemLongClickListener
@Before
@@ -46,41 +54,48 @@
@Test
fun onLongClick_typeStatic_pinShortcutItem_parameterIsItemPinnedIsTrue() {
- dockItemLongClickListener =
- createDockItemLongClickListener(TestUtils.createAppItem(DockAppItem.Type.STATIC))
+ dockItemLongClickListener = createDockItemLongClickListener(
+ TestUtils.createAppItem(type = DockAppItem.Type.STATIC)
+ )
dockItemLongClickListener.onLongClick(viewMock)
verify(dockItemLongClickListener).createPinShortcutItem(
- any<Resources>(),
- eq(true),
- any<Runnable>(),
- any<Runnable>()
+ any<Resources>(),
+ eq(true),
+ any<Runnable>(),
+ any<Runnable>()
)
}
@Test
fun onLongClick_typeDynamic_pinShortcutItem_parameterIsItemPinnedIsFalse() {
- dockItemLongClickListener =
- createDockItemLongClickListener(TestUtils.createAppItem(DockAppItem.Type.DYNAMIC))
+ dockItemLongClickListener = createDockItemLongClickListener(
+ TestUtils.createAppItem(type = DockAppItem.Type.DYNAMIC)
+ )
dockItemLongClickListener.onLongClick(viewMock)
verify(dockItemLongClickListener).createPinShortcutItem(
- any<Resources>(),
- eq(false),
- any<Runnable>(),
- any<Runnable>()
+ any<Resources>(),
+ eq(false),
+ any<Runnable>(),
+ any<Runnable>()
)
}
private fun createDockItemLongClickListener(
- dockAppItem: DockAppItem = dockAppItemMock
+ dockAppItem: DockAppItem = TestUtils.createAppItem()
): DockItemLongClickListener {
return spy(object : DockItemLongClickListener(
dockAppItem,
runnableMock1,
- runnableMock2
+ runnableMock2,
+ ComponentName("testPkg", "testClass"),
+ contextMock,
+ carMediaManagerMock,
+ setOf()
+
) {
override fun createCarUiShortcutsPopupBuilder(): CarUiShortcutsPopup.Builder =
carUiShortcutsPopupBuilderMock
diff --git a/docklib/tests/src/com/android/car/docklib/view/animation/ExcitementAnimationHelperTest.kt b/docklib/tests/src/com/android/car/docklib/view/animation/ExcitementAnimationHelperTest.kt
new file mode 100644
index 0000000..ac12e3c
--- /dev/null
+++ b/docklib/tests/src/com/android/car/docklib/view/animation/ExcitementAnimationHelperTest.kt
@@ -0,0 +1,59 @@
+package com.android.car.docklib.view.animation
+
+import androidx.core.animation.Animator
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+
+@RunWith(AndroidJUnit4::class)
+class ExcitementAnimationHelperTest {
+ private val animatorMock = mock<Animator>()
+ private val successCallbackMock = mock<Runnable>()
+ private val failureCallbackMock = mock<Runnable>()
+
+ @Test
+ fun getAnimatorListener_onAnimationEnd_onlySuccessCallbackCalled() {
+ val animatorListener =
+ ExcitementAnimationHelper.getAnimatorListener(
+ successCallbackMock,
+ failureCallbackMock
+ )
+
+ animatorListener.onAnimationEnd(animatorMock)
+
+ verify(successCallbackMock).run()
+ verify(failureCallbackMock, never()).run()
+ }
+
+ @Test
+ fun getAnimatorListener_onAnimationCancel_onlyFailureCallbackCalled() {
+ val animatorListener =
+ ExcitementAnimationHelper.getAnimatorListener(
+ successCallbackMock,
+ failureCallbackMock
+ )
+
+ animatorListener.onAnimationCancel(animatorMock)
+
+ verify(failureCallbackMock).run()
+ verify(successCallbackMock, never()).run()
+ }
+
+ @Test
+ fun getAnimatorListener_onAnimationCancelAndonAnimationEnd_onlyFailureCallbackCalled() {
+ val animatorListener =
+ ExcitementAnimationHelper.getAnimatorListener(
+ successCallbackMock,
+ failureCallbackMock
+ )
+
+ animatorListener.onAnimationCancel(animatorMock)
+ animatorListener.onAnimationEnd(animatorMock)
+
+ verify(failureCallbackMock).run()
+ verify(successCallbackMock, never()).run()
+ }
+}
diff --git a/gradle.properties b/gradle.properties
index b5987b6..235e0e2 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -21,4 +21,4 @@
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
-android.nonTransitiveRClass=true
+android.nonTransitiveRClass=false
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 58944a6..a81b7b0 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Thu Sep 28 21:17:18 PDT 2023
+#Wed Jan 31 17:34:35 PST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/app/src/com/android/car/carlauncher/LaunchRootCarTaskViewCallbacks.java b/libs/aconfig-platform-compat/build.gradle.kts
similarity index 68%
copy from app/src/com/android/car/carlauncher/LaunchRootCarTaskViewCallbacks.java
copy to libs/aconfig-platform-compat/build.gradle.kts
index e30ffe5..f150500 100644
--- a/app/src/com/android/car/carlauncher/LaunchRootCarTaskViewCallbacks.java
+++ b/libs/aconfig-platform-compat/build.gradle.kts
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -14,10 +14,12 @@
* limitations under the License.
*/
-package com.android.car.carlauncher;
+plugins {
+ java
+}
-/**
- * A callbacks interface for {@link LaunchRootCarTaskView}.
- */
-public interface LaunchRootCarTaskViewCallbacks extends
- CarTaskViewCallbacks {}
+sourceSets {
+ main {
+ java.setSrcDirs(listOf("java"))
+ }
+}
diff --git a/libs/aconfig-platform-compat/java/android/compat/annotation/UnsupportedAppUsage.java b/libs/aconfig-platform-compat/java/android/compat/annotation/UnsupportedAppUsage.java
new file mode 120000
index 0000000..5dd4e31
--- /dev/null
+++ b/libs/aconfig-platform-compat/java/android/compat/annotation/UnsupportedAppUsage.java
@@ -0,0 +1 @@
+../../../../../../../../../../tools/platform-compat/java/android/compat/annotation/UnsupportedAppUsage.java
\ No newline at end of file
diff --git a/libs/aconfig-platform-compat/java/com/android/aconfig/annotations b/libs/aconfig-platform-compat/java/com/android/aconfig/annotations
new file mode 120000
index 0000000..591d7e9
--- /dev/null
+++ b/libs/aconfig-platform-compat/java/com/android/aconfig/annotations
@@ -0,0 +1 @@
+../../../../../../../../../../frameworks/libs/modules-utils/java/com/android/aconfig/annotations/
\ No newline at end of file
diff --git a/libs/appgrid/Android.bp b/libs/appgrid/Android.bp
index 06dd2c9..22b05f9 100644
--- a/libs/appgrid/Android.bp
+++ b/libs/appgrid/Android.bp
@@ -25,33 +25,46 @@
},
sdk_version: "module_current",
min_sdk_version: "31",
- srcs: ["lib/src/com/android/car/carlauncher/proto/launcher_item.proto"]
+ srcs: ["lib/src/com/android/car/carlauncher/proto/launcher_item.proto"],
}
android_library {
name: "CarAppGrid-lib",
platform_apis: true,
- srcs: ["lib/src/**/*.java",
- "lib/src/**/*.kt",
- "lib/hidden_apis_enabled/**/*.java"],
+ srcs: [
+ "lib/src/**/*.java",
+ "lib/src/**/*.kt",
+ ":hidden_api_enabled_srcs",
+ ],
resource_dirs: ["lib/res"],
static_libs: [
+ "CarLauncherCommon",
"androidx-constraintlayout_constraintlayout-solver",
"androidx-constraintlayout_constraintlayout",
"androidx.lifecycle_lifecycle-extensions",
"car-media-common",
"guava",
"car-ui-lib",
+ "car_launcher_flags_java_lib",
"launcher_item",
"CarDockUtilLib",
+ "androidx.lifecycle_lifecycle-runtime-ktx",
+ "androidx.lifecycle_lifecycle-viewmodel-ktx",
+ "androidx.activity_activity-ktx",
+ "androidx.fragment_fragment-ktx",
+ "androidx.core_core-ktx",
+ "kotlinx-coroutines-core",
+ "kotlinx-coroutines-android",
],
libs: ["android.car"],
manifest: "lib/AndroidManifest.xml",
+ // TODO(b/319708040): re-enable use_resource_processor
+ use_resource_processor: false,
}
// Uses test key instead of the regular "platform" key
@@ -64,8 +77,10 @@
resource_dirs: ["app/src/main/res"],
- srcs: ["app/src/**/*.java",
- "app/src/**/*.kt",],
+ srcs: [
+ "app/src/**/*.java",
+ "app/src/**/*.kt",
+ ],
platform_apis: true,
diff --git a/libs/appgrid/lib/AndroidManifest.xml b/libs/appgrid/lib/AndroidManifest.xml
index 3d3d8ab..ccae000 100644
--- a/libs/appgrid/lib/AndroidManifest.xml
+++ b/libs/appgrid/lib/AndroidManifest.xml
@@ -39,6 +39,7 @@
<activity
android:name="com.android.car.carlauncher.AppGridActivity"
android:launchMode="singleInstance"
+ android:windowSoftInputMode="adjustPan"
android:exported="true"
android:theme="@style/Theme.Launcher.AppGridActivity"
android:excludeFromRecents="true">
diff --git a/libs/appgrid/lib/build.gradle b/libs/appgrid/lib/build.gradle
index 2cd55c6..b082fa0 100644
--- a/libs/appgrid/lib/build.gradle
+++ b/libs/appgrid/lib/build.gradle
@@ -18,6 +18,14 @@
id 'com.android.library'
id "com.google.protobuf"
id 'kotlin-android'
+ id 'aconfig'
+}
+
+aconfig {
+ aconfigDeclaration {
+ packageName.set("com.android.car.carlauncher")
+ srcFile.setFrom(files("../../../app/car_launcher_flags.aconfig"))
+ }
}
android {
@@ -39,7 +47,6 @@
res.srcDirs = ['res']
java.srcDirs = ['src']
java {
- srcDirs += 'hidden_apis_disabled/src/com/android/car/carlauncher/hidden'
srcDirs += 'build/generated/source/proto/java'
}
kotlin {
@@ -49,6 +56,19 @@
srcDir 'src/com/android/car/carlauncher/proto' // default value
}
}
+
+ test {
+ java.srcDirs = ['robotests/src']
+ res.srcDirs = ['robotests/res']
+ }
+
+ }
+
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
+ returnDefaultValues = true
+ }
}
compileOptions {
@@ -68,26 +88,43 @@
dependencies {
compileOnly files(gradle.ext.lib_car_system_stubs)
compileOnly files(gradle.ext.lib_car_ui_lib_oem_apis)
+ implementation project(":libs:hidden-apis-compat:hidden-apis-disabled")
+ implementation project(":libs:car-launcher-common")
implementation project(":libs:car-apps-common")
implementation project(":libs:car-media-common")
implementation project(":libs:car-ui-lib")
+ implementation project(":docklib-util")
implementation 'androidx.core:core-ktx:1.12.0'
- api 'androidx.annotation:annotation:1.7.0'
+ api 'androidx.annotation:annotation:1.7.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
- implementation 'com.google.android.material:material:1.10.0'
- implementation 'com.android.car.ui:car-ui-lib:2.5.1'
+ implementation 'com.google.android.material:material:1.11.0'
implementation 'com.google.guava:guava:31.0.1-jre'
- testImplementation 'junit:junit:4.13.2'
- androidTestImplementation 'androidx.test.ext:junit:1.1.5'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
- implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "androidx.datastore:datastore:1.0.0"
- implementation "com.google.protobuf:protobuf-javalite:3.20.1"
+ implementation 'androidx.preference:preference-ktx:1.2.1'
+ implementation 'androidx.recyclerview:recyclerview:1.3.2'
+ implementation('com.google.protobuf:protobuf-javalite:3.25.1')
+ implementation 'androidx.media:media:1.7.0'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+ //Test dependencies
+ testImplementation files(gradle.ext.lib_car_system_stubs)
+ testImplementation files(gradle.ext.lib_car_test_api)
+ testImplementation 'junit:junit:4.13.2'
+ testImplementation 'androidx.test:core-ktx:1.5.0'
+
+ testImplementation 'org.robolectric:robolectric:4.11.1'
+
+ testImplementation "org.mockito:mockito-core:5.8.0"
+ testImplementation "org.mockito.kotlin:mockito-kotlin:3.2.0"
+
+ testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3"
+
}
protobuf {
protoc {
- artifact = "com.google.protobuf:protoc:3.23.4"
+ artifact = "com.google.protobuf:protoc:3.25.1"
}
// Generates the java Protobuf-lite code for the Protobufs in this project. See
diff --git a/libs/appgrid/lib/res/layout/app_grid_activity.xml b/libs/appgrid/lib/res/layout/app_grid_activity.xml
deleted file mode 100644
index 2554924..0000000
--- a/libs/appgrid/lib/res/layout/app_grid_activity.xml
+++ /dev/null
@@ -1,53 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2019 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.
--->
-<com.android.car.ui.FocusArea
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/focus_area"
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:gravity="center">
- <LinearLayout
- android:id="@+id/apps_grid_background"
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:gravity="center">
- <com.android.car.carlauncher.Banner
- android:id="@+id/tos_banner"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:visibility="gone"
- app:first_button_text="@string/banner_review_button_text"
- app:second_button_text="@string/banner_dismiss_button_text"
- app:title_text="@string/banner_title_text" />
- <com.android.car.carlauncher.AppGridRecyclerView
- android:id="@+id/apps_grid"
- android:layout_width="match_parent"
- android:layout_height="match_parent"/>
- <FrameLayout
- android:id="@+id/page_indicator_container"
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
- <com.android.car.carlauncher.PageIndicator
- android:id="@+id/page_indicator"
- android:layout_width="match_parent"
- android:layout_height="@dimen/page_indicator_height"/>
- </FrameLayout>
- </LinearLayout>
-</com.android.car.ui.FocusArea>
diff --git a/libs/appgrid/lib/res/layout/app_grid_container_activity.xml b/libs/appgrid/lib/res/layout/app_grid_container_activity.xml
new file mode 100644
index 0000000..aed1ce5
--- /dev/null
+++ b/libs/appgrid/lib/res/layout/app_grid_container_activity.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2024 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"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/fragmentContainer"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/libs/appgrid/lib/res/layout/app_grid_fragment.xml b/libs/appgrid/lib/res/layout/app_grid_fragment.xml
new file mode 100644
index 0000000..96bd0ea
--- /dev/null
+++ b/libs/appgrid/lib/res/layout/app_grid_fragment.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 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.
+-->
+<com.android.car.ui.FocusArea
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/focus_area"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center">
+ <FrameLayout
+ android:id="@+id/apps_grid_background_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center">
+ <com.android.car.carlauncher.Banner
+ app:first_button_text="@string/banner_review_button_text"
+ app:second_button_text="@string/banner_dismiss_button_text"
+ app:title_text="@string/banner_title_text"
+ android:id="@+id/tos_banner"
+ android:alpha="0.0"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:elevation="10dp"
+ android:visibility="gone" />
+ <LinearLayout
+ android:id="@+id/apps_grid_background"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center">
+ <com.android.car.carlauncher.AppGridRecyclerView
+ android:id="@+id/apps_grid"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+ <FrameLayout
+ android:id="@+id/page_indicator_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <com.android.car.carlauncher.PageIndicator
+ android:id="@+id/page_indicator"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/page_indicator_height"/>
+ </FrameLayout>
+ </LinearLayout>
+ </FrameLayout>
+</com.android.car.ui.FocusArea>
diff --git a/libs/appgrid/lib/res/layout/banner.xml b/libs/appgrid/lib/res/layout/banner.xml
index fca31c1..147d2f8 100644
--- a/libs/appgrid/lib/res/layout/banner.xml
+++ b/libs/appgrid/lib/res/layout/banner.xml
@@ -22,7 +22,7 @@
android:layout_height="wrap_content"
android:background="@color/banner_background_color">
<TextView
- android:id="@+id/second_button"
+ android:id="@+id/banner_second_button"
android:layout_marginEnd="56dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintWidth_max="@dimen/banner_button_maximum_width"
@@ -30,9 +30,9 @@
style="@style/BannerButton"/>
<TextView
android:background="@android:color/transparent"
- android:id="@+id/first_button"
+ android:id="@+id/banner_first_button"
android:layout_marginEnd="72dp"
- app:layout_constraintEnd_toStartOf="@id/second_button"
+ app:layout_constraintEnd_toStartOf="@id/banner_second_button"
app:layout_constraintWidth_max="@dimen/banner_button_maximum_width"
app:layout_constraintTop_toTopOf="parent"
style="@style/BannerButton"/>
@@ -45,12 +45,12 @@
android:layout_marginTop="@dimen/banner_content_margin"
android:layout_width="0dp"
android:textColor="@color/banner_title_text_color"
- app:layout_constraintEnd_toStartOf="@id/first_button"
- app:layout_constraintStart_toEndOf="@+id/icon"
+ app:layout_constraintEnd_toStartOf="@id/banner_first_button"
+ app:layout_constraintStart_toEndOf="@+id/banner_icon"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
- android:id="@+id/icon"
+ android:id="@+id/banner_icon"
android:layout_height="@dimen/banner_image_view_size"
android:layout_marginStart="35dp"
android:layout_marginTop="@dimen/banner_content_margin"
diff --git a/libs/appgrid/lib/res/values-af/strings.xml b/libs/appgrid/lib/res/values-af/strings.xml
index a9e994c..e4a4ed3 100644
--- a/libs/appgrid/lib/res/values-af/strings.xml
+++ b/libs/appgrid/lib/res/values-af/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Hierdie funksie sal alle gepasmaakte rangskikking verwyder. Wil jy voortgaan?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Alle apps"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Media-apps"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Stop app"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Appinligting"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> is gestop."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Stop app?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"As jy ’n app dwing om te stop, kan dit sleg optree."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"App kan nie gestop word nie."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> kan nie gebruik word terwyl jy bestuur nie."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Versteek ontfoutingapps"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Wys ontfoutingapps"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"voorneme:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-am/strings.xml b/libs/appgrid/lib/res/values-am/strings.xml
index e489dd5..694a108 100644
--- a/libs/appgrid/lib/res/values-am/strings.xml
+++ b/libs/appgrid/lib/res/values-am/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"ይህ ተግባር ሁሉንም ብጁ ቅደም ተከተል ማስያዞች ያስወግዳል። መቀጠል ይፈልጋሉ?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"ሁሉም መተግበሪያዎች"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"የሚዲያ መተግበሪያዎች"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"መተግበሪያን አቁም"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"የመተግበሪያ መረጃ"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> እንዲቆም ተደርጓል።"</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"መተግበሪያ ይቁም?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"አንድ መተግበሪያን በኃይል እንዲቆም ካደረጉት በትክክል ላይሠራ ይችላል።"</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"መተግበሪያው ሊቆም አይችልም።"</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> በመንዳት ላይ ሳለ ጥቅም ላይ መዋል አይችልም።"</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"የስህተት ማረሚያ መተግበሪያዎችን ደብቅ"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"የስህተት ማረሚያ መተግበሪያዎችን አሳይ"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-ar/strings.xml b/libs/appgrid/lib/res/values-ar/strings.xml
index b810128..a93f352 100644
--- a/libs/appgrid/lib/res/values-ar/strings.xml
+++ b/libs/appgrid/lib/res/values-ar/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"ستزيل هذه الدالة أي ترتيب مخصّص. هل تريد المتابعة؟"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"جميع التطبيقات"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"تطبيقات الوسائط"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"إيقاف التطبيق"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"معلومات التطبيقات"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"تم إيقاف \"<xliff:g id="APP_NAME">%1$s</xliff:g>\"."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"هل تريد إيقاف التطبيق؟"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"في حال فرض إيقاف التطبيق، قد لا يعمل بشكل صحيح."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"يتعذّر إيقاف التطبيق."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"لا يمكن استخدام \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" أثناء القيادة."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"إخفاء تطبيقات تصحيح الأخطاء"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"إظهار تطبيقات تصحيح الأخطاء"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-as/strings.xml b/libs/appgrid/lib/res/values-as/strings.xml
index 8003b6b..a51fe0f 100644
--- a/libs/appgrid/lib/res/values-as/strings.xml
+++ b/libs/appgrid/lib/res/values-as/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"এই ফাংশ্বনটোৱে আটাইবোৰ কাষ্টম অৰ্ডাৰ আঁতৰাব। আপুনি অব্যাহত ৰাখিব বিচাৰেনে?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"আটাইবোৰ এপ্"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"মিডিয়া এপ্"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"এপ্ বন্ধ কৰক"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"এপৰ তথ্য"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> বন্ধ কৰা হৈছে।"</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"এপ্ বন্ধ কৰিবনে?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"আপুনি কোনো এপ্ বলেৰে বন্ধ কৰিবলৈ চেষ্টা কৰিলে, ই অস্বাভাৱিক আচৰণ কৰিব পাৰে।"</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"এপ্ বন্ধ কৰিব নোৱাৰি।"</string>
- <string name="driving_toast_text" msgid="397905281933065053">"গাড়ী চলাই থকাৰ সময়ত <xliff:g id="APP_NAME">%1$s</xliff:g> ব্যৱহাৰ কৰিব নোৱাৰি।"</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"ডিবাগ এপ্সমূহ লুকুৱাওক"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"ডিবাগ এপ্সমূহ দেখুৱাওক"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-az/strings.xml b/libs/appgrid/lib/res/values-az/strings.xml
index ff2b6a9..fc5918d 100644
--- a/libs/appgrid/lib/res/values-az/strings.xml
+++ b/libs/appgrid/lib/res/values-az/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Bu funksiya fərdi sıralamanı siləcək. Davam edilsin?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Bütün tətbiqlər"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Media tətbiqləri"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Tətbiqi dayandırın"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Tətbiq haqqında"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> dayandırılıb."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Tətbiq dayandırılsın?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Tətbiqi məcburi dayandırsanız, səhv işləyə bilər."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Tətbiqi dayandırmaq olmur."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Avtomobil sürərkən <xliff:g id="APP_NAME">%1$s</xliff:g> istifadə edilə bilməz."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Sazlama tətbiqlərini gizlədin"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Sazlama tətbiqlərini göstərin"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-b+sr+Latn/strings.xml b/libs/appgrid/lib/res/values-b+sr+Latn/strings.xml
index a7863c7..1a36a31 100644
--- a/libs/appgrid/lib/res/values-b+sr+Latn/strings.xml
+++ b/libs/appgrid/lib/res/values-b+sr+Latn/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Ova funkcija će ukloniti celokupan prilagođen raspored. Želite da nastavite?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Sve aplikacije"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Medijske aplikacije"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Zaustavi aplikaciju"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Informacije o aplikaciji"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"Aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g> je zaustavljena."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Želite da zaustavite aplikaciju?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Ako prinudno zaustavite aplikaciju, možda će se ponašati neočekivano."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Aplikacija ne može da se zaustavi."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> ne može da se koristi tokom vožnje."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Sakrij aplikacije za otklanjanje grešaka"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Prikaži aplikacije za otklanjanje grešaka"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-be/strings.xml b/libs/appgrid/lib/res/values-be/strings.xml
index 71381d0..7dfb707 100644
--- a/libs/appgrid/lib/res/values-be/strings.xml
+++ b/libs/appgrid/lib/res/values-be/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Гэта функцыя скасуе любы карыстальніцкі парадак сартавання. Працягнуць?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Усе праграмы"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Мультымедыйныя праграмы"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Спыніць праграму"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Звесткі пра праграму"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"Праграма \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" спынена."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Спыніць праграму?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Прымусовае спыненне праграмы можа прывесці да збою."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Немагчыма спыніць праграму."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Праграмай \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" нельга карыстацца, калі вы за рулём."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Схаваць праграмы адладкі"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Паказаць праграмы адладкі"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-bg/strings.xml b/libs/appgrid/lib/res/values-bg/strings.xml
index d07284f..9efac53 100644
--- a/libs/appgrid/lib/res/values-bg/strings.xml
+++ b/libs/appgrid/lib/res/values-bg/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Тази функция ще премахне персонализираното нареждане. Искате ли да продължите?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Всички приложения"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Медийни приложения"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Спиране на приложението"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Информация от приложенията"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"Приложението <xliff:g id="APP_NAME">%1$s</xliff:g> е спряно."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Да се спре ли приложението?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Ако принудително спрете приложение, то може да не функционира правилно."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Приложението не може да бъде спряно."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> не може да се използва при шофиране."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Скриване на приложенията за отстраняване на грешки"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Показване на приложенията за отстраняв. на грешки"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-bn/strings.xml b/libs/appgrid/lib/res/values-bn/strings.xml
index 45d4e60..a3fe8df 100644
--- a/libs/appgrid/lib/res/values-bn/strings.xml
+++ b/libs/appgrid/lib/res/values-bn/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"এই ফাংশন সবকটি কাস্টম অর্ডারিং সরিয়ে দেবে। আপনি কি এগিয়ে যেতে চান?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"সব অ্যাপ"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"মিডিয়া অ্যাপ"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"অ্যাপ বন্ধ করুন"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"অ্যাপের তথ্য"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> বন্ধ করা হয়েছে।"</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"অ্যাপ বন্ধ করবেন?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"আপনি কোনও অ্যাপকে জোর করে বন্ধ করলে, তা সঠিক ভাবে কাজ নাও করতে পারে।"</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"অ্যাপ বন্ধ করা যাচ্ছে না।"</string>
- <string name="driving_toast_text" msgid="397905281933065053">"ড্রাইভ করার সময় <xliff:g id="APP_NAME">%1$s</xliff:g> ব্যবহার করা যাবে না।"</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"ডিবাগ অ্যাপ লুকান"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"ডিবাগ অ্যাপ দেখুন"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-bs/strings.xml b/libs/appgrid/lib/res/values-bs/strings.xml
index 1f931a9..09d2753 100644
--- a/libs/appgrid/lib/res/values-bs/strings.xml
+++ b/libs/appgrid/lib/res/values-bs/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Ova funkcija će ukloniti sve prilagođene narudžbe. Želite li nastaviti?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Sve aplikacije"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Medijske aplikacije"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Zaustavi aplikaciju"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Informacije o aplikaciji"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"Aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g> je zaustavljena."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Zaustaviti aplikaciju?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Ako prisilno zaustavite aplikaciju, moguće je da će se ponašati nepredviđeno."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Nije moguće zaustaviti aplikaciju."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Nije moguće koristiti aplikaciju <xliff:g id="APP_NAME">%1$s</xliff:g> tokom vožnje."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Sakrij aplikacije za otklanjanje grešaka"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Prikaži aplikacije za otklanjanje grešaka"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-ca/strings.xml b/libs/appgrid/lib/res/values-ca/strings.xml
index bad0936..53d48a0 100644
--- a/libs/appgrid/lib/res/values-ca/strings.xml
+++ b/libs/appgrid/lib/res/values-ca/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Aquesta funció suprimirà l\'ordre personalitzat. Vols continuar?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Totes les aplicacions"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Aplicacions multimèdia"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Atura l\'aplicació"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Informació de l\'aplicació"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> s\'ha aturat."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Vols aturar l\'aplicació?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Si forces l\'aturada d\'una aplicació, és possible que no funcioni correctament."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"L\'aplicació no es pot aturar."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"No es pot utilitzar <xliff:g id="APP_NAME">%1$s</xliff:g> mentre es condueix."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Amaga les aplicacions de depuració"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Mostra les aplicacions de depuració"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-cs/strings.xml b/libs/appgrid/lib/res/values-cs/strings.xml
index 42c7b54..0c6d134 100644
--- a/libs/appgrid/lib/res/values-cs/strings.xml
+++ b/libs/appgrid/lib/res/values-cs/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Tato funkce zruší veškeré vlastní řazení. Chcete pokračovat?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Všechny aplikace"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Mediální aplikace"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Zastavit aplikaci"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Informace o aplikaci"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"Aplikace <xliff:g id="APP_NAME">%1$s</xliff:g> byla zastavena."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Zastavit aplikaci?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Vynucené zastavení může způsobit nepředvídatelné chování aplikace."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Aplikaci nelze ukončit."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Aplikaci <xliff:g id="APP_NAME">%1$s</xliff:g> nelze používat při řízení."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Skrýt ladicí aplikace"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Zobrazit ladicí aplikace"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-da/strings.xml b/libs/appgrid/lib/res/values-da/strings.xml
index 90f9b1e..39b61d7 100644
--- a/libs/appgrid/lib/res/values-da/strings.xml
+++ b/libs/appgrid/lib/res/values-da/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Denne funktion fjerner alle tilpassede rækkefølger. Vil du fortsætte?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Alle apps"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Medieapps"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Stands app"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Appoplysninger"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> er blevet standset."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Vil du standse appen?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Hvis du tvinger en app til at standse, kan det medføre, at den ikke fungerer korrekt."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Appen kan ikke standses."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> kan ikke bruges under kørsel."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Skjul apps til fejlretning"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Vis apps til fejlretning"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-de/strings.xml b/libs/appgrid/lib/res/values-de/strings.xml
index 259b82b..7817c11 100644
--- a/libs/appgrid/lib/res/values-de/strings.xml
+++ b/libs/appgrid/lib/res/values-de/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Durch diese Funktion wird die gesamte benutzerdefinierte Sortierung entfernt. Möchtest du fortfahren?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Alle Apps"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Medien-Apps"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"App beenden"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"App-Info"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> wurde beendet."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"App beenden?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Das Beenden der App zu erzwingen kann zu unerwünschtem Verhalten führen."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Die App kann nicht beendet werden."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> kann während der Fahrt nicht genutzt werden."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Debug-Apps verbergen"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Debug-Apps anzeigen"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-el/strings.xml b/libs/appgrid/lib/res/values-el/strings.xml
index d5f12fa..4a98939 100644
--- a/libs/appgrid/lib/res/values-el/strings.xml
+++ b/libs/appgrid/lib/res/values-el/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Αυτή η λειτουργία θα καταργήσει όλες τις προσαρμοσμένες ταξινομήσεις. Θέλετε να συνεχίσετε;"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Όλες οι εφαρμογές"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Εφαρμογές μέσων"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Διακοπή εφαρμογής"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Πληροφορίες εφαρμογής"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"Η εφαρμογή <xliff:g id="APP_NAME">%1$s</xliff:g> διακόπηκε."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Διακοπή εφαρμογής;"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Αν κάνετε αναγκαστική διακοπή μιας εφαρμογής, ενδέχεται να μην λειτουργεί σωστά."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Δεν είναι δυνατή η διακοπή της εφαρμογής."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Αδύνατη η χρήση της εφαρμογής <xliff:g id="APP_NAME">%1$s</xliff:g> κατά την οδήγηση."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Απόκρυψη εφαρμογών εντοπισμού σφαλμάτων"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Εμφάνιση εφαρμογών εντοπισμού σφαλμάτων"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-en-rAU/strings.xml b/libs/appgrid/lib/res/values-en-rAU/strings.xml
index c7d3cc2..06e44c5 100644
--- a/libs/appgrid/lib/res/values-en-rAU/strings.xml
+++ b/libs/appgrid/lib/res/values-en-rAU/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"This function will remove all customised ordering. Do you want to continue?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"All apps"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Media apps"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Stop app"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"App info"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> has been stopped."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Stop app?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"If you force stop an app, it may misbehave."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"App can\'t be stopped."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> can\'t be used while driving."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Hide debug apps"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Show debug apps"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-en-rCA/strings.xml b/libs/appgrid/lib/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000..09c85fd
--- /dev/null
+++ b/libs/appgrid/lib/res/values-en-rCA/strings.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright (C) 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="reset_appgrid_title" msgid="6491348358859198288">"Reset app grid to A-Z order"</string>
+ <string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"This function will remove all custom ordering. Do you want to continue?"</string>
+ <string name="app_launcher_title_all_apps" msgid="3522783138519460233">"All apps"</string>
+ <string name="app_launcher_title_media_only" msgid="7194631822174015710">"Media apps"</string>
+ <string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"App can’t be stopped."</string>
+ <string name="hide_debug_apps" msgid="7140064693464751647">"Hide debug apps"</string>
+ <string name="show_debug_apps" msgid="2748157232151197494">"Show debug apps"</string>
+ <string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
+ <string name="banner_title_text" msgid="8827498256184464356">"To use user tos disabled apps, agree to User tos"</string>
+ <string name="banner_review_button_text" msgid="369410598918950148">"Review"</string>
+ <string name="banner_dismiss_button_text" msgid="5389352614429069562">"Not Now"</string>
+</resources>
diff --git a/libs/appgrid/lib/res/values-en-rGB/strings.xml b/libs/appgrid/lib/res/values-en-rGB/strings.xml
index c7d3cc2..06e44c5 100644
--- a/libs/appgrid/lib/res/values-en-rGB/strings.xml
+++ b/libs/appgrid/lib/res/values-en-rGB/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"This function will remove all customised ordering. Do you want to continue?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"All apps"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Media apps"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Stop app"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"App info"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> has been stopped."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Stop app?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"If you force stop an app, it may misbehave."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"App can\'t be stopped."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> can\'t be used while driving."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Hide debug apps"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Show debug apps"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-en-rIN/strings.xml b/libs/appgrid/lib/res/values-en-rIN/strings.xml
index c7d3cc2..06e44c5 100644
--- a/libs/appgrid/lib/res/values-en-rIN/strings.xml
+++ b/libs/appgrid/lib/res/values-en-rIN/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"This function will remove all customised ordering. Do you want to continue?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"All apps"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Media apps"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Stop app"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"App info"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> has been stopped."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Stop app?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"If you force stop an app, it may misbehave."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"App can\'t be stopped."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> can\'t be used while driving."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Hide debug apps"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Show debug apps"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-en-rXC/strings.xml b/libs/appgrid/lib/res/values-en-rXC/strings.xml
index a993053..0d9b4bb 100644
--- a/libs/appgrid/lib/res/values-en-rXC/strings.xml
+++ b/libs/appgrid/lib/res/values-en-rXC/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"This function will remove all custom ordering. Do you want to continue?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"All apps"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Media apps"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Stop app"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"App info"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> has been stopped."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Stop app?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"If you force stop an app, it may misbehave."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"App can’t be stopped."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> can\'t be used while driving."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Hide debug apps"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Show debug apps"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-es-rUS/strings.xml b/libs/appgrid/lib/res/values-es-rUS/strings.xml
index 80e0ae2..bf1fdbc 100644
--- a/libs/appgrid/lib/res/values-es-rUS/strings.xml
+++ b/libs/appgrid/lib/res/values-es-rUS/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Esta función quitará todo el orden personalizado. ¿Quieres continuar?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Todas las apps"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Apps de música"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Detener app"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Información de la app"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"Se detuvo <xliff:g id="APP_NAME">%1$s</xliff:g>."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"¿Quieres detener la app?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Si fuerzas la detención de una app, es posible que funcione incorrectamente."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"No se puede detener la app."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"No puedes usar <xliff:g id="APP_NAME">%1$s</xliff:g> mientras conduces."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Ocultar apps de depuración"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Mostrar apps de depuración"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-es/strings.xml b/libs/appgrid/lib/res/values-es/strings.xml
index fc387e2..63cbadf 100644
--- a/libs/appgrid/lib/res/values-es/strings.xml
+++ b/libs/appgrid/lib/res/values-es/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Esta función eliminará todo el orden personalizado. ¿Quieres continuar?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Todas las aplicaciones"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Aplicaciones multimedia"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Detener aplicación"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Información de la aplicación"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> se ha detenido."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"¿Detener la aplicación?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Si fuerzas la detención de una aplicación, puede que no funcione correctamente."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"La aplicación no se puede detener."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> no se puede usar mientras se conduce."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Ocultar aplicaciones de depuración"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Mostrar aplicaciones de depuración"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-et/strings.xml b/libs/appgrid/lib/res/values-et/strings.xml
index a009afc..dfb991f 100644
--- a/libs/appgrid/lib/res/values-et/strings.xml
+++ b/libs/appgrid/lib/res/values-et/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"See funktsioon eemaldab kogu kohandatud järjestuse. Kas soovite jätkata?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Kõik rakendused"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Meediarakendused"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Peata rakendus"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Rakenduse teave"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> peatati."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Kas peatada rakendus?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Kui sundpeatate rakenduse, võib see valesti toimida."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Rakendust ei saa peatada."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Rakendust <xliff:g id="APP_NAME">%1$s</xliff:g> ei saa sõidu ajal kasutada."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Peida silumisrakendused"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Kuva silumisrakendused"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-eu/strings.xml b/libs/appgrid/lib/res/values-eu/strings.xml
index 9a2cbc1..8100fe5 100644
--- a/libs/appgrid/lib/res/values-eu/strings.xml
+++ b/libs/appgrid/lib/res/values-eu/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Funtzio horrek hurrenkera pertsonalizatua kenduko du. Aurrera egin nahi duzu?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Aplikazio guztiak"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Multimedia-aplikazioak"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Gelditu aplikazioa"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Aplikazioari buruzko informazioa"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"Gelditu da <xliff:g id="APP_NAME">%1$s</xliff:g>."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Aplikazioa gelditu nahi duzu?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Aplikazioak gelditzera behartzen badituzu, baliteke behar bezala ez funtzionatzea."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Ezin da gelditu aplikazioa."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> ezin da erabili gidatu bitartean."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Ezkutatu arazteko aplikazioak"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Erakutsi arazteko aplikazioak"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-fa/strings.xml b/libs/appgrid/lib/res/values-fa/strings.xml
index d179c4f..1750368 100644
--- a/libs/appgrid/lib/res/values-fa/strings.xml
+++ b/libs/appgrid/lib/res/values-fa/strings.xml
@@ -21,17 +21,11 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"این عملکرد همه ترتیبهای سفارشی را حذف خواهد کرد. میخواهید ادامه دهید؟"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"همه برنامهها"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"برنامههای رسانه"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"متوقف کردن برنامه"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"اطلاعات برنامه"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> متوقف شده است."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"برنامه متوقف شود؟"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"توقف اجباری برنامه ممکن است باعث عملکرد نادرست آن شود."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"برنامه متوقف نمیشود."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"هنگام رانندگی نمیتوان از <xliff:g id="APP_NAME">%1$s</xliff:g> استفاده کرد."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"پنهان کردن برنامههای اشکالزدایی"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"نمایش برنامههای اشکالزدایی"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
<string name="banner_title_text" msgid="8827498256184464356">"برای استفاده از برنامههایی که بهدلیل شرایط خدمات کاربر غیرفعال شدهاند، با «شرایط خدمات کاربر» موافقت کنید"</string>
<string name="banner_review_button_text" msgid="369410598918950148">"مرور کردن"</string>
- <string name="banner_dismiss_button_text" msgid="5389352614429069562">"اکنون نه"</string>
+ <string name="banner_dismiss_button_text" msgid="5389352614429069562">"حالا نه"</string>
</resources>
diff --git a/libs/appgrid/lib/res/values-fi/strings.xml b/libs/appgrid/lib/res/values-fi/strings.xml
index 6ad5ac6..e56ac63 100644
--- a/libs/appgrid/lib/res/values-fi/strings.xml
+++ b/libs/appgrid/lib/res/values-fi/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Toiminto poistaa kaikki omat järjestykset. Haluatko jatkaa?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Kaikki sovellukset"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Mediasovellukset"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Sulje sovellus"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Sovelluksen tiedot"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> on suljettu."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Suljetaanko sovellus?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Jos pakotat sovelluksen sulkeutumaan, se ei välttämättä toimi oikein."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Sovellusta ei voi keskeyttää."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> ei voi olla käytössä ajon aikana."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Piilota virheenkorjaussovellukset"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Näytä virheenkorjaussovellukset"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-fr-rCA/strings.xml b/libs/appgrid/lib/res/values-fr-rCA/strings.xml
index 2203ae8..49d83e5 100644
--- a/libs/appgrid/lib/res/values-fr-rCA/strings.xml
+++ b/libs/appgrid/lib/res/values-fr-rCA/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Cette fonction va retirer tous les classements personnalisés. Voulez-vous continuer?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Toutes les applications"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Applications multimédias"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Arrêter l\'application"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Détails de l\'application"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> a été arrêtée."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Voulez-vous arrêter l\'application?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Si vous forcez l\'arrêt d\'une application, son fonctionnement peut en être affecté."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Impossible d\'arrêter l\'application."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> ne peut pas être utilisée en conduisant."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Masquer les applications de débogage"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Afficher les applications de débogage"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-fr/strings.xml b/libs/appgrid/lib/res/values-fr/strings.xml
index 5a251ec..a24af5a 100644
--- a/libs/appgrid/lib/res/values-fr/strings.xml
+++ b/libs/appgrid/lib/res/values-fr/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Cette fonction supprimera tous les tris personnalisés. Voulez-vous continuer ?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Toutes les applications"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Applications multimédias"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Arrêter l\'appli"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Informations d\'applications"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"L\'appli <xliff:g id="APP_NAME">%1$s</xliff:g> a été arrêtée."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Arrêter l\'appli ?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"L\'arrêt forcé d\'une appli peut provoquer un fonctionnement instable."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Impossible d\'arrêter l\'appli."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Impossible d\'utiliser <xliff:g id="APP_NAME">%1$s</xliff:g> en conduisant."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Masquer les applis de débogage"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Afficher les applis de débogage"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-gl/strings.xml b/libs/appgrid/lib/res/values-gl/strings.xml
index 29fb3e9..a9a821a 100644
--- a/libs/appgrid/lib/res/values-gl/strings.xml
+++ b/libs/appgrid/lib/res/values-gl/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Esta función quitará toda a orde personalizada. Queres continuar?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Todas as aplicacións"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Aplicacións multimedia"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Deter aplicación"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Información da aplicación"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"Detívose a aplicación <xliff:g id="APP_NAME">%1$s</xliff:g>."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Queres deter a aplicación?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Se forzas a parada dunha aplicación, é posible que non funcione correctamente."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Non se pode deter a aplicación."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Non se pode utilizar <xliff:g id="APP_NAME">%1$s</xliff:g> mentres se conduce."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Ocultar aplicacións de depuración"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Mostrar aplicacións de depuración"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-gu/strings.xml b/libs/appgrid/lib/res/values-gu/strings.xml
index db2961a..893e20b 100644
--- a/libs/appgrid/lib/res/values-gu/strings.xml
+++ b/libs/appgrid/lib/res/values-gu/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"આ સુવિધા, સેટ કરવામાં આવેલા બધા કસ્ટમ ક્રમ કાઢી નાખશે. શું તમે ચાલુ રાખવા માગો છો?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"બધી ઍપ"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"મીડિયા ઍપ"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"ઍપ બંધ કરો"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"ઍપની માહિતી"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> બંધ કરવામાં આવી છે."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"શું ઍપ બંધ કરીએ?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"જો તમે કોઈ ઍપને ફરજિયાત બંધ કરો, તો તે અયોગ્ય વર્તન કરી શકે છે."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"ઍપ બંધ કરી શકાતી નથી."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"ડ્રાઇવ કરતી વખતે <xliff:g id="APP_NAME">%1$s</xliff:g>નો ઉપયોગ કરી શકાતો નથી."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"ડિબગ ઍપ છુપાવો"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"ડિબગ ઍપ બતાવો"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-hi/strings.xml b/libs/appgrid/lib/res/values-hi/strings.xml
index 8c48d20..7cfe1dc 100644
--- a/libs/appgrid/lib/res/values-hi/strings.xml
+++ b/libs/appgrid/lib/res/values-hi/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"इस कार्रवाई से, पसंद के मुताबिक सेट किए सभी क्रम हटा दिए जाएंगे. क्या आपको यह कार्रवाई करनी है?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"सभी ऐप्लिकेशन"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"मीडिया ऐप्लिकेशन"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"ऐप्लिकेशन को बंद करें"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"ऐप्लिकेशन की जानकारी"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> को रोक दिया गया है."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"क्या आपको ऐप्लिकेशन रोकना है?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"अगर किसी ऐप्लिकेशन को ज़बरदस्ती रोका जाता है, तो हो सकता है कि वह ठीक तरह से काम न करें."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"ऐप्लिकेशन को बंद नहीं किया जा सका."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"गाड़ी चलाते समय <xliff:g id="APP_NAME">%1$s</xliff:g> ऐप्लिकेशन इस्तेमाल नहीं किया जा सकता."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"डीबग किए गए ऐप्लिकेशन छिपाएं"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"डीबग किए गए ऐप्लिकेशन दिखाएं"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-hr/strings.xml b/libs/appgrid/lib/res/values-hr/strings.xml
index 248d751..d040c32 100644
--- a/libs/appgrid/lib/res/values-hr/strings.xml
+++ b/libs/appgrid/lib/res/values-hr/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Ta će funkcija ukloniti sav prilagođeni poredak. Želite li nastaviti?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Sve aplikacije"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Aplikacije za medijske sadržaje"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Zaustavi aplikaciju"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Informacije o aplikaciji"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"Aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g> je zaustavljena."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Želite li zaustaviti aplikaciju?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Ako silom zaustavite aplikaciju, možda će se ponašati nepredviđeno."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Aplikacija se ne može zaustaviti."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g> ne može se koristiti tijekom vožnje."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Sakrij aplikacije za otklanjanje pogrešaka"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Prikaži aplikacije za otklanjanje pogrešaka"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-hu/strings.xml b/libs/appgrid/lib/res/values-hu/strings.xml
index 2295436..e048db7 100644
--- a/libs/appgrid/lib/res/values-hu/strings.xml
+++ b/libs/appgrid/lib/res/values-hu/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"A funkció bekapcsolásával minden egyéni elrendezést eltávolít. Biztosan folytatja?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Összes alkalmazás"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Médiaalkalmazások"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Alkalmazás leállítása"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Alkalmazásadatok"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> leállítva."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Leállítja az alkalmazást?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Ha egy alkalmazást leállásra kényszerít, lehetséges, hogy hibásan fog működni."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Az alkalmazás nem állítható le."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"A(z) <xliff:g id="APP_NAME">%1$s</xliff:g> nem használható vezetés közben."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Hibakereső alkalmazások elrejtése"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Hibakereső alkalmazások megjelenítése"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-hy/strings.xml b/libs/appgrid/lib/res/values-hy/strings.xml
index 2f1ed69..d7b1fbf 100644
--- a/libs/appgrid/lib/res/values-hy/strings.xml
+++ b/libs/appgrid/lib/res/values-hy/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Այս գործառույթը կհեռացնի հավելվածների հատուկ դասավորությունը։ Շարունակե՞լ։"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Բոլոր հավելվածները"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Մեդիա հավելվածներ"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Կանգնեցնել հավելվածը"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Հավելվածի մասին"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> հավելվածը կանգնեցվեց։"</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Կանգնեցնե՞լ հավելվածը"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Հավելվածի ստիպողական կանգնեցումը կարող է ազդել դրա աշխատանքի վրա։"</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Չհաջողվեց կանգնեցնել հավելվածի աշխատանքը։"</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> հավելվածը հնարավոր չէ օգտագործել վարելու ժամանակ։"</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Թաքցնել վրիպազերծման հավելվածները"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Ցույց տալ վրիպազերծման հավելվածները"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-in/strings.xml b/libs/appgrid/lib/res/values-in/strings.xml
index 885f891..0c75392 100644
--- a/libs/appgrid/lib/res/values-in/strings.xml
+++ b/libs/appgrid/lib/res/values-in/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Fungsi ini akan menghapus semua pengurutan kustom. Ingin melanjutkan?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Semua aplikasi"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Aplikasi media"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Hentikan aplikasi"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Info aplikasi"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> telah dihentikan."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Hentikan aplikasi?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Jika aplikasi dihentikan paksa, fungsinya mungkin akan terganggu."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Aplikasi tidak dapat dihentikan."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> tidak dapat digunakan saat mengemudi."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Sembunyikan aplikasi debug"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Tampilkan aplikasi debug"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-is/strings.xml b/libs/appgrid/lib/res/values-is/strings.xml
index 21819f9..037666a 100644
--- a/libs/appgrid/lib/res/values-is/strings.xml
+++ b/libs/appgrid/lib/res/values-is/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Þessi eiginleiki fjarlægir alla sérsniðna röðun. Viltu halda áfram?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Öll forrit"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Margmiðlunarforrit"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Stöðva forrit"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Upplýsingar um forrit"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> var stöðvað."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Stöðva forritið?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Ef þú þvingar lokun forrits gæti það látið illa."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Ekki er hægt að stöðva forrit."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Ekki er hægt að nota <xliff:g id="APP_NAME">%1$s</xliff:g> við akstur."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Fela villuleitarforrit"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Sýna villuleitarforrit"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-it/strings.xml b/libs/appgrid/lib/res/values-it/strings.xml
index 2ab37fe..f7be1fa 100644
--- a/libs/appgrid/lib/res/values-it/strings.xml
+++ b/libs/appgrid/lib/res/values-it/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Questa funzione rimuove tutti gli ordini personalizzati. Vuoi continuare?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Tutte le app"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"App multimediali"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Interrompi l\'app"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Informazioni app"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"L\'app <xliff:g id="APP_NAME">%1$s</xliff:g> è stata interrotta."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Interrompere l\'app?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Se forzi l\'interruzione di un\'app, questa potrebbe funzionare in modo anomalo."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Impossibile interrompere l\'app."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Non è possibile usare l\'app <xliff:g id="APP_NAME">%1$s</xliff:g> durante la guida."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Nascondi app di debug"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Mostra app di debug"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-iw/strings.xml b/libs/appgrid/lib/res/values-iw/strings.xml
index 4513b26..ecd1805 100644
--- a/libs/appgrid/lib/res/values-iw/strings.xml
+++ b/libs/appgrid/lib/res/values-iw/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"הפונקציה הזו תסיר את כל סידורי התצוגה שהותאמו אישית. רוצה להמשיך?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"כל האפליקציות"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"אפליקציות מדיה"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"עצירת האפליקציה"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"פרטי האפליקציה"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"עצרת את האפליקציה <xliff:g id="APP_NAME">%1$s</xliff:g>."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"לעצור את האפליקציה?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"אם סוגרים אפליקציה באופן ידני, יכול להיות שהיא לא תפעל כמו שצריך."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"לא ניתן לעצור את פעולת האפליקציה."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"לא ניתן להשתמש באפליקציה <xliff:g id="APP_NAME">%1$s</xliff:g> במהלך הנהיגה."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"הסתרת אפליקציות לניפוי באגים"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"הצגת אפליקציות לניפוי באגים"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-ja/strings.xml b/libs/appgrid/lib/res/values-ja/strings.xml
index fefa214..c34b301 100644
--- a/libs/appgrid/lib/res/values-ja/strings.xml
+++ b/libs/appgrid/lib/res/values-ja/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"この機能を実行すると、カスタムの並べ替えがすべて削除されます。続行しますか?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"すべてのアプリ"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"メディアアプリ"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"アプリを停止"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"アプリ情報"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> を停止しました。"</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"アプリを停止しますか?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"アプリを強制停止すると、アプリが正常に機能しないことがあります。"</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"アプリを停止できません。"</string>
- <string name="driving_toast_text" msgid="397905281933065053">"運転中は <xliff:g id="APP_NAME">%1$s</xliff:g> を使用できません。"</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"デバッグアプリを表示しない"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"デバッグアプリを表示する"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-ka/strings.xml b/libs/appgrid/lib/res/values-ka/strings.xml
index 4f7d5e7..6114bb8 100644
--- a/libs/appgrid/lib/res/values-ka/strings.xml
+++ b/libs/appgrid/lib/res/values-ka/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"ეს ფუნქცია წაშლის ყველა მორგებულ შეკვეთას. გსურთ გაგრძელება?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"ყველა აპი"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"მედია აპები"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"აპის შეჩერება"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"აპის ინფორმაცია"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> შეჩერებულია."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"გსურთ აპის შეჩერება?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"თუ აპს შეწყვეტას აიძულებთ, შესაძლოა, მან ცუდად განაგრძოს მუშაობა."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"აპის შეჩერება შეუძლებელია."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"მანქანის მართვისას <xliff:g id="APP_NAME">%1$s</xliff:g>-ს ვერ გამოიყენებთ."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"გამართვის აპების დამალვა"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"გამართვის აპების ჩვენება"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-kk/strings.xml b/libs/appgrid/lib/res/values-kk/strings.xml
index 24ff876..35616a0 100644
--- a/libs/appgrid/lib/res/values-kk/strings.xml
+++ b/libs/appgrid/lib/res/values-kk/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Бұл функция арнаулы ретті толығымен өшіреді. Жалғастырғыңыз келе ме?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Барлық қолданба"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Мультимедиа қолданбалары"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Қолданба жұмысын тоқтату"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Қолданба туралы ақпарат"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> жұмысы тоқтатылды."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Қолданба жұмысы тоқтатылсын ба?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Қолданбаны қолмен тоқтату оның жұмысына кері әсерін тигізуі мүмкін."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Қолданба жұмысын тоқтату мүмкін емес."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Көлік жүргізу кезінде <xliff:g id="APP_NAME">%1$s</xliff:g> қолданбасын пайдалануға болмайды."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Түзету қолданбаларын жасыру"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Түзету қолданбаларын көрсету"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-km/strings.xml b/libs/appgrid/lib/res/values-km/strings.xml
index 731e396..6599142 100644
--- a/libs/appgrid/lib/res/values-km/strings.xml
+++ b/libs/appgrid/lib/res/values-km/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"មុខងារនេះនឹងដកការបញ្ជាទិញផ្ទាល់ខ្លួនទាំងអស់ចេញ។ តើអ្នកចង់បន្តដែរឬទេ?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"កម្មវិធីទាំងអស់"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"កម្មវិធីមេឌៀ"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"បញ្ឈប់កម្មវិធី"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"ព័ត៌មានកម្មវិធី"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> ត្រូវបានបញ្ឈប់។"</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"បញ្ឈប់កម្មវិធីឬ?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"ប្រសិនបើអ្នកបង្ខំឱ្យបញ្ឈប់កម្មវិធី កម្មវិធីអាចនឹងដំណើរការមិនត្រឹមត្រូវ។"</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"មិនអាចបញ្ឈប់កម្មវិធីបានទេ។"</string>
- <string name="driving_toast_text" msgid="397905281933065053">"មិនអាចប្រើ <xliff:g id="APP_NAME">%1$s</xliff:g> នៅពេលបើកបរបានទេ។"</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"លាក់កម្មវិធីជួសជុល"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"បង្ហាញកម្មវិធីជួសជុល"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-kn/strings.xml b/libs/appgrid/lib/res/values-kn/strings.xml
index ecd5344..5b0c65f 100644
--- a/libs/appgrid/lib/res/values-kn/strings.xml
+++ b/libs/appgrid/lib/res/values-kn/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"ಈ ಕಾರ್ಯವು ಎಲ್ಲಾ ಕಸ್ಟಮ್ ಆರ್ಡರ್ ಮಾಡುವಿಕೆಯನ್ನು ತೆಗೆದುಹಾಕುತ್ತದೆ. ನೀವು ಮುಂದುವರಿಸಲು ಬಯಸುತ್ತೀರಾ?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"ಎಲ್ಲಾ ಆ್ಯಪ್ಗಳು"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"ಮಾಧ್ಯಮ ಆ್ಯಪ್ಗಳು"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"ಆ್ಯಪ್ ಅನ್ನು ನಿಲ್ಲಿಸಿ"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"ಆ್ಯಪ್ ಮಾಹಿತಿ"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> ಅನ್ನು ನಿಲ್ಲಿಸಲಾಗಿದೆ."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"ಆ್ಯಪ್ ಅನ್ನು ನಿಲ್ಲಿಸಬೇಕೆ?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"ನೀವು ಆ್ಯಪ್ ಅನ್ನು ಬಲವಂತವಾಗಿ ನಿಲ್ಲಿಸಿದರೆ, ಅದು ಸರಿಯಾಗಿ ಕಾರ್ಯನಿರ್ವಹಿಸದಿರಬಹುದು."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"ಆ್ಯಪ್ ನಿಲ್ಲಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"ಡ್ರೈವ್ ಮಾಡುವಾಗ <xliff:g id="APP_NAME">%1$s</xliff:g> ಬಳಸಲು ಸಾಧ್ಯವಾಗುವುದಿಲ್ಲ."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"ಡೀಬಗ್ ಆ್ಯಪ್ಗಳನ್ನು ಮರೆಮಾಡಿ"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"ಡೀಬಗ್ ಆ್ಯಪ್ಗಳನ್ನು ತೋರಿಸಿ"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-ko/strings.xml b/libs/appgrid/lib/res/values-ko/strings.xml
index 3f417fe..846e8f2 100644
--- a/libs/appgrid/lib/res/values-ko/strings.xml
+++ b/libs/appgrid/lib/res/values-ko/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"이 기능은 모든 맞춤설정 정렬을 삭제합니다. 계속하시겠습니까?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"모든 앱"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"미디어 앱"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"앱 종료"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"앱 정보"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> 앱이 종료되었습니다."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"앱을 종료하시겠습니까?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"강제로 앱을 종료하면 예기치 않은 오류가 발생할 수 있습니다."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"앱을 닫을 수 없습니다."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"운전 중에는 <xliff:g id="APP_NAME">%1$s</xliff:g> 앱을 사용할 수 없습니다."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"디버그 앱 숨기기"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"디버그 앱 표시"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=true;end"</string>
diff --git a/libs/appgrid/lib/res/values-ky/strings.xml b/libs/appgrid/lib/res/values-ky/strings.xml
index a506c7c..37b02b0 100644
--- a/libs/appgrid/lib/res/values-ky/strings.xml
+++ b/libs/appgrid/lib/res/values-ky/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Бул функция бардык ыңгайлаштырылган иреттерди өчүрөт. Улантасызбы?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Бардык колдонмолор"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Мультимедиа колдонмолору"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Колдонмону токтотуу"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Колдонмо тууралуу"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> токтотулду."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Колдонмону токтотосузбу?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Колдонмону мажбурлап токтотсоңуз, ал туура эмес иштеп калышы мүмкүн."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Колдонмону токтотууга болбойт."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Айдап баратканда <xliff:g id="APP_NAME">%1$s</xliff:g> колдонулбайт."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Мүчүлүштүктөрдү оңдоочу колдонмолорду жашыруу"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Мүчүлүштүктөрдү оңдоочу колдонмолорду көрсөтүү"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-lo/strings.xml b/libs/appgrid/lib/res/values-lo/strings.xml
index 738a165..fe98eae 100644
--- a/libs/appgrid/lib/res/values-lo/strings.xml
+++ b/libs/appgrid/lib/res/values-lo/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"ຟັງຊັນນີ້ຈະລຶບການຈັດລຳດັບທີ່ກຳນົດເອງອອກທັງໝົດ. ທ່ານຕ້ອງການສືບຕໍ່ບໍ?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"ແອັບທັງໝົດ"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"ແອັບມີເດຍ"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"ຢຸດແອັບ"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"ຂໍ້ມູນແອັບ"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"ຢຸດ <xliff:g id="APP_NAME">%1$s</xliff:g> ແລ້ວ."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"ຢຸດແອັບໄວ້ບໍ?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"ຫານທ່ານບັງຄັບປິດແອັບໃດໜຶ່ງ, ມັນອາດເຮັດວຽກຜິດປົກກະຕິໄດ້."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"ບໍ່ສາມາດຢຸດແອັບໄດ້."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"ບໍ່ສາມາດໃຊ້ <xliff:g id="APP_NAME">%1$s</xliff:g> ໃນຂະນະທີ່ຂັບລົດໄດ້."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"ເຊື່ອງແອັບດີບັກ"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"ສະແດງແອັບດີບັກ"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-lt/strings.xml b/libs/appgrid/lib/res/values-lt/strings.xml
index ca94d0e..12ddee8 100644
--- a/libs/appgrid/lib/res/values-lt/strings.xml
+++ b/libs/appgrid/lib/res/values-lt/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Ši funkcija pašalins visą tinkintą tvarką. Ar norite tęsti?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Visos programos"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Medijos programos"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Sustabdyti programą"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Programos informacija"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"Programa „<xliff:g id="APP_NAME">%1$s</xliff:g>“ sustabdyta."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Sustabdyti programą?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Jei priverstinai sustabdysite programą, ji gali neveikti tinkamai."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Programos negalima sustabdyti."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Vairuojant negalima naudoti „<xliff:g id="APP_NAME">%1$s</xliff:g>“."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Slėpti derinimo programas"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Rodyti derinimo programas"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-lv/strings.xml b/libs/appgrid/lib/res/values-lv/strings.xml
index 0ba342d..93fdbda 100644
--- a/libs/appgrid/lib/res/values-lv/strings.xml
+++ b/libs/appgrid/lib/res/values-lv/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Ar šo funkciju tiks mainīta pielāgotā lietotņu secība. Vai vēlaties turpināt?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Visas lietotnes"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Multivides lietotnes"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Apturēt lietotnes darbību"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Informācija par lietotni"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"Lietotnes <xliff:g id="APP_NAME">%1$s</xliff:g> darbība ir apturēta."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Vai apturēt lietotnes darbību?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Piespiedu kārtā apturot lietotnes darbību, var rasties šīs lietotnes darbības traucējumi."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Lietotnes darbību nevar apturēt."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Lietotni <xliff:g id="APP_NAME">%1$s</xliff:g> nevar izmantot braukšanas laikā."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Slēpt atkļūdošanas lietotnes"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Rādīt atkļūdošanas lietotnes"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-mk/strings.xml b/libs/appgrid/lib/res/values-mk/strings.xml
index 1f204c1..dee31cb 100644
--- a/libs/appgrid/lib/res/values-mk/strings.xml
+++ b/libs/appgrid/lib/res/values-mk/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Оваа функција ќе ги отстрани сите приспособени подредувања. Дали сакате да продолжите?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Сите апликации"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Апликации за аудиовизуелни содржини"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Сопри ја апликацијата"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Информации за апликацијата"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> е сопрена."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Да се сопре апликацијата?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Ако сопрете апликација присилно, таа може да не се однесува правилно."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Апликацијата не може да се сопре."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> не може да се користи при возење."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Скриј апликации за отстранување грешки"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Прикажи апликации за отстранување грешки"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-ml/strings.xml b/libs/appgrid/lib/res/values-ml/strings.xml
index 868d31c..e1bfd41 100644
--- a/libs/appgrid/lib/res/values-ml/strings.xml
+++ b/libs/appgrid/lib/res/values-ml/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"ഈ ഫംഗ്ഷൻ, എല്ലാ ഇഷ്ടാനുസൃത ക്രമപ്പെടുത്തലുകളും നീക്കം ചെയ്യും. നിങ്ങൾക്ക് തുടരണമെന്നുണ്ടോ?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"എല്ലാ ആപ്പുകളും"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"മീഡിയ ആപ്പുകൾ"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"ആപ്പിന്റെ പ്രവർത്തനം നിർത്തുക"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"ആപ്പ് വിവരങ്ങൾ"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> എന്നതിന്റെ പ്രവർത്തനം നിർത്തി."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"ആപ്പിന്റെ പ്രവർത്തനം നിർത്തണോ?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"ഒരു ആപ്പിന്റെ പ്രവർത്തനം നിർബന്ധിതമായി നിർത്തിയാൽ, അത് ശരിയായി പ്രവർത്തിക്കാനിടയില്ല."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"ആപ്പ് നിർത്താനാകില്ല."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"ഡ്രൈവിംഗിനിടെ <xliff:g id="APP_NAME">%1$s</xliff:g> ഉപയോഗിക്കാനാകില്ല."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"ഡീബഗ് ആപ്പുകൾ മറയ്ക്കുക"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"ഡീബഗ് ആപ്പുകൾ കാണിക്കുക"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-mn/strings.xml b/libs/appgrid/lib/res/values-mn/strings.xml
index 476d009..6fe8f51 100644
--- a/libs/appgrid/lib/res/values-mn/strings.xml
+++ b/libs/appgrid/lib/res/values-mn/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Энэ функц нь бүх захиалгат дарааллыг хасна. Та үргэлжлүүлэхийг хүсэж байна уу?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Бүх апп"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Медиа аппууд"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Аппыг зогсоох"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Аппын мэдээлэл"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g>-г зогсоосон."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Аппыг зогсоох уу?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Хэрэв та аппыг хүчээр зогсоовол энэ нь буруу ажиллаж магадгүй."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Аппыг зогсоох боломжгүй."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g>-г жолоо барьж байх үед ашиглах боломжгүй."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Дебаг хийх аппуудыг нуух"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Дебаг хийх аппуудыг харуулах"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-mr/strings.xml b/libs/appgrid/lib/res/values-mr/strings.xml
index c4b830f..f28ff673 100644
--- a/libs/appgrid/lib/res/values-mr/strings.xml
+++ b/libs/appgrid/lib/res/values-mr/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"हे फंक्शन सर्व कस्टम ऑर्डरिंग काढून टाकेल. तुम्हाला पुढे सुरू ठेवायचे आहे का?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"सर्व अॅप्स"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"मीडिया अॅप्स"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"अॅप थांबवा"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"ॲप माहिती"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> थांबवले आहे."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"अॅप थांबवायचे आहे का?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"तुम्ही अॅप सक्तीने थांबवल्यास, ते गैरवर्तन करू शकते."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"अॅप थांबवू शकत नाही."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"ड्राइव्ह करताना <xliff:g id="APP_NAME">%1$s</xliff:g> वापरता येऊ शकत नाही."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"डीबग केलेली ॲप्स लपवा"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"डीबग केलेली ॲप्स दाखवा"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-ms/strings.xml b/libs/appgrid/lib/res/values-ms/strings.xml
index 161c68d..606187f 100644
--- a/libs/appgrid/lib/res/values-ms/strings.xml
+++ b/libs/appgrid/lib/res/values-ms/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Fungsi ini akan mengalih keluar semua susunan tersuai. Adakah anda mahu meneruskan tindakan?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Semua apl"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Apl media"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Hentikan apl"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Maklumat apl"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> telah dihentikan."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Hentikan apl?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Jika anda menghentikan apl secara paksa, fungsi apl mungkin terganggu."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Apl tidak dapat dihentikan."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> tidak boleh digunakan semasa memandu."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Sembunyikan apl nyahpepijat"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Tunjukkan apl nyahpepijat"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=true;end"</string>
diff --git a/libs/appgrid/lib/res/values-my/strings.xml b/libs/appgrid/lib/res/values-my/strings.xml
index 3bdcac7..9868099 100644
--- a/libs/appgrid/lib/res/values-my/strings.xml
+++ b/libs/appgrid/lib/res/values-my/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"ဤလုပ်ဆောင်ချက်သည် စိတ်ကြိုက်စီစဉ်မှုအားလုံးကို ဖယ်ရှားမည်။ ရှေ့ဆက်လိုပါသလား။"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"အက်ပ်အားလုံး"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"မီဒီယာ အက်ပ်များ"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"အက်ပ် ရပ်ရန်"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"အက်ပ်အချက်အလက်"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> ရပ်သွားသည်။"</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"အက်ပ်ကို ရပ်မလား။"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"အက်ပ်ကို မဖြစ်မနေ ရပ်ခိုင်းလျှင် အမှားဖြစ်နိုင်သည်။"</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"အက်ပ်ကို ရပ်၍မရပါ။"</string>
- <string name="driving_toast_text" msgid="397905281933065053">"ကားမောင်းနေစဉ် <xliff:g id="APP_NAME">%1$s</xliff:g> ကို သုံး၍မရပါ။"</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"အမှားရှာပြင်သည့်အက်ပ်များ ဖျောက်ထားရန်"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"အမှားရှာပြင်သည့်အက်ပ်များ ပြပါ"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-nb/strings.xml b/libs/appgrid/lib/res/values-nb/strings.xml
index 65159b6..ce3165e 100644
--- a/libs/appgrid/lib/res/values-nb/strings.xml
+++ b/libs/appgrid/lib/res/values-nb/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Denne funksjonen fjerner alle tilpassede rekkefølger. Vil du fortsette?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Alle apper"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Medieapper"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Stopp appen"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Appinformasjon"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> er stoppet."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Vil du stoppe appen?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Hvis du tvinger en app til å stoppe, kan det oppstå problemer."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Appen kan ikke stoppes."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Du kan ikke bruke <xliff:g id="APP_NAME">%1$s</xliff:g> mens du kjører."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Skjul feilsøkingsapper"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Vis feilsøkingsapper"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-ne/strings.xml b/libs/appgrid/lib/res/values-ne/strings.xml
index 7aae9b0..0d2ffb6 100644
--- a/libs/appgrid/lib/res/values-ne/strings.xml
+++ b/libs/appgrid/lib/res/values-ne/strings.xml
@@ -21,15 +21,9 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"यो फङ्सनले रोजाइअनुसार सेट गरिएका सबै क्रम हटाउने छ। तपाईं जारी राख्न चाहनुहुन्छ?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"सबै एप"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"मिडिया एपहरू"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"एप बन्द गर्नुहोस्"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"एपसम्बन्धी जानकारी"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> बन्द गरिएको छ।"</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"एप बन्द गर्ने हो?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"तपाईंले कुनै एपलाई जबरजस्ती रोक्नुभयो भने त्यसले सही तरिकाले काम नगर्न सक्छ।"</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"एप बन्द गर्न सकिँदैन।"</string>
- <string name="driving_toast_text" msgid="397905281933065053">"ड्राइभ गर्दा <xliff:g id="APP_NAME">%1$s</xliff:g> प्रयोग गर्न सकिँदैन।"</string>
- <string name="hide_debug_apps" msgid="7140064693464751647">"डिबग एपहरू लुकाइयोस्"</string>
- <string name="show_debug_apps" msgid="2748157232151197494">"डिबग एपहरू देखाइयोस्"</string>
+ <string name="hide_debug_apps" msgid="7140064693464751647">"डिबग एपहरू लुकाउनुहोस्"</string>
+ <string name="show_debug_apps" msgid="2748157232151197494">"डिबग एपहरू देखाउनुहोस्"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
<string name="banner_title_text" msgid="8827498256184464356">"तपाईं प्रयोगकर्ताले सहमति जनाउनु पर्ने सेवाका सर्तहरूका कारण निष्क्रिय पारिएका एपहरू प्रयोग गर्न चाहनुहुन्छ भने प्रयोगकर्ताले सहमति जनाउनु पर्ने सेवाका सर्तहरूमा सहमति जनाउनुहोस्"</string>
<string name="banner_review_button_text" msgid="369410598918950148">"समीक्षा गर्नुहोस्"</string>
diff --git a/libs/appgrid/lib/res/values-nl/strings.xml b/libs/appgrid/lib/res/values-nl/strings.xml
index 3a2af95..f249441 100644
--- a/libs/appgrid/lib/res/values-nl/strings.xml
+++ b/libs/appgrid/lib/res/values-nl/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Met deze functie wordt de aangepaste volgorde helemaal verwijderd. Wil je doorgaan?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Alle apps"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Media-apps"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"App stoppen"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"App-informatie"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> is gestopt."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"App stoppen?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Als je een app gedwongen stopt, kan deze onverwacht gedrag vertonen."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"De app kan niet worden gestopt."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Je kunt <xliff:g id="APP_NAME">%1$s</xliff:g> niet gebruiken tijdens het rijden"</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Foutopsporingsapps verbergen"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Foutopsporingsapps tonen"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-or/strings.xml b/libs/appgrid/lib/res/values-or/strings.xml
index eda5a9a..326df07 100644
--- a/libs/appgrid/lib/res/values-or/strings.xml
+++ b/libs/appgrid/lib/res/values-or/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"ଏହି ଫଙ୍କସନ ସମସ୍ତ କଷ୍ଟମ ଅର୍ଡରିଂକୁ କାଢ଼ି ଦେବ। ଆପଣ ଜାରି ରଖିବାକୁ ଚାହାଁନ୍ତି?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"ସମସ୍ତ ଆପ୍ସ"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"ମିଡିଆ ଆପ୍ସ"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"ଆପକୁ ବନ୍ଦ କରନ୍ତୁ"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"ଆପ ସୂଚନା"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g>କୁ ବନ୍ଦ କରାଯାଇଛି।"</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"ଆପକୁ ବନ୍ଦ କରିବେ?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"ଯଦି ଆପଣ ଏକ ଆପକୁ ବାଧ୍ୟତାର ସହ ବନ୍ଦ କରନ୍ତି, ତେବେ ଏହା ଠିକ୍ ଭାବେ କାମ ନକରିପାରେ।"</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"ଆପ ବନ୍ଦ କରାଯାଇପାରିବ ନାହିଁ।"</string>
- <string name="driving_toast_text" msgid="397905281933065053">"ଡ୍ରାଇଭ୍ କରିବା ସମୟରେ <xliff:g id="APP_NAME">%1$s</xliff:g> ବ୍ୟବହାର କରାଯାଇପାରିବ ନାହିଁ।"</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"ଡିବଗ ଆପ୍ସ ଲୁଚାନ୍ତୁ"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"ଡିବଗ ଆପ୍ସ ଦେଖାନ୍ତୁ"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-pa/strings.xml b/libs/appgrid/lib/res/values-pa/strings.xml
index b78886a..5eded97 100644
--- a/libs/appgrid/lib/res/values-pa/strings.xml
+++ b/libs/appgrid/lib/res/values-pa/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"ਇਸ ਫੰਕਸ਼ਨ ਨਾਲ ਸਾਰਾ ਵਿਉਂਤਿਆ ਕ੍ਰਮ ਹਟ ਜਾਵੇਗਾ। ਕੀ ਤੁਸੀਂ ਜਾਰੀ ਰੱਖਣਾ ਚਾਹੁੰਦੇ ਹੋ?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"ਸਾਰੀਆਂ ਐਪਾਂ"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"ਮੀਡੀਆ ਐਪਾਂ"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"ਐਪ ਬੰਦ ਕਰੋ"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"ਐਪ ਜਾਣਕਾਰੀ"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> ਨੂੰ ਬੰਦ ਕਰ ਦਿੱਤਾ ਗਿਆ ਹੈ।"</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"ਕੀ ਐਪ ਨੂੰ ਬੰਦ ਕਰਨਾ ਹੈ?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"ਜੇ ਤੁਸੀਂ ਕਿਸੇ ਐਪ ਨੂੰ ਜ਼ਬਰਦਸਤੀ ਬੰਦ ਕਰਦੇ ਹੋ, ਤਾਂ ਹੋ ਸਕਦਾ ਹੈ ਇਹ ਠੀਕ ਤਰ੍ਹਾਂ ਕੰਮ ਨਾ ਕਰੇ।"</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"ਐਪ ਨੂੰ ਬੰਦ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ।"</string>
- <string name="driving_toast_text" msgid="397905281933065053">"ਗੱਡੀ ਚਲਾਉਣ ਵੇਲੇ <xliff:g id="APP_NAME">%1$s</xliff:g> ਨੂੰ ਵਰਤਿਆ ਨਹੀਂ ਜਾ ਸਕਦਾ।"</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"ਡੀਬੱਗ ਐਪਾਂ ਲੁਕਾਓ"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"ਡੀਬੱਗ ਐਪਾਂ ਦਿਖਾਓ"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-pl/strings.xml b/libs/appgrid/lib/res/values-pl/strings.xml
index 1e8e257..db03284 100644
--- a/libs/appgrid/lib/res/values-pl/strings.xml
+++ b/libs/appgrid/lib/res/values-pl/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Ta funkcja usuwa wszystkie niestandardowe ustawienia kolejności. Czy chcesz kontynuować?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Wszystkie aplikacje"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Aplikacje multimedialne"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Zatrzymaj aplikację"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Informacje o aplikacji"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"Aplikacja <xliff:g id="APP_NAME">%1$s</xliff:g> została zatrzymana."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Zatrzymać aplikację?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Jeśli wymusisz zatrzymanie aplikacji, może ona zadziałać nieprawidłowo."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Aplikacji nie można zatrzymać."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Podczas jazdy nie można korzystać z aplikacji <xliff:g id="APP_NAME">%1$s</xliff:g>."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Ukryj aplikacje do debugowania"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Pokaż aplikacje do debugowania"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-pt-rPT/strings.xml b/libs/appgrid/lib/res/values-pt-rPT/strings.xml
index f310ce9..db6dadc 100644
--- a/libs/appgrid/lib/res/values-pt-rPT/strings.xml
+++ b/libs/appgrid/lib/res/values-pt-rPT/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Esta função vai remover toda a ordenação personalizada. Quer continuar?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Todas as apps"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Apps de multimédia"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Parar app"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Informações da app"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"A app <xliff:g id="APP_NAME">%1$s</xliff:g> foi parada."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Parar a app?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Se forçar a paragem de uma app, esta pode apresentar um comportamento anormal."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Não é possível parar a app."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Não é possível usar a app <xliff:g id="APP_NAME">%1$s</xliff:g> durante a condução."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Ocultar apps de depuração"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Mostrar apps de depuração"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-pt/strings.xml b/libs/appgrid/lib/res/values-pt/strings.xml
index 052dc2e..5cd5927 100644
--- a/libs/appgrid/lib/res/values-pt/strings.xml
+++ b/libs/appgrid/lib/res/values-pt/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Esta função vai remover a ordem personalizada. Você quer continuar?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Todos os apps"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Apps de música"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Parar o app"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Informações do app"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"O app <xliff:g id="APP_NAME">%1$s</xliff:g> foi parado."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Parar o app?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Se você forçar o fechamento de um app, ele pode apresentar mau funcionamento."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Não é possível interromper o app."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"O app <xliff:g id="APP_NAME">%1$s</xliff:g> não pode ser usado ao dirigir."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Ocultar apps de depuração"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Mostrar apps de depuração"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-ro/strings.xml b/libs/appgrid/lib/res/values-ro/strings.xml
index 0d9aa1b..c3f2e44 100644
--- a/libs/appgrid/lib/res/values-ro/strings.xml
+++ b/libs/appgrid/lib/res/values-ro/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Această funcție va elimina orice ordonare personalizată. Continui?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Toate aplicațiile"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Aplicații media"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Oprește aplicația"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Informații despre aplicație"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> a fost oprită."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Oprești aplicația?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Dacă forțezi oprirea unei aplicații, aceasta se poate comporta necorespunzător."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Aplicația nu poate fi oprită."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Nu poți folosi <xliff:g id="APP_NAME">%1$s</xliff:g> în timp ce conduci."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Ascunde aplicațiile de remediere a erorilor"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Afișează aplicațiile de remediere a erorilor"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-ru/strings.xml b/libs/appgrid/lib/res/values-ru/strings.xml
index fdcee77..18282f5 100644
--- a/libs/appgrid/lib/res/values-ru/strings.xml
+++ b/libs/appgrid/lib/res/values-ru/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Текущий порядок приложений будет изменен на алфавитный. Продолжить?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Все приложения"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Мультимедийные приложения"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Остановить"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Сведения о приложении"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"Приложение \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" остановлено."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Остановить приложение?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Принудительное закрытие приложения может отразиться на его функциональности."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Невозможно остановить приложение."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Приложение \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" нельзя использовать при вождении."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Скрыть приложения для отладки"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Показать приложения для отладки"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-si/strings.xml b/libs/appgrid/lib/res/values-si/strings.xml
index 20051df..bc281c7 100644
--- a/libs/appgrid/lib/res/values-si/strings.xml
+++ b/libs/appgrid/lib/res/values-si/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"මෙම කාර්යය සියලු අභිරුචි අනුපිළිවෙලක් ඉවත් කරනු ඇත. ඔබට ඉදිරියට යාමට අවශ්යද?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"සියලු යෙදුම්"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"මාධ්ය යෙදුම්"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"යෙදුම නවත්වන්න"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"යෙදුම් තතු"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> නතර කර ඇත."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"යෙදුම නවත්වන්න ද?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"ඔබ යෙදුමක් බලෙන් නැවත වුවහොත්, එය වැරදි ලෙස ක්රියා කරනු ඇත."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"යෙදුම නැවැත්විය නොහැක."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> හට රිය පදවන අතරේ යතුරු පුවරුව භාවිතා කළ නොහැක."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"නිදොස් කිරීමේ යෙදුම් සඟවන්න"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"නිදොස් කිරීමේ යෙදුම් පෙන්වන්න"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-sk/strings.xml b/libs/appgrid/lib/res/values-sk/strings.xml
index 6438114..4c03800 100644
--- a/libs/appgrid/lib/res/values-sk/strings.xml
+++ b/libs/appgrid/lib/res/values-sk/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Táto funkcia zruší všetko vlastné poradie. Chcete pokračovať?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Všetky aplikácie"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Prehrávače"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Zastaviť aplikáciu"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Informácie o aplikácii"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"Aplikácia <xliff:g id="APP_NAME">%1$s</xliff:g> bola zastavená."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Chcete zastaviť aplikáciu?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Ak vynútite zastavenie aplikácie, môže sa správať zvláštne."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Aplikáciu nie je možné ukončiť."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Pri šoférovaní nie je možné používať aplikáciu <xliff:g id="APP_NAME">%1$s</xliff:g>."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Skryť aplikácie na ladenie"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Zobraziť aplikácie na ladenie"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-sl/strings.xml b/libs/appgrid/lib/res/values-sl/strings.xml
index ea80a02..2ecf767 100644
--- a/libs/appgrid/lib/res/values-sl/strings.xml
+++ b/libs/appgrid/lib/res/values-sl/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Ta funkcija bo odstranila vse razvrščanje po meri. Ali želite nadaljevati?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Vse aplikacije"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Aplikacije za predstavnost"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Ustavi aplikacijo"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Podatki o aplikacijah"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"Aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g> je ustavljena."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Želite ustaviti aplikacijo?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Če boste vsilili zaustavitev aplikacije, morda ne bo pravilno delovala."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Aplikacije ni mogoče ustaviti."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Med vožnjo ni mogoče uporabljati aplikacije <xliff:g id="APP_NAME">%1$s</xliff:g>."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Skrij aplikacije za odpravljanje napak"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Prikaži aplikacije za odpravljanje napak"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-sq/strings.xml b/libs/appgrid/lib/res/values-sq/strings.xml
index 263be17..0cf2bec 100644
--- a/libs/appgrid/lib/res/values-sq/strings.xml
+++ b/libs/appgrid/lib/res/values-sq/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Ky funksion do të heqë të gjitha renditjet e personalizuara. Dëshiron të vazhdosh?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Të gjitha aplikacionet"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Aplikacionet e medias"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Ndalo aplikacionin"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Informacione mbi aplikacionin"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> është ndaluar."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Të ndalohet aplikacioni?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Nëse e ndalon me forcë një aplikacion, ai mund të ketë çrregullime në funksionim."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Aplikacioni nuk mund të ndalohet."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"\"<xliff:g id="APP_NAME">%1$s</xliff:g>\" nuk mund të përdoret gjatë drejtimit të makinës."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Fshih aplikacionet e korrigjimit të defekteve"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Shfaq aplikacionet e korrigjimit të defekteve"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-sr/strings.xml b/libs/appgrid/lib/res/values-sr/strings.xml
index 0f692f2..3d51943 100644
--- a/libs/appgrid/lib/res/values-sr/strings.xml
+++ b/libs/appgrid/lib/res/values-sr/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Ова функција ће уклонити целокупан прилагођен распоред. Желите да наставите?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Све апликације"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Медијске апликације"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Заустави апликацију"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Информације о апликацији"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"Апликација <xliff:g id="APP_NAME">%1$s</xliff:g> је заустављена."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Желите да зауставите апликацију?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Ако принудно зауставите апликацију, можда ће се понашати неочекивано."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Апликација не може да се заустави."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> не може да се користи током вожње."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Сакриј апликације за отклањање грешака"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Прикажи апликације за отклањање грешака"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-sv/strings.xml b/libs/appgrid/lib/res/values-sv/strings.xml
index 0ef43bc..016f3ce 100644
--- a/libs/appgrid/lib/res/values-sv/strings.xml
+++ b/libs/appgrid/lib/res/values-sv/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Funktionen tar bort alla anpassade ordningar. Vill du fortsätta?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Alla appar"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Medieappar"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Avsluta appen"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Appinformation"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> har avslutats."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Vill du avsluta appen?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Om du tvingar appen att avsluta kanske den inte fungerar som den ska."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Det går inte att avsluta appen."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g> går inte att använda under körning."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Dölj felsökningsappar"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Visa felsökningsappar"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-sw/strings.xml b/libs/appgrid/lib/res/values-sw/strings.xml
index 66fb8d3..9650d1d 100644
--- a/libs/appgrid/lib/res/values-sw/strings.xml
+++ b/libs/appgrid/lib/res/values-sw/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Utendaji huu utaondoa mipangilio yote maalum ya upangaji. Je, ungependa kuendelea?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Programu zote"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Programu za muziki"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Zima programu"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Maelezo ya programu"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"Umezima programu ya <xliff:g id="APP_NAME">%1$s</xliff:g>."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Je, ungependa kuzima programu?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Huenda programu isifanye kazi ipasavyo, iwapo utalazimisha kuizima."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Programu haiwezi kuzimwa."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Huwezi kutumia <xliff:g id="APP_NAME">%1$s</xliff:g> wakati unaendesha gari."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Ficha programu za kutatua hitilafu"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Onyesha programu za kutatua hitilafu"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-ta/strings.xml b/libs/appgrid/lib/res/values-ta/strings.xml
index e7a6698..8374a00 100644
--- a/libs/appgrid/lib/res/values-ta/strings.xml
+++ b/libs/appgrid/lib/res/values-ta/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"இது பிரத்தியேக வரிசைகள் அனைத்தையும் அகற்றும். தொடர விரும்புகிறீர்களா?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"அனைத்து ஆப்ஸும்"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"மீடியா ஆப்ஸ்"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"ஆப்ஸை நிறுத்து"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"ஆப்ஸ் தகவல்கள்"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> ஆப்ஸ் நிறுத்தப்பட்டது."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"ஆப்ஸை நிறுத்தவா?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"ஆப்ஸை உடனே நிறுத்தினால் அது சரியாகச் செயல்படாமல் போகக்கூடும்."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"ஆப்ஸை நிறுத்த முடியாது."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"காரை ஓட்டும்போது <xliff:g id="APP_NAME">%1$s</xliff:g> ஆப்ஸைப் பயன்படுத்த முடியாது."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"பிழைதிருத்தும் ஆப்ஸை மறை"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"பிழைதிருத்தும் ஆப்ஸைக் காட்டு"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-te/strings.xml b/libs/appgrid/lib/res/values-te/strings.xml
index a7f46d2..346ca68 100644
--- a/libs/appgrid/lib/res/values-te/strings.xml
+++ b/libs/appgrid/lib/res/values-te/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"ఈ ఫంక్షన్ అన్ని అనుకూల ఆర్డరింగ్ను తీసివేస్తుంది. మీరు కొనసాగాలనుకుంటున్నారా?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"అన్ని యాప్లు"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"మీడియా యాప్లు"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"యాప్ను ఆపివేయండి"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"యాప్ సమాచారం"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> ఆపివేయబడింది."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"యాప్ను ఆపివేయాలా?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"మీరు యాప్ను ఫోర్స్ ఆపివేస్తే, అది సరిగ్గా పని చేయకపోవచ్చు."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"యాప్ను ఆపడం సాధ్యం కాదు."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"డ్రైవింగ్ చేస్తున్నపుడు <xliff:g id="APP_NAME">%1$s</xliff:g>ను ఉపయోగించలేరు."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"డీబగ్ యాప్లను దాచండి"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"డీబగ్ యాప్లను చూపించండి"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-th/strings.xml b/libs/appgrid/lib/res/values-th/strings.xml
index 5a30b02..98dcdd9 100644
--- a/libs/appgrid/lib/res/values-th/strings.xml
+++ b/libs/appgrid/lib/res/values-th/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"ฟังก์ชันนี้จะนำการจัดลำดับที่กำหนดเองออกทั้งหมด ต้องการทำต่อไหม"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"แอปทั้งหมด"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"แอปสื่อ"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"หยุดแอป"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"ข้อมูลแอป"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"หยุด <xliff:g id="APP_NAME">%1$s</xliff:g> แล้ว"</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"หยุดแอปไหม"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"แอปอาจทำงานผิดพลาดหากบังคับให้หยุด"</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"หยุดแอปไม่ได้"</string>
- <string name="driving_toast_text" msgid="397905281933065053">"ใช้ <xliff:g id="APP_NAME">%1$s</xliff:g> ขณะขับรถไม่ได้"</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"ซ่อนแอปแก้ไขข้อบกพร่อง"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"แสดงแอปแก้ไขข้อบกพร่อง"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-tl/strings.xml b/libs/appgrid/lib/res/values-tl/strings.xml
index c23a923..623a521 100644
--- a/libs/appgrid/lib/res/values-tl/strings.xml
+++ b/libs/appgrid/lib/res/values-tl/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Aalisin ng function na ito ang lahat ng custom na pagkakasunod-sunod. Gusto mo bang magpatuloy?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Lahat ng app"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Mga media app"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Ihinto ang app"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Impormasyon ng app"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"Inihinto ang <xliff:g id="APP_NAME">%1$s</xliff:g>."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Ihinto ang app?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Kung sapilitan mong ihihinto ang isang app, posible itong gumana nang maayos."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Hindi puwedeng ihinto ang app."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Hindi magagamit ang <xliff:g id="APP_NAME">%1$s</xliff:g> habang nagmamaneho."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"I-hide ang mga debug app"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Ipakita ang mga debug app"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-tr/strings.xml b/libs/appgrid/lib/res/values-tr/strings.xml
index a8eae95..8a96385 100644
--- a/libs/appgrid/lib/res/values-tr/strings.xml
+++ b/libs/appgrid/lib/res/values-tr/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Bu işlev tüm özel sıralamayı kaldıracaktır. Devam etmek istiyor musunuz?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Tüm uygulamalar"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Medya uygulamaları"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Uygulamayı durdur"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Uygulama bilgisi"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> durduruldu."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Uygulama durdurulsun mu?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Uygulamayı zorla durdurursanız hatalı davranabilir."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Uygulama durdurulamıyor."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"<xliff:g id="APP_NAME">%1$s</xliff:g>, sürüş sırasında kullanılamaz."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Hata ayıklama uygulamalarını gizle"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Hata ayıklama uygulamalarını göster"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-uk/strings.xml b/libs/appgrid/lib/res/values-uk/strings.xml
index e35e55d..dec8afb 100644
--- a/libs/appgrid/lib/res/values-uk/strings.xml
+++ b/libs/appgrid/lib/res/values-uk/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Якщо застосувати цю функцію, визначений користувачем порядок розташування додатків зміниться. Продовжити?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Усі додатки"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Мультимедійні додатки"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Припинити роботу додатка"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Інформація про додаток"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"Роботу додатка <xliff:g id="APP_NAME">%1$s</xliff:g> припинено."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Припинити роботу додатка?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Примусове вимкнення додатка може призвести до збою в його роботі."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Не вдалося припинити роботу додатка."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Додатком <xliff:g id="APP_NAME">%1$s</xliff:g> не можна користуватися під час руху."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Сховати додатки для налагодження"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Показати додатки для налагодження"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-ur/strings.xml b/libs/appgrid/lib/res/values-ur/strings.xml
index 34191e2..3b65bc7 100644
--- a/libs/appgrid/lib/res/values-ur/strings.xml
+++ b/libs/appgrid/lib/res/values-ur/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"یہ فنکشن تمام حسب ضرورت ترتیب کو ہٹا دے گا۔ کیا آپ جاری رکھنا چاہتے ہیں؟"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"سبھی ایپس"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"میڈیا ایپس"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"ایپ بند کریں"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"ایپ کی معلومات"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> کو بند کر دیا گیا ہے۔"</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"ایپ بند کریں؟"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"اگر آپ کسی ایپ کو زبردستی بند کر دیتے ہیں تو یہ غلط برتاؤ کر سکتی ہے۔"</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"ایپ کو بند نہیں کیا جا سکتا۔"</string>
- <string name="driving_toast_text" msgid="397905281933065053">"ڈرائیو کرتے وقت <xliff:g id="APP_NAME">%1$s</xliff:g> کا استعمال نہیں کیا جا سکتا۔"</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"ڈیبگ ایپس چھپائیں"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"ڈیبگ ایپس دکھائیں"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-uz/strings.xml b/libs/appgrid/lib/res/values-uz/strings.xml
index 6441fce..26cbf5c 100644
--- a/libs/appgrid/lib/res/values-uz/strings.xml
+++ b/libs/appgrid/lib/res/values-uz/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Bu funksiya barcha maxsus buyurtmalarni bekor qiladi. Davom etasizmi?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Barcha ilovalar"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Media ilovalari"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Ilovani toʻxtatish"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Ilova haqida"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> ilovasi toʻxtatildi."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Ilova toʻxtatilsinmi?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Ilovani majburan toʻxtatish uning ishlashiga taʼsir koʻrsatishi mumkin."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Ilovani toʻxtatish imkonsiz."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Avtomobil rejimida <xliff:g id="APP_NAME">%1$s</xliff:g> ishlamaydi."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Nosozliklarni aniqlash ilovalarini berkitish"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Nosozliklarni aniqlash ilovalarini chiqarish"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-vi/strings.xml b/libs/appgrid/lib/res/values-vi/strings.xml
index fee8689..0c0f70f 100644
--- a/libs/appgrid/lib/res/values-vi/strings.xml
+++ b/libs/appgrid/lib/res/values-vi/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Chức năng này sẽ xoá mọi thứ tự tuỳ chỉnh. Bạn có muốn tiếp tục không?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Tất cả ứng dụng"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Ứng dụng đa phương tiện"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Dừng ứng dụng"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Thông tin ứng dụng"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"<xliff:g id="APP_NAME">%1$s</xliff:g> đã bị dừng."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Dừng ứng dụng?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Nếu bạn buộc một ứng dụng dừng lại, ứng dụng đó có thể hoạt động không đúng cách."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"Không dừng được ứng dụng."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"Không thể dùng <xliff:g id="APP_NAME">%1$s</xliff:g> trong khi lái xe."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Ẩn các ứng dụng gỡ lỗi"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Hiện các ứng dụng gỡ lỗi"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-zh-rCN/strings.xml b/libs/appgrid/lib/res/values-zh-rCN/strings.xml
index 6755a3b..5b40485 100644
--- a/libs/appgrid/lib/res/values-zh-rCN/strings.xml
+++ b/libs/appgrid/lib/res/values-zh-rCN/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"此功能会撤消所有自定义排序。要继续吗?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"所有应用"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"媒体应用"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"停止应用"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"应用信息"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"已停止“<xliff:g id="APP_NAME">%1$s</xliff:g>”。"</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"要停止应用吗?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"强行停止某个应用可能会导致其出现异常。"</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"无法停止应用。"</string>
- <string name="driving_toast_text" msgid="397905281933065053">"驾车时无法使用“<xliff:g id="APP_NAME">%1$s</xliff:g>”。"</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"隐藏调试应用"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"显示调试应用"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-zh-rHK/strings.xml b/libs/appgrid/lib/res/values-zh-rHK/strings.xml
index 2583635..2b632b9 100644
--- a/libs/appgrid/lib/res/values-zh-rHK/strings.xml
+++ b/libs/appgrid/lib/res/values-zh-rHK/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"此功能會移除所有自訂順序。你要繼續嗎?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"所有應用程式"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"媒體應用程式"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"停止應用程式"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"應用程式資料"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"已停止「<xliff:g id="APP_NAME">%1$s</xliff:g>」。"</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"要停止應用程式嗎?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"強制停止應用程式,可能會導致操作不正常。"</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"無法停止應用程式。"</string>
- <string name="driving_toast_text" msgid="397905281933065053">"駕駛時無法使用「<xliff:g id="APP_NAME">%1$s</xliff:g>」。"</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"隱藏偵錯應用程式"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"顯示偵錯應用程式"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-zh-rTW/strings.xml b/libs/appgrid/lib/res/values-zh-rTW/strings.xml
index baeb589..2eb6003 100644
--- a/libs/appgrid/lib/res/values-zh-rTW/strings.xml
+++ b/libs/appgrid/lib/res/values-zh-rTW/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"這項功能會移除所有自訂排序,要繼續嗎?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"所有應用程式"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"媒體應用程式"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"停止應用程式"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"應用程式資訊"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"已停止「<xliff:g id="APP_NAME">%1$s</xliff:g>」。"</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"要停止應用程式嗎?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"如果你強制停止應用程式,應用程式可能會出現異常行為。"</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"無法停止應用程式。"</string>
- <string name="driving_toast_text" msgid="397905281933065053">"開車時無法使用「<xliff:g id="APP_NAME">%1$s</xliff:g>」。"</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"隱藏偵錯應用程式"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"顯示偵錯應用程式"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values-zu/strings.xml b/libs/appgrid/lib/res/values-zu/strings.xml
index 89cc9e9..f861098 100644
--- a/libs/appgrid/lib/res/values-zu/strings.xml
+++ b/libs/appgrid/lib/res/values-zu/strings.xml
@@ -21,13 +21,7 @@
<string name="reset_appgrid_dialogue_message" msgid="2278301828239327586">"Lo msebenzi uzosusa konke uku-oda ngokwezifiso. Ingabe ufuna ukuqhubeka?"</string>
<string name="app_launcher_title_all_apps" msgid="3522783138519460233">"Wonke ama-app"</string>
<string name="app_launcher_title_media_only" msgid="7194631822174015710">"Ama-app emidiya"</string>
- <string name="app_launcher_stop_app_action" msgid="4488562150865461282">"Misa i-app"</string>
- <string name="app_launcher_app_info_action" msgid="475788297140730900">"Ulwazi lwe-app"</string>
- <string name="app_launcher_stop_app_success_toast_text" msgid="1504575154930117738">"I-<xliff:g id="APP_NAME">%1$s</xliff:g> imisiwe."</string>
- <string name="app_launcher_stop_app_dialog_title" msgid="339411511647776450">"Misa i-app?"</string>
- <string name="app_launcher_stop_app_dialog_text" msgid="5113650734637193870">"Uma uphoqelela ukumisa i-app, kungenzeka ukuthi ingasebenzi kahle."</string>
<string name="app_launcher_stop_app_cant_stop_text" msgid="6513703446595313338">"I-App ayikwazi ukumiswa."</string>
- <string name="driving_toast_text" msgid="397905281933065053">"I-<xliff:g id="APP_NAME">%1$s</xliff:g> ayikwazi ukusetshenziswa ngenkathi ushayela."</string>
<string name="hide_debug_apps" msgid="7140064693464751647">"Fihla ama-app okususa iphutha"</string>
<string name="show_debug_apps" msgid="2748157232151197494">"Bonisa ama-app okususa iphutha"</string>
<string name="user_tos_activity_intent" msgid="5323981034042569291">"intent:#Intent;action=com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=false;end"</string>
diff --git a/libs/appgrid/lib/res/values/colors.xml b/libs/appgrid/lib/res/values/colors.xml
index 580f625..a62fd85 100644
--- a/libs/appgrid/lib/res/values/colors.xml
+++ b/libs/appgrid/lib/res/values/colors.xml
@@ -17,7 +17,6 @@
<resources>
<color name="icon_tint">#FFF8F9FA</color>
<color name="recent_apps_line_divider_color">#1FFFFFFF</color>
- <color name="shortcuts_icon_color">#FFFFFFFF</color>
<color name="page_indicator_bar_color">#FF66B5FF</color>
<color name="app_item_on_hover_border_color">#FF66B5FF</color>
<color name="app_item_on_hover_background_color">#3D66B5FF</color>
diff --git a/libs/appgrid/lib/res/values/config.xml b/libs/appgrid/lib/res/values/config.xml
index 5d214fc..34497aa 100644
--- a/libs/appgrid/lib/res/values/config.xml
+++ b/libs/appgrid/lib/res/values/config.xml
@@ -39,4 +39,17 @@
msg_mirroring_redirect_uri_key
</string>
+ <!--- The number of days to wait before resurfacing the terms of service banner
+ on the app grid after being dismissed
+
+ * Integer value 0 implies to show the banner after every reboot
+ -->
+ <integer name="config_tos_banner_resurface_time_days">1</integer>
+
+ <!--
+ Config for allowing NDO apps to be opened while driving if they contain an active media
+ session. These NDO apps will still be blocked by blocking UI, but may be provided controls.
+ This does not affect the media widget's ability to show or control media sessions.
+ -->
+ <bool name="config_enableMediaSessionAppsWhileDriving">true</bool>
</resources>
diff --git a/libs/appgrid/lib/res/values/overlayable.xml b/libs/appgrid/lib/res/values/overlayable.xml
index 3e29924..90f36e6 100644
--- a/libs/appgrid/lib/res/values/overlayable.xml
+++ b/libs/appgrid/lib/res/values/overlayable.xml
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
-<!-- Copyright (C) 2023 The Android Open Source Project
+<!-- Copyright (C) 2024 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
@@ -23,6 +23,7 @@
<item type="bool" name="car_app_show_recent_apps"/>
<item type="bool" name="car_app_show_toolbar"/>
<item type="bool" name="config_allow_reordering"/>
+ <item type="bool" name="config_enableMediaSessionAppsWhileDriving"/>
<item type="bool" name="use_defined_app_grid_dimensions"/>
<item type="bool" name="use_vertical_app_grid"/>
<item type="color" name="app_item_on_hover_background_color"/>
@@ -34,7 +35,6 @@
<item type="color" name="icon_tint"/>
<item type="color" name="page_indicator_bar_color"/>
<item type="color" name="recent_apps_line_divider_color"/>
- <item type="color" name="shortcuts_icon_color"/>
<item type="dimen" name="app_bar_height"/>
<item type="dimen" name="app_grid_header_margin"/>
<item type="dimen" name="app_grid_height"/>
@@ -75,29 +75,30 @@
<item type="dimen" name="threshold_to_start_drag_drop"/>
<item type="drawable" name="app_item_highlight"/>
<item type="drawable" name="banner_warning_image"/>
- <item type="drawable" name="ic_app_info"/>
- <item type="drawable" name="ic_force_stop_caution_icon"/>
<item type="drawable" name="page_indicator_bar"/>
<item type="id" name="app_icon"/>
<item type="id" name="app_item"/>
<item type="id" name="app_name"/>
<item type="id" name="apps_grid"/>
<item type="id" name="apps_grid_background"/>
+ <item type="id" name="apps_grid_background_container"/>
+ <item type="id" name="banner_first_button"/>
+ <item type="id" name="banner_icon"/>
+ <item type="id" name="banner_second_button"/>
<item type="id" name="banner_title"/>
<item type="id" name="divider"/>
- <item type="id" name="first_button"/>
<item type="id" name="focus_area"/>
- <item type="id" name="icon"/>
+ <item type="id" name="fragmentContainer"/>
<item type="id" name="page_indicator"/>
<item type="id" name="page_indicator_container"/>
<item type="id" name="recent_apps_row"/>
- <item type="id" name="second_button"/>
<item type="id" name="tos_banner"/>
<item type="integer" name="car_app_selector_column_number"/>
<item type="integer" name="car_app_selector_row_number"/>
<item type="integer" name="config_msg_register_mirroring_pkg_code"/>
<item type="integer" name="config_msg_send_mirroring_pkg_code"/>
<item type="integer" name="config_msg_unregister_mirroring_pkg_code"/>
+ <item type="integer" name="config_tos_banner_resurface_time_days"/>
<item type="integer" name="ms_background_highlight_duration"/>
<item type="integer" name="ms_drop_animation_delay"/>
<item type="integer" name="ms_long_press_animation_duration"/>
@@ -107,16 +108,12 @@
<item type="integer" name="ms_scrollbar_appear_animation_duration"/>
<item type="integer" name="ms_scrollbar_fade_animation_delay"/>
<item type="integer" name="ms_scrollbar_fade_animation_duration"/>
- <item type="layout" name="app_grid_activity"/>
+ <item type="layout" name="app_grid_container_activity"/>
+ <item type="layout" name="app_grid_fragment"/>
<item type="layout" name="app_item"/>
<item type="layout" name="banner"/>
<item type="layout" name="recent_apps_row"/>
- <item type="string" name="app_launcher_app_info_action"/>
- <item type="string" name="app_launcher_stop_app_action"/>
<item type="string" name="app_launcher_stop_app_cant_stop_text"/>
- <item type="string" name="app_launcher_stop_app_dialog_text"/>
- <item type="string" name="app_launcher_stop_app_dialog_title"/>
- <item type="string" name="app_launcher_stop_app_success_toast_text"/>
<item type="string" name="app_launcher_title_all_apps"/>
<item type="string" name="app_launcher_title_media_only"/>
<item type="string" name="banner_dismiss_button_text"/>
@@ -126,7 +123,6 @@
<item type="string" name="config_msg_mirroring_redirect_uri_key"/>
<item type="string" name="config_msg_mirroring_service_class_name"/>
<item type="string" name="config_msg_mirroring_service_pkg_name"/>
- <item type="string" name="driving_toast_text"/>
<item type="string" name="hide_debug_apps"/>
<item type="string" name="reset_appgrid_dialogue_message"/>
<item type="string" name="reset_appgrid_title"/>
diff --git a/libs/appgrid/lib/res/values/strings.xml b/libs/appgrid/lib/res/values/strings.xml
index 5d1d0c6..55abe29 100644
--- a/libs/appgrid/lib/res/values/strings.xml
+++ b/libs/appgrid/lib/res/values/strings.xml
@@ -22,26 +22,9 @@
</string>
<string name="app_launcher_title_all_apps">All apps</string>
<string name="app_launcher_title_media_only">Media apps</string>
- <string name="app_launcher_stop_app_action">Stop app</string>
- <string name="app_launcher_app_info_action">App info</string>
- <string name="app_launcher_stop_app_success_toast_text">
- <xliff:g id="app_name" example="Radio">%1$s</xliff:g>
- has been stopped.
- </string>
- <!-- Manage applications, title for dialog when killing persistent apps. [CHAR LIMIT=40] -->
- <string name="app_launcher_stop_app_dialog_title">Stop app?</string>
- <!-- Manage applications, text for dialog when killing persistent apps. [CHAR LIMIT=200] -->
- <string name="app_launcher_stop_app_dialog_text">If you force stop an app, it may
- misbehave.
- </string>
<!-- Manage applications, text for dialog when apps can not be stopped -->
<string name="app_launcher_stop_app_cant_stop_text">App can’t be stopped.</string>
- <string name="driving_toast_text">
- <xliff:g id="app_name" example="Settings">%1$s</xliff:g>
- can\'t be used while driving.
- </string>
-
<!-- Toolbar MenuItem text for hiding debug apps,
only visible on debug builds [CHAR_LIMIT=50] -->
<string name="hide_debug_apps">Hide debug apps</string>
diff --git a/libs/appgrid/lib/robotests/Android.bp b/libs/appgrid/lib/robotests/Android.bp
new file mode 100644
index 0000000..42aa1c7
--- /dev/null
+++ b/libs/appgrid/lib/robotests/Android.bp
@@ -0,0 +1,54 @@
+// Copyright (C) 2024 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+ default_team: "trendy_team_system_experience",
+}
+
+android_robolectric_test {
+
+ name: "CarAppGridUnitTests",
+
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+
+ java_resource_dirs: ["config"],
+
+ static_libs: [
+ "mockito-robolectric-prebuilt",
+ "mockito-kotlin2",
+ "androidx.test.rules",
+ "kotlinx_coroutines_test",
+ "androidx.test.core",
+ "android.car.testapi",
+ "android.car-system-stubs",
+ ],
+
+ test_suites: [
+ "automotive-general-tests",
+ "general-tests",
+ ],
+
+ test_options: {
+ timeout: 36000,
+ },
+
+ instrumentation_for: "CarAppGridTestApp",
+
+ strict_mode: false,
+}
diff --git a/libs/appgrid/lib/robotests/config/robolectric.properties b/libs/appgrid/lib/robotests/config/robolectric.properties
new file mode 100644
index 0000000..9b85e56
--- /dev/null
+++ b/libs/appgrid/lib/robotests/config/robolectric.properties
@@ -0,0 +1,17 @@
+#
+# Copyright (C) 2024 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.
+#
+
+sdk=NEWEST_SDK
diff --git a/libs/appgrid/lib/robotests/res/values/config.xml b/libs/appgrid/lib/robotests/res/values/config.xml
new file mode 100644
index 0000000..2372d43
--- /dev/null
+++ b/libs/appgrid/lib/robotests/res/values/config.xml
@@ -0,0 +1,41 @@
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<!--Robolectric test Resources-->
+<resources>
+
+ <!-- Service information to get screen mirroring information -->
+ <string name="config_msg_mirroring_service_pkg_name" translatable="false">
+ com.android.car.mirroring.test
+ </string>
+ <string name="config_msg_mirroring_service_class_name" translatable="false">
+ com.android.car.mirroring.test.ServiceClassName
+ </string>
+
+ <integer name="config_msg_register_mirroring_pkg_code">1</integer>
+ <integer name="config_msg_unregister_mirroring_pkg_code">2</integer>
+ <integer name="config_msg_send_mirroring_pkg_code">3</integer>
+ <string name="config_msg_mirroring_pkg_name_key" translatable="false">
+ msg_mirroring_pkg_name_key
+ </string>
+ <string name="config_msg_mirroring_redirect_uri_key" translatable="false">
+ msg_mirroring_redirect_uri_key
+ </string>
+
+ <!-- Launcher Apps to hide -->
+ <string-array name="hidden_apps" />
+
+</resources>
diff --git a/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/AppOrderProtoDataSourceImplTest.kt b/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/AppOrderProtoDataSourceImplTest.kt
new file mode 100644
index 0000000..f45b10f
--- /dev/null
+++ b/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/AppOrderProtoDataSourceImplTest.kt
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.datasources
+
+import com.android.car.carlauncher.LauncherItemProto
+import com.android.car.carlauncher.LauncherItemProto.LauncherItemMessage
+import com.android.car.carlauncher.datasources.AppOrderDataSource.AppOrderInfo
+import com.android.car.carlauncher.datastore.launcheritem.LauncherItemListSource
+import junit.framework.TestCase.assertEquals
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class AppOrderProtoDataSourceImplTest {
+
+ private val scope = TestScope()
+ private val bgDispatcher =
+ StandardTestDispatcher(scope.testScheduler, name = "Background dispatcher")
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testGetSavedAppOrder_noSavedOrder_sendsEmptyList() = scope.runTest {
+ val launcherItemListSource: LauncherItemListSource = mock()
+ val appOrderDataSource = AppOrderProtoDataSourceImpl(launcherItemListSource, bgDispatcher)
+ val flows = mutableListOf<List<AppOrderInfo>>()
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ appOrderDataSource.getSavedAppOrder().toList(flows)
+ }
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+
+ assertEquals(1, flows.size)
+ verify(launcherItemListSource).readFromFile()
+ assertEquals(emptyList<AppOrderInfo>(), flows[0])
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testGetSavedAppOrder_hasSavedOrder_sendsUpdatedList() = scope.runTest {
+ val previouslySavedApp1 = LauncherItemMessage.newBuilder()
+ .setPackageName("PackageName1")
+ .setClassName("ClassName1")
+ .setDisplayName("DisplayName1")
+ .setRelativePosition(1)
+ .setContainerID(-1).build()
+ val previouslySavedApp2 = LauncherItemMessage.newBuilder()
+ .setPackageName("PackageName2")
+ .setClassName("ClassName2")
+ .setDisplayName("DisplayName2")
+ .setRelativePosition(2)
+ .setContainerID(-1).build()
+ val savedProtoAppOrder = listOf(previouslySavedApp1, previouslySavedApp2)
+ val expectedAppOrder =
+ savedProtoAppOrder.map {
+ AppOrderInfo(
+ it.packageName,
+ it.className,
+ it.displayName
+ )
+ }
+ val launcherItemListSource: LauncherItemListSource = mock {
+ on { readFromFile() } doReturn
+ LauncherItemProto.LauncherItemListMessage.newBuilder()
+ .addAllLauncherItemMessage(savedProtoAppOrder).build()
+ }
+ val appOrderDataSource = AppOrderProtoDataSourceImpl(launcherItemListSource, bgDispatcher)
+ val flows = mutableListOf<List<AppOrderInfo>>()
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ appOrderDataSource.getSavedAppOrder().toList(flows)
+ }
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+
+ assertEquals(1, flows.size)
+ verify(launcherItemListSource).readFromFile()
+ assertEquals(expectedAppOrder, flows[0])
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testSavedAppOrder_savesPersistently_reportsChangeToCollectors() = scope.runTest {
+ val newAppInfo1 = AppOrderInfo("PackageName1", "ClassName1", "DisplayName1")
+ val newAppInfo2 = AppOrderInfo("PackageName2", "ClassName2", "DisplayName2")
+ val newSaveOrder = listOf(newAppInfo1, newAppInfo2)
+ val expectedProtoAppOrder = newSaveOrder.mapIndexed { index, it ->
+ LauncherItemMessage.newBuilder()
+ .setPackageName(it.packageName)
+ .setClassName(it.className)
+ .setDisplayName(it.displayName)
+ .setRelativePosition(index)
+ .setContainerID(-1).build()
+ }
+ val expectedAppOrderProtoMessage = LauncherItemProto.LauncherItemListMessage.newBuilder()
+ .addAllLauncherItemMessage(expectedProtoAppOrder).build()
+ val launcherItemListSource: LauncherItemListSource = mock()
+ val appOrderDataSource = AppOrderProtoDataSourceImpl(launcherItemListSource, bgDispatcher)
+ val savedOrderFlows = mutableListOf<List<AppOrderInfo>>()
+ val savedOrderComparatorFlows = mutableListOf<Comparator<AppOrderInfo>>()
+ // collect flows to listen for updates.
+ launch(StandardTestDispatcher(testScheduler)) {
+ appOrderDataSource.getSavedAppOrder().toList(savedOrderFlows)
+ }
+ advanceUntilIdle()
+ launch(StandardTestDispatcher(testScheduler)) {
+ appOrderDataSource.getSavedAppOrderComparator().toList(savedOrderComparatorFlows)
+ }
+ advanceUntilIdle()
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ appOrderDataSource.saveAppOrder(newSaveOrder)
+ }
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+
+ assertEquals(2, savedOrderFlows.size)
+ assertEquals(2, savedOrderComparatorFlows.size)
+ verify(launcherItemListSource, times(2)).readFromFile()
+ verify(launcherItemListSource).writeToFile(expectedAppOrderProtoMessage)
+ // initial empty app order.
+ assertEquals(emptyList<AppOrderInfo>(), savedOrderFlows[0])
+ // app order after newly saved order.
+ assertEquals(newSaveOrder, savedOrderFlows[1])
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testGetSavedAppOrderComparator_sortsAppsWithSavedOrder() = scope.runTest {
+ val previouslySavedApp1 = LauncherItemMessage.newBuilder()
+ .setPackageName("PackageName1")
+ .setClassName("ClassName1")
+ .setDisplayName("DisplayName1")
+ .setRelativePosition(1)
+ .setContainerID(-1).build()
+ val previouslySavedApp2 = LauncherItemMessage.newBuilder()
+ .setPackageName("PackageName2")
+ .setClassName("ClassName2")
+ .setDisplayName("DisplayName2")
+ .setRelativePosition(2)
+ .setContainerID(-1).build()
+ val savedProtoAppOrder = listOf(previouslySavedApp1, previouslySavedApp2)
+ val launcherItemListSource: LauncherItemListSource = mock {
+ on { readFromFile() } doReturn
+ LauncherItemProto.LauncherItemListMessage.newBuilder()
+ .addAllLauncherItemMessage(savedProtoAppOrder).build()
+ }
+ // mix of known and unknown app order
+ val appToBeSorted1 = AppOrderInfo("PackageName4", "ClassName4", "DisplayName4")
+ val appToBeSorted2 = AppOrderInfo("PackageName2", "ClassName2", "DisplayName2")
+ val appToBeSorted3 = AppOrderInfo("PackageName1", "ClassName1", "DisplayName1")
+ val appToBeSorted4 = AppOrderInfo("PackageName3", "ClassName3", "DisplayName3")
+ // Unsorted list of apps applied with the comparator under test.
+ val appsTobeSorted = listOf(appToBeSorted1, appToBeSorted2, appToBeSorted3, appToBeSorted4)
+ // The above list is expected to be sorted as below. Apps with unknown app order are sorted
+ // by AppOrderInfo#displayName.
+ val expectedOrder = listOf(appToBeSorted3, appToBeSorted2, appToBeSorted4, appToBeSorted1)
+ val appOrderDataSource = AppOrderProtoDataSourceImpl(launcherItemListSource, bgDispatcher)
+ val flows = mutableListOf<Comparator<AppOrderInfo>>()
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ appOrderDataSource.getSavedAppOrderComparator().toList(flows)
+ }
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+
+ assertEquals(1, flows.size)
+ verify(launcherItemListSource).readFromFile()
+ assertEquals(expectedOrder, appsTobeSorted.sortedWith(flows[0]))
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testClearAppOrder_clearsAppOrder_reportsUpdateToCollectors() = scope.runTest {
+ val previouslySavedApp1 = LauncherItemMessage.newBuilder()
+ .setPackageName("PackageName1")
+ .setClassName("ClassName1")
+ .setDisplayName("DisplayName1")
+ .setRelativePosition(1)
+ .setContainerID(-1).build()
+ val savedProtoAppOrder = listOf(previouslySavedApp1)
+ val expectedAppOrder = savedProtoAppOrder.map {
+ AppOrderInfo(
+ it.packageName,
+ it.className,
+ it.displayName
+ )
+ }
+ val launcherItemListSource: LauncherItemListSource = mock {
+ on { readFromFile() } doReturn
+ LauncherItemProto.LauncherItemListMessage.newBuilder()
+ .addAllLauncherItemMessage(savedProtoAppOrder).build()
+ on { deleteFile() } doReturn true
+ }
+ val appOrderDataSource = AppOrderProtoDataSourceImpl(launcherItemListSource, bgDispatcher)
+ // collect flows to listen for updates.
+ val savedOrderFlows = mutableListOf<List<AppOrderInfo>>()
+ val savedOrderComparatorFlows = mutableListOf<Comparator<AppOrderInfo>>()
+ launch(StandardTestDispatcher(testScheduler)) {
+ appOrderDataSource.getSavedAppOrder().toList(savedOrderFlows)
+ }
+ advanceUntilIdle()
+ launch(StandardTestDispatcher(testScheduler)) {
+ appOrderDataSource.getSavedAppOrderComparator().toList(savedOrderComparatorFlows)
+ }
+ advanceUntilIdle()
+
+ appOrderDataSource.clearAppOrder()
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+
+ assertEquals(2, savedOrderFlows.size)
+ assertEquals(2, savedOrderComparatorFlows.size)
+ verify(launcherItemListSource).deleteFile()
+ assertEquals(expectedAppOrder, savedOrderFlows[0])
+ assertEquals(expectedAppOrder, savedOrderFlows[0])
+ assertEquals(emptyList<AppOrderInfo>(), savedOrderFlows[1])
+ assertEquals(emptyList<AppOrderInfo>(), savedOrderFlows[1])
+ }
+}
diff --git a/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/ControlCenterMirroringDataSourceImplTest.kt b/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/ControlCenterMirroringDataSourceImplTest.kt
new file mode 100644
index 0000000..abbad98
--- /dev/null
+++ b/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/ControlCenterMirroringDataSourceImplTest.kt
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.datasources
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.os.Message
+import android.os.Messenger
+import com.android.car.carlauncher.R
+import com.android.car.carlauncher.datasources.ControlCenterMirroringDataSource.MirroringPackageData
+import com.android.car.carlauncher.datasources.ControlCenterMirroringDataSourceImpl.MirroringServiceConnection
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertNotNull
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.shadows.ShadowLooper
+
+@RunWith(RobolectricTestRunner::class)
+class ControlCenterMirroringDataSourceImplTest {
+
+ private val scope = TestScope()
+ private val bgDispatcher =
+ StandardTestDispatcher(scope.testScheduler, name = "Background dispatcher")
+ private val resources = RuntimeEnvironment.getApplication().resources
+ private var bindServiceIntent: Intent? = null
+ private var bindServiceConnection: MirroringServiceConnection? = null
+ private var bindServiceFlags: Int? = null
+ private val bindService: (Intent, MirroringServiceConnection, Int) -> Unit =
+ { intent, serviceConnection, flags ->
+ bindServiceIntent = intent
+ bindServiceConnection = serviceConnection
+ bindServiceFlags = flags
+ }
+
+ private val unbindService: (MirroringServiceConnection) -> Unit = mock()
+
+ private val packageManager: PackageManager = mock {
+ on { resolveService(any(), anyInt()) } doReturn ResolveInfo()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun getAppMirroringSession_registersService() = scope.runTest {
+ val expectedBindServiceFlags = Context.BIND_AUTO_CREATE or Context.BIND_IMPORTANT
+ val controlCenterMirroringDataSource = ControlCenterMirroringDataSourceImpl(
+ resources,
+ bindService,
+ unbindService,
+ packageManager,
+ bgDispatcher
+ )
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ controlCenterMirroringDataSource.getAppMirroringSession().collect()
+ }
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+
+ val intentCaptor = ArgumentCaptor.forClass(Intent::class.java)
+ verify(packageManager).resolveService(intentCaptor.capture(), anyInt())
+ assertEquals(
+ ComponentName(
+ resources.getString(R.string.config_msg_mirroring_service_pkg_name),
+ resources.getString(R.string.config_msg_mirroring_service_class_name)
+ ),
+ intentCaptor.value.component
+ )
+ assertEquals(bindServiceIntent, intentCaptor.value)
+ assertNotNull(bindServiceConnection)
+ assertNotNull(bindServiceConnection?.mClientMessenger)
+ assertEquals(expectedBindServiceFlags, bindServiceFlags)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun getAppMirroringSession_noActiveSession_sendsNoMirroringData() = scope.runTest {
+ val controlCenterMirroringDataSource = ControlCenterMirroringDataSourceImpl(
+ resources,
+ bindService,
+ unbindService,
+ packageManager,
+ bgDispatcher
+ )
+ val flows = mutableListOf<MirroringPackageData>()
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ controlCenterMirroringDataSource.getAppMirroringSession().toList(flows)
+ }
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+
+ assertEquals(1, flows.size)
+ assertEquals(MirroringPackageData.NO_MIRRORING, flows[0])
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun getAppMirroringSession_activeSession_sendsMirroringData() = scope.runTest {
+ val msg = createClientMessage()
+ val controlCenterMirroringDataSource = ControlCenterMirroringDataSourceImpl(
+ resources,
+ bindService,
+ unbindService,
+ packageManager,
+ bgDispatcher
+ )
+ val flows = mutableListOf<MirroringPackageData>()
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ controlCenterMirroringDataSource.getAppMirroringSession().toList(flows)
+ }
+ advanceUntilIdle()
+ // Fake sending Mirroring messages to receiver
+ val clientMessenger = bindServiceConnection?.mClientMessenger
+ clientMessenger?.send(msg)
+ ShadowLooper.runUiThreadTasks()
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+
+ assertEquals(2, flows.size)
+ assertEquals(MirroringPackageData.NO_MIRRORING, flows[0]) // Initial no mirroring packet
+ assertEquals(mirroringPackageName, flows[1].packageName)
+ assert(
+ Intent.parseUri(mirroringRedirectUri, Intent.URI_INTENT_SCHEME)
+ .filterEquals(flows[1].launchIntent)
+ )
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun getAppMirroringSession_sessionEnd_sendsNoMirroring() = scope.runTest {
+ // Empty packageName for no mirroring
+ val msg = createClientMessage(packageName = "")
+ val controlCenterMirroringDataSource = ControlCenterMirroringDataSourceImpl(
+ resources,
+ bindService,
+ unbindService,
+ packageManager,
+ bgDispatcher
+ )
+ val flows = mutableListOf<MirroringPackageData>()
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ controlCenterMirroringDataSource.getAppMirroringSession().toList(flows)
+ }
+ advanceUntilIdle()
+ // Fake sending Mirroring messages to receiver
+ val clientMessenger = bindServiceConnection?.mClientMessenger
+ clientMessenger?.send(msg)
+ ShadowLooper.runUiThreadTasks()
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+
+ assertEquals(2, flows.size)
+ assertEquals(MirroringPackageData.NO_MIRRORING, flows[0]) // Initial no mirroring packet
+ assertEquals(MirroringPackageData.NO_MIRRORING, flows[1]) // Service no mirroring packet
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun getAppMirroringSession_scopeClosed_shouldCleanUp() = scope.runTest {
+ val expectedUnRegisterMsg = Message.obtain(
+ null,
+ resources.getInteger(R.integer.config_msg_unregister_mirroring_pkg_code)
+ )
+ var actualUnRegisterMsg: Message? = null
+ val serviceMessenger = Messenger(object : Handler(Looper.myLooper()!!) {
+ override fun handleMessage(msg: Message) {
+ super.handleMessage(msg)
+ actualUnRegisterMsg = msg
+ }
+ })
+ val controlCenterMirroringDataSource = ControlCenterMirroringDataSourceImpl(
+ resources,
+ bindService,
+ unbindService,
+ packageManager,
+ bgDispatcher
+ )
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ controlCenterMirroringDataSource.getAppMirroringSession().collect()
+ }
+ advanceUntilIdle()
+ // Assign an external serviceMessenger
+ bindServiceConnection?.mServiceMessenger = serviceMessenger
+ coroutineContext.cancelChildren()
+ advanceUntilIdle()
+ ShadowLooper.runUiThreadTasks()
+
+ assertEquals(expectedUnRegisterMsg.toString(), actualUnRegisterMsg.toString())
+ assertNotNull(bindServiceConnection)
+ verify(unbindService).invoke(bindServiceConnection!!)
+ }
+
+ /**
+ * @param packageName name of the package under mirroring session,
+ * If Empty it signifies [MirroringPackageData.NO_MIRRORING].
+ * @return [Message] to send to the Client to report mirroring session.
+ */
+ private fun createClientMessage(packageName: String = mirroringPackageName): Message {
+ val sendMirroringPackageCode =
+ resources.getInteger(R.integer.config_msg_send_mirroring_pkg_code)
+ val packageNameKey = resources.getString(R.string.config_msg_mirroring_pkg_name_key)
+ val redirectUriKey = resources.getString(R.string.config_msg_mirroring_redirect_uri_key)
+ val msg: Message = Message.obtain(
+ null,
+ resources.getInteger(R.integer.config_msg_register_mirroring_pkg_code)
+ )
+ msg.what = sendMirroringPackageCode
+ msg.obj = Bundle().apply {
+ putString(packageNameKey, packageName)
+ putString(redirectUriKey, mirroringRedirectUri)
+ }
+ return msg
+ }
+
+ companion object {
+ const val mirroringPackageName = "TestPackageName"
+ const val mirroringRedirectUri = "intent:#Intent;action=android.test.MIRRORING_TEST;end"
+ }
+}
diff --git a/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/LauncherActivitiesDataSourceImplTest.kt b/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/LauncherActivitiesDataSourceImplTest.kt
new file mode 100644
index 0000000..d32ccc6
--- /dev/null
+++ b/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/LauncherActivitiesDataSourceImplTest.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.datasources
+
+import android.content.BroadcastReceiver
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.LauncherActivityInfo
+import android.content.pm.LauncherApps
+import android.net.Uri
+import android.os.UserHandle
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+
+@RunWith(RobolectricTestRunner::class)
+class LauncherActivitiesDataSourceImplTest {
+
+ private val scope = TestScope()
+ private val bgDispatcher =
+ StandardTestDispatcher(scope.testScheduler, name = "Background dispatcher")
+
+ private val launcherActivities: List<LauncherActivityInfo> = listOf(mock(), mock())
+ private var broadcastReceiverCallback: BroadcastReceiver? = null
+ private val registerReceiverFun: (BroadcastReceiver, IntentFilter) -> Unit =
+ { broadcastReceiver, _ ->
+ broadcastReceiverCallback = broadcastReceiver
+ }
+ private val unregisterReceiverFun: (BroadcastReceiver) -> Unit = mock()
+ private val myUserHandle: UserHandle = mock()
+ private val launcherApps: LauncherApps = mock {
+ on { getActivityList(null, myUserHandle) } doReturn launcherActivities
+ }
+ private val dataSource: LauncherActivitiesDataSource = LauncherActivitiesDataSourceImpl(
+ launcherApps,
+ registerReceiverFun,
+ unregisterReceiverFun,
+ myUserHandle,
+ RuntimeEnvironment.getApplication().resources,
+ bgDispatcher
+ )
+
+ @Test
+ fun testGetAllLauncherActivities() = scope.runTest {
+ val listOfApps = dataSource.getAllLauncherActivities()
+
+ assertEquals(listOfApps.size, 2)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testOnPackagesChanged_broadcastReceived_shouldUpdateFlow() = scope.runTest {
+ // reset the broadcastReceiverCallback to null
+ broadcastReceiverCallback = null
+ val flows = mutableListOf<String>()
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ dataSource.getOnPackagesChanged().toList(flows)
+ }
+ advanceUntilIdle()
+ // Make a fake change in packages broadcast event.
+ val uri1 =
+ mock<Uri> { on { schemeSpecificPart } doReturn BROADCAST_EXPECTED_PACKAGE_NAME_1 }
+ val intent1: Intent = mock {
+ on { data } doReturn uri1
+ }
+ broadcastReceiverCallback?.onReceive(mock(), intent1)
+ advanceUntilIdle()
+ // Make another fake broadcast event with different package name
+ val uri2 =
+ mock<Uri> { on { schemeSpecificPart } doReturn BROADCAST_EXPECTED_PACKAGE_NAME_2 }
+ val intent2: Intent = mock {
+ on { data } doReturn uri2
+ }
+ broadcastReceiverCallback?.onReceive(mock(), intent2)
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+
+ // BroadcastReceiver must been set after the producer call is trigger.
+ assertNotNull(broadcastReceiverCallback)
+ // Producer block sends an empty package immediately to the collector.
+ assertEquals(flows[0], "")
+ assertEquals(flows[1], BROADCAST_EXPECTED_PACKAGE_NAME_1)
+ assertEquals(flows[2], BROADCAST_EXPECTED_PACKAGE_NAME_2)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testOnPackagesChanged_scopeClosed_shouldCleanup() = scope.runTest {
+ // reset the broadcastReceiverCallback to null
+ broadcastReceiverCallback = null
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ dataSource.getOnPackagesChanged().collect()
+ }
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+ advanceUntilIdle()
+
+ // close all child coroutines, this should close the scope.
+ assertNotNull(broadcastReceiverCallback)
+ broadcastReceiverCallback?.let {
+ verify(unregisterReceiverFun).invoke(it)
+ }
+ }
+
+ companion object {
+ const val BROADCAST_EXPECTED_PACKAGE_NAME_1 = "com.test.example1"
+ const val BROADCAST_EXPECTED_PACKAGE_NAME_2 = "com.test.example2"
+ }
+}
diff --git a/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/MediaTemplateAppsDataSourceImplTest.kt b/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/MediaTemplateAppsDataSourceImplTest.kt
new file mode 100644
index 0000000..066fe27
--- /dev/null
+++ b/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/MediaTemplateAppsDataSourceImplTest.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.datasources
+
+import android.content.ComponentName
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.content.pm.ServiceInfo
+import junit.framework.TestCase.assertEquals
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.annotation.Config
+import shadows.ShadowMediaSource
+
+@RunWith(RobolectricTestRunner::class)
+@Config(shadows = [ShadowMediaSource::class])
+class MediaTemplateAppsDataSourceImplTest {
+
+ private val scope = TestScope()
+ private val appContext = RuntimeEnvironment.getApplication().applicationContext
+ private val bgDispatcher =
+ StandardTestDispatcher(scope.testScheduler, name = "Background dispatcher")
+
+ private val listOfComponentNames = listOf(
+ ComponentName("com.test.media.package1", "Media"), // 0, MediaService
+ ComponentName("com.test.media.package2", "Media"), // 1, MediaService
+ ComponentName(
+ "com.test.custom.media.package2",
+ "CustomMedia"
+ ), // 2, has MediaService + Launcher Activity = CustomMediaSource
+ ComponentName("com.test.not.media.package3", "NotMedia"), // 3, Not a MediaService
+ )
+
+ // List of MediaServices returned by the PackageManager for queryIntentServices.
+ private val mediaServices: List<ResolveInfo> = listOfComponentNames.map { getResolveInfo(it) }
+
+ /**
+ * Returns a mocked ResolveInfo
+ * @param componentName packageName + className of the mocked [ServiceInfo]
+ */
+ private fun getResolveInfo(componentName: ComponentName): ResolveInfo {
+ return ResolveInfo().apply {
+ serviceInfo = ServiceInfo().apply {
+ packageName = componentName.packageName
+ name = componentName.className
+ }
+ }
+ }
+
+ @Test
+ fun getAllMediaServices_noCustomComponents_shouldFilterMediaTemplateApps() = scope.runTest {
+ ShadowMediaSource.setMediaTemplates(
+ listOf(
+ listOfComponentNames[0],
+ listOfComponentNames[1]
+ )
+ )
+ val packageManager: PackageManager = mock {
+ on {
+ queryIntentServices(
+ any(), anyInt()
+ )
+ } doReturn mediaServices
+ }
+ val mediaAppsDataSource = MediaTemplateAppsDataSourceImpl(
+ packageManager,
+ appContext,
+ bgDispatcher
+ )
+
+ val outputMediaServiceInfoList =
+ mediaAppsDataSource.getAllMediaServices(includeCustomMediaPackages = false)
+
+ val expectedMediaList =
+ mediaServices.filterIndexed { index, _ -> index != 2 && index != 3 }
+ assertEquals(expectedMediaList, outputMediaServiceInfoList)
+ }
+
+ @Test
+ fun getAllMediaServices_includeCustomComponents_shouldFilterMediaTemplateAndCustomApps() =
+ scope.runTest {
+ ShadowMediaSource.setMediaTemplates(
+ listOf(
+ listOfComponentNames[0],
+ listOfComponentNames[1]
+ )
+ )
+ ShadowMediaSource.setCustomTemplates(listOf(listOfComponentNames[2]))
+ val packageManager: PackageManager = mock {
+ on {
+ queryIntentServices(
+ any(), anyInt()
+ )
+ } doReturn mediaServices
+ }
+ val mediaAppsDataSource = MediaTemplateAppsDataSourceImpl(
+ packageManager,
+ appContext,
+ bgDispatcher
+ )
+
+ val outputMediaServiceInfoList =
+ mediaAppsDataSource.getAllMediaServices(includeCustomMediaPackages = true)
+
+ val expectedMediaList = mediaServices.dropLast(1)
+ assertEquals(expectedMediaList, outputMediaServiceInfoList)
+ }
+}
diff --git a/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/UXRestrictionsDataSourceImplTest.kt b/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/UXRestrictionsDataSourceImplTest.kt
new file mode 100644
index 0000000..e7d9e64
--- /dev/null
+++ b/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/UXRestrictionsDataSourceImplTest.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.datasources
+
+import android.car.Car
+import android.car.content.pm.CarPackageManager
+import android.car.drivingstate.CarUxRestrictions
+import android.car.drivingstate.CarUxRestrictionsManager
+import android.car.testapi.FakeCar
+import android.content.Context
+import android.media.session.MediaSessionManager
+import androidx.test.core.app.ApplicationProvider
+import java.lang.reflect.Field
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertFalse
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.shadows.ShadowLooper
+
+@RunWith(RobolectricTestRunner::class)
+class UXRestrictionsDataSourceImplTest {
+
+ private val scope = TestScope()
+ private val bgDispatcher =
+ StandardTestDispatcher(scope.testScheduler, name = "Background dispatcher")
+ private val fakeCar: FakeCar =
+ FakeCar.createFakeCar(ApplicationProvider.getApplicationContext())
+ private val carUxRestrictionsManager =
+ fakeCar.car.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE) as CarUxRestrictionsManager
+ private val carUxRestrictionsController = fakeCar.carUxRestrictionController
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ /**
+ * Updates the CarUxRestrictions and notifies any active listeners.
+ *
+ * TODO: b/319266967 - FakeCarUxRestrictionsService always sets requiresDistractionOptimization
+ * to 'false' when creating CarUxRestrictions.
+ * Use reflection to set CarUxRestrictions with mRequiresDistractionOptimization as true.
+ */
+ private fun updateCarUxRestrictions(isDistractionOptimized: Boolean) {
+ // Set Restrictions to trigger callback.
+ carUxRestrictionsController.setUxRestrictions(CarUxRestrictions.UX_RESTRICTIONS_BASELINE)
+
+ val carUxRestrictionsField: Field =
+ carUxRestrictionsController.javaClass.getDeclaredField("mCarUxRestrictions")
+ carUxRestrictionsField.isAccessible = true
+ val currentCarUxRestrictions =
+ carUxRestrictionsField[carUxRestrictionsController] as CarUxRestrictions
+ val requiresDistractionOptimizationField: Field =
+ currentCarUxRestrictions.javaClass.getDeclaredField("mRequiresDistractionOptimization")
+ requiresDistractionOptimizationField.isAccessible = true
+
+ requiresDistractionOptimizationField[currentCarUxRestrictions] = isDistractionOptimized
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun requiresDistractionOptimization_sendsRequired() =
+ scope.runTest {
+ val uxRestrictionDataSource =
+ UXRestrictionDataSourceImpl(
+ context,
+ carUxRestrictionsManager,
+ mock(CarPackageManager::class.java),
+ mock(MediaSessionManager::class.java),
+ RuntimeEnvironment.getApplication().resources,
+ bgDispatcher
+ )
+ val outputFlows = mutableListOf<Boolean>()
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ uxRestrictionDataSource.requiresDistractionOptimization().toList(outputFlows)
+ }
+ advanceUntilIdle()
+ updateCarUxRestrictions(isDistractionOptimized = true)
+ ShadowLooper.runUiThreadTasks()
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+
+ // Asserts initial value of the DO(Distraction Optimization) as false and updated
+ // callback as true.
+ assertEquals(listOf(false, true), outputFlows)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun requiresDistractionOptimization_sendsNotRequired() = scope.runTest {
+ val uxRestrictionDataSource =
+ UXRestrictionDataSourceImpl(
+ context,
+ carUxRestrictionsManager,
+ mock(CarPackageManager::class.java),
+ mock(MediaSessionManager::class.java),
+ RuntimeEnvironment.getApplication().resources,
+ bgDispatcher
+ )
+ val outputFlows = mutableListOf<Boolean>()
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ uxRestrictionDataSource.requiresDistractionOptimization().toList(outputFlows)
+ }
+ advanceUntilIdle()
+ updateCarUxRestrictions(isDistractionOptimized = false)
+ ShadowLooper.runUiThreadTasks()
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+
+ // Asserts initial value of the DO(Distraction Optimization) as false and updated
+ // callback as false.
+ assertEquals(listOf(false, false), outputFlows)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun requiresDistractionOptimization_scopeClosed_shouldCleanUp() = scope.runTest {
+ val uxRestrictionDataSource =
+ UXRestrictionDataSourceImpl(
+ context,
+ carUxRestrictionsManager,
+ mock(CarPackageManager::class.java),
+ mock(MediaSessionManager::class.java),
+ RuntimeEnvironment.getApplication().resources,
+ bgDispatcher
+ )
+ val outputFlows = mutableListOf<Boolean>()
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ uxRestrictionDataSource.requiresDistractionOptimization().toList(outputFlows)
+ }
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+ advanceUntilIdle()
+
+ assertFalse(carUxRestrictionsController.isListenerRegistered)
+ }
+}
diff --git a/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/restricted/DisabledAppsDataSourceImplTest.kt b/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/restricted/DisabledAppsDataSourceImplTest.kt
new file mode 100644
index 0000000..71c3f72
--- /dev/null
+++ b/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/restricted/DisabledAppsDataSourceImplTest.kt
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.datasources.restricted
+
+import android.car.settings.CarSettings
+import android.content.ComponentName
+import android.content.ContentResolver
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.provider.Settings
+import com.android.car.carlauncher.datasources.restricted.DisabledAppsDataSourceImpl.Companion.PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR
+import junit.framework.TestCase.assertEquals
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.shadows.ShadowLooper
+
+@RunWith(RobolectricTestRunner::class)
+class DisabledAppsDataSourceImplTest {
+
+ private val scope = TestScope()
+ private val bgDispatcher =
+ StandardTestDispatcher(scope.testScheduler, name = "Background dispatcher")
+
+ private val packageManager: PackageManager = mock {
+ on {
+ queryIntentActivities(
+ any(), any<PackageManager.ResolveInfoFlags>()
+ )
+ } doReturn listOf(
+ getResolveInfo(INSTALLED_COMPONENT_NAME_1),
+ getResolveInfo(INSTALLED_COMPONENT_NAME_2),
+ getResolveInfo(INSTALLED_COMPONENT_NAME_3),
+ )
+ }
+
+ private val contentResolver: ContentResolver =
+ RuntimeEnvironment.getApplication().contentResolver
+
+ /**
+ * Returns a mocked ResolveInfo
+ * @param componentName packageName + className of the mocked [ActivityInfo]
+ */
+ private fun getResolveInfo(componentName: ComponentName): ResolveInfo {
+ return ResolveInfo().apply {
+ activityInfo = ActivityInfo().apply {
+ packageName = componentName.packageName
+ name = componentName.className
+ }
+ }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testGetDisabledApps_shouldReturnMatchedDisabledApps() = scope.runTest {
+ Settings.Secure.putString(
+ contentResolver,
+ CarSettings.Secure.KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE,
+ EXPECTED_MATCH_DISABLED_PACKAGE_NAME_1 +
+ PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR +
+ EXPECTED_NO_MATCH_DISABLED_PACKAGE_NAME_3
+ )
+ val disabledAppsDataSource = DisabledAppsDataSourceImpl(
+ contentResolver,
+ packageManager,
+ bgDispatcher
+ )
+ val flows = mutableListOf<List<ResolveInfo>>()
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ disabledAppsDataSource.getDisabledApps().toList(flows)
+ }
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+
+ assertEquals(1, flows.size)
+ val actualMatchedPackages =
+ flows[0].map { it.activityInfo.packageName }
+ assertEquals(listOf(EXPECTED_MATCH_DISABLED_PACKAGE_NAME_1), actualMatchedPackages)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testGetDisabledApps_disabledAppsUpdated_shouldUpdateFlow() = scope.runTest {
+ Settings.Secure.putString(
+ contentResolver,
+ CarSettings.Secure.KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE,
+ EXPECTED_MATCH_DISABLED_PACKAGE_NAME_1 +
+ PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR +
+ EXPECTED_NO_MATCH_DISABLED_PACKAGE_NAME_3
+ )
+ val disabledAppsDataSource = DisabledAppsDataSourceImpl(
+ contentResolver,
+ packageManager,
+ bgDispatcher
+ )
+ val flows = mutableListOf<List<ResolveInfo>>()
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ disabledAppsDataSource.getDisabledApps().toList(flows)
+ }
+
+ advanceUntilIdle()
+ // Changes in Disabled app while the client is still subscribed to the changes.
+ Settings.Secure.putString(
+ contentResolver,
+ CarSettings.Secure.KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE,
+ EXPECTED_MATCH_DISABLED_PACKAGE_NAME_1 +
+ PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR +
+ EXPECTED_NO_MATCH_DISABLED_PACKAGE_NAME_3 +
+ PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR +
+ EXPECTED_MATCH_DISABLED_PACKAGE_NAME_2
+ )
+ advanceUntilIdle()
+ ShadowLooper.runUiThreadTasks()
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+
+ assertEquals(2, flows.size)
+ val actualUpdatedMatchedPackages =
+ flows[1].map { it.activityInfo.packageName }
+ assertEquals(
+ listOf(
+ EXPECTED_MATCH_DISABLED_PACKAGE_NAME_1,
+ EXPECTED_MATCH_DISABLED_PACKAGE_NAME_2
+ ),
+ actualUpdatedMatchedPackages
+ )
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testGetDisabledApps_scopeClosed_shouldCleanUp() = scope.runTest {
+ val contentResolverSpy = spy(contentResolver)
+ val disabledAppsDataSource = DisabledAppsDataSourceImpl(
+ contentResolverSpy,
+ packageManager,
+ bgDispatcher
+ )
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ disabledAppsDataSource.getDisabledApps().collect()
+ }
+
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+ advanceUntilIdle()
+
+ verify(contentResolverSpy).unregisterContentObserver(any())
+ }
+
+ companion object {
+ // packageNames listed in Settings.Secure for disabled apps
+ const val EXPECTED_MATCH_DISABLED_PACKAGE_NAME_1 = "com.test.example1"
+ const val EXPECTED_MATCH_DISABLED_PACKAGE_NAME_2 = "com.test.example2"
+ const val EXPECTED_NO_MATCH_DISABLED_PACKAGE_NAME_3 = "com.test.example3"
+
+ // componentNames available for the packageManager to query
+ val INSTALLED_COMPONENT_NAME_1 =
+ ComponentName(EXPECTED_MATCH_DISABLED_PACKAGE_NAME_1, "ExampleClass1")
+ val INSTALLED_COMPONENT_NAME_2 = ComponentName("com.test.example4", "ExampleClass2")
+ val INSTALLED_COMPONENT_NAME_3 =
+ ComponentName(EXPECTED_MATCH_DISABLED_PACKAGE_NAME_2, "ExampleClass3")
+ }
+}
diff --git a/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/restricted/TosAppsDataSourceImplTest.kt b/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/restricted/TosAppsDataSourceImplTest.kt
new file mode 100644
index 0000000..79d1530
--- /dev/null
+++ b/libs/appgrid/lib/robotests/src/com/android/car/carlauncher/datasources/restricted/TosAppsDataSourceImplTest.kt
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.datasources.restricted
+
+import android.car.settings.CarSettings
+import android.car.settings.CarSettings.Secure.KEY_USER_TOS_ACCEPTED
+import android.content.ComponentName
+import android.content.ContentResolver
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.database.ContentObserver
+import android.provider.Settings
+import com.android.car.carlauncher.datasources.restricted.TosDataSourceImpl.Companion.TOS_ACCEPTED
+import com.android.car.carlauncher.datasources.restricted.TosDataSourceImpl.Companion.TOS_DISABLED_APPS_SEPARATOR
+import com.android.car.carlauncher.datasources.restricted.TosDataSourceImpl.Companion.TOS_NOT_ACCEPTED
+import com.android.car.carlauncher.datasources.restricted.TosDataSourceImpl.Companion.TOS_UNINITIALIZED
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertTrue
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.shadows.ShadowLooper
+
+@RunWith(RobolectricTestRunner::class)
+class TosAppsDataSourceImplTest {
+
+ private val scope = TestScope()
+ private val bgDispatcher =
+ StandardTestDispatcher(scope.testScheduler, name = "Background dispatcher")
+
+ private val packageManager: PackageManager = mock {
+ on {
+ queryIntentActivities(
+ any(), any<PackageManager.ResolveInfoFlags>()
+ )
+ } doReturn listOf(
+ getResolveInfo(INSTALLED_COMPONENT_NAME_1),
+ getResolveInfo(INSTALLED_COMPONENT_NAME_2),
+ getResolveInfo(INSTALLED_COMPONENT_NAME_3),
+ )
+ }
+
+ private val contentResolver: ContentResolver =
+ RuntimeEnvironment.getApplication().contentResolver
+
+ /**
+ * Returns a mocked ResolveInfo
+ * @param componentName packageName + className of the mocked [ActivityInfo]
+ */
+ private fun getResolveInfo(componentName: ComponentName): ResolveInfo {
+ return ResolveInfo().apply {
+ activityInfo = ActivityInfo().apply {
+ packageName = componentName.packageName
+ name = componentName.className
+ }
+ }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testGetTosState_tosAccepted_shouldReturnBlockAppsAsFalse() = scope.runTest {
+ Settings.Secure.putString(
+ contentResolver,
+ KEY_USER_TOS_ACCEPTED,
+ TOS_ACCEPTED
+ )
+ val tosDataSource = TosDataSourceImpl(contentResolver, packageManager, bgDispatcher)
+ val flows = mutableListOf<TosState>()
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ tosDataSource.getTosState().toList(flows)
+ }
+ advanceUntilIdle()
+ ShadowLooper.runUiThreadTasks()
+ advanceUntilIdle()
+
+ assertEquals(1, flows.size)
+ assertEquals(false, flows[0].shouldBlockTosApps)
+ assertTrue(flows[0].restrictedApps.isEmpty())
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testGetTosState_tosUnInitialized_shouldReturnBlockAppsAsFalse() = scope.runTest {
+ Settings.Secure.putString(
+ contentResolver,
+ KEY_USER_TOS_ACCEPTED,
+ TOS_UNINITIALIZED
+ )
+ val tosDataSource = TosDataSourceImpl(contentResolver, packageManager, bgDispatcher)
+ val flows = mutableListOf<TosState>()
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ tosDataSource.getTosState().toList(flows)
+ }
+ advanceUntilIdle()
+ ShadowLooper.runUiThreadTasks()
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+
+ assertEquals(1, flows.size)
+ assertEquals(false, flows[0].shouldBlockTosApps)
+ assertTrue(flows[0].restrictedApps.isEmpty())
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testGetTosState_tosNotAccepted_shouldReturnBlockAppsAsTrue() = scope.runTest {
+ Settings.Secure.putString(
+ contentResolver,
+ KEY_USER_TOS_ACCEPTED,
+ TOS_NOT_ACCEPTED
+ )
+ Settings.Secure.putString(
+ contentResolver,
+ CarSettings.Secure.KEY_UNACCEPTED_TOS_DISABLED_APPS,
+ "$EXPECTED_MATCH_TOS_DISABLED_PACKAGE_NAME_1$TOS_DISABLED_APPS_SEPARATOR" +
+ "$EXPECTED_NO_MATCH_TOS_DISABLED_PACKAGE_NAME_3$TOS_DISABLED_APPS_SEPARATOR" +
+ EXPECTED_MATCH_TOS_DISABLED_PACKAGE_NAME_2
+ )
+ val tosDataSource = TosDataSourceImpl(contentResolver, packageManager, bgDispatcher)
+ val flows = mutableListOf<TosState>()
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ tosDataSource.getTosState().toList(flows)
+ }
+ advanceUntilIdle()
+ ShadowLooper.runUiThreadTasks()
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+
+ assertEquals(1, flows.size)
+ assertEquals(true, flows[0].shouldBlockTosApps)
+ val actualTosDisabledApps = flows[0].restrictedApps.map { it.activityInfo.packageName }
+ assertEquals(
+ listOf(
+ EXPECTED_MATCH_TOS_DISABLED_PACKAGE_NAME_1,
+ EXPECTED_MATCH_TOS_DISABLED_PACKAGE_NAME_2
+ ),
+ actualTosDisabledApps
+ )
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testGetTosState_tosChangedToAccepted_shouldUnregisterObserver() = scope.runTest {
+ val contentResolverSpy = spy(contentResolver)
+ Settings.Secure.putString(
+ contentResolverSpy,
+ KEY_USER_TOS_ACCEPTED,
+ TOS_NOT_ACCEPTED
+ )
+ Settings.Secure.putString(
+ contentResolverSpy,
+ CarSettings.Secure.KEY_UNACCEPTED_TOS_DISABLED_APPS,
+ "$EXPECTED_MATCH_TOS_DISABLED_PACKAGE_NAME_1$TOS_DISABLED_APPS_SEPARATOR" +
+ "$EXPECTED_NO_MATCH_TOS_DISABLED_PACKAGE_NAME_3$TOS_DISABLED_APPS_SEPARATOR" +
+ EXPECTED_MATCH_TOS_DISABLED_PACKAGE_NAME_2
+ )
+ val tosDataSource = TosDataSourceImpl(contentResolverSpy, packageManager, bgDispatcher)
+ val flows = mutableListOf<TosState>()
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ tosDataSource.getTosState().toList(flows)
+ }
+ advanceUntilIdle()
+ // Tos state changed to Accepted
+ Settings.Secure.putString(
+ contentResolverSpy,
+ KEY_USER_TOS_ACCEPTED,
+ TOS_ACCEPTED
+ )
+ ShadowLooper.runUiThreadTasks()
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+
+ assertEquals(2, flows.size)
+ // Initially shouldBlockTosApps is expected to be true.
+ assertEquals(true, flows[0].shouldBlockTosApps)
+ // After change in TOS state to Accepted shouldBlockTosApps is expected to be false.
+ assertEquals(false, flows[1].shouldBlockTosApps)
+ val actualChangedTosDisabledApps = flows[1].restrictedApps.map {
+ it.activityInfo.packageName
+ }
+ assertTrue(actualChangedTosDisabledApps.isEmpty())
+ verify(contentResolverSpy).unregisterContentObserver(any())
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testGetTosState_tosChangedToNotAccepted_shouldNotUnregisterObserver() = scope.runTest {
+ val contentResolverSpy = spy(contentResolver)
+ Settings.Secure.putString(
+ contentResolverSpy,
+ KEY_USER_TOS_ACCEPTED,
+ TOS_UNINITIALIZED
+ )
+ val tosDataSource = TosDataSourceImpl(contentResolverSpy, packageManager, bgDispatcher)
+ val flows = mutableListOf<TosState>()
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ tosDataSource.getTosState().toList(flows)
+ }
+ advanceUntilIdle()
+ // Tos state changed to Not_Accepted
+ Settings.Secure.putString(
+ contentResolverSpy,
+ KEY_USER_TOS_ACCEPTED,
+ TOS_NOT_ACCEPTED
+ )
+ ShadowLooper.runUiThreadTasks()
+ advanceUntilIdle()
+ // Tos state updates the list of blocked apps.
+ Settings.Secure.putString(
+ contentResolverSpy,
+ CarSettings.Secure.KEY_UNACCEPTED_TOS_DISABLED_APPS,
+ "$EXPECTED_MATCH_TOS_DISABLED_PACKAGE_NAME_1$TOS_DISABLED_APPS_SEPARATOR" +
+ "$EXPECTED_NO_MATCH_TOS_DISABLED_PACKAGE_NAME_3$TOS_DISABLED_APPS_SEPARATOR" +
+ EXPECTED_MATCH_TOS_DISABLED_PACKAGE_NAME_2
+ )
+ ShadowLooper.runUiThreadTasks()
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+
+ // Three updates: 1-UnInitialized, 2-NotAccepted, 3-BlockedAppsChanged.
+ assertEquals(3, flows.size)
+ // Initially shouldBlockTosApps is expected to be false in NotInitialized state.
+ assertEquals(false, flows[0].shouldBlockTosApps)
+ assertTrue(flows[0].restrictedApps.isEmpty())
+ // After change in TOS state to NotAccepted shouldBlockTosApps is expected to be true.
+ assertEquals(true, flows[1].shouldBlockTosApps)
+ // Since the list of blocked apps is also updated we will received another update.
+ assertEquals(true, flows[2].shouldBlockTosApps)
+ val actualChangedTosDisabledApps = flows[2].restrictedApps.map {
+ it.activityInfo.packageName
+ }
+ // Updates the list of blocked apps.
+ assertEquals(
+ listOf(
+ EXPECTED_MATCH_TOS_DISABLED_PACKAGE_NAME_1,
+ EXPECTED_MATCH_TOS_DISABLED_PACKAGE_NAME_2
+ ),
+ actualChangedTosDisabledApps
+ )
+ // We should not unregister the contentObservers.
+ verify(contentResolverSpy, never()).unregisterContentObserver(any())
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testGetTosState_scopeClosed_shouldCleanUp() = scope.runTest {
+ val contentResolverSpy = spy(contentResolver)
+ Settings.Secure.putString(
+ contentResolverSpy,
+ KEY_USER_TOS_ACCEPTED,
+ TOS_UNINITIALIZED
+ )
+ val tosDataSource = TosDataSourceImpl(contentResolverSpy, packageManager, bgDispatcher)
+
+ launch(StandardTestDispatcher(testScheduler)) {
+ tosDataSource.getTosState().collect()
+ }
+ advanceUntilIdle()
+ coroutineContext.cancelChildren()
+ advanceUntilIdle()
+
+ val observerCapture = ArgumentCaptor.forClass(ContentObserver::class.java)
+ // registers same observer for two uri.
+ verify(contentResolverSpy, times(2))
+ .registerContentObserver(any(), any(), observerCapture.capture())
+ // After scope it closed it unregisters the content observer.
+ verify(contentResolverSpy).unregisterContentObserver(observerCapture.value)
+ }
+
+ companion object {
+ // packageNames listed in Settings.Secure for tos disabled apps
+ private const val EXPECTED_MATCH_TOS_DISABLED_PACKAGE_NAME_1 = "com.test.example1"
+ private const val EXPECTED_MATCH_TOS_DISABLED_PACKAGE_NAME_2 = "com.test.example2"
+ private const val EXPECTED_NO_MATCH_TOS_DISABLED_PACKAGE_NAME_3 = "com.test.example3"
+
+ // componentNames available for the packageManager to query
+ private val INSTALLED_COMPONENT_NAME_1 =
+ ComponentName(EXPECTED_MATCH_TOS_DISABLED_PACKAGE_NAME_1, "ExampleClass1")
+ private val INSTALLED_COMPONENT_NAME_2 =
+ ComponentName("com.test.example4", "ExampleClass2")
+ private val INSTALLED_COMPONENT_NAME_3 =
+ ComponentName(EXPECTED_MATCH_TOS_DISABLED_PACKAGE_NAME_2, "ExampleClass3")
+ }
+}
diff --git a/libs/appgrid/lib/robotests/src/shadows/ShadowMediaSource.kt b/libs/appgrid/lib/robotests/src/shadows/ShadowMediaSource.kt
new file mode 100644
index 0000000..69640b1
--- /dev/null
+++ b/libs/appgrid/lib/robotests/src/shadows/ShadowMediaSource.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 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 shadows
+
+import android.content.ComponentName
+import android.content.Context
+import com.android.car.media.common.source.MediaSource
+import org.robolectric.annotation.Implements
+
+@Implements(MediaSource::class)
+class ShadowMediaSource {
+ companion object {
+ private val mediaTemplateComponents = mutableSetOf<ComponentName>()
+ private val customComponents = mutableSetOf<ComponentName>()
+
+ @JvmStatic
+ fun isMediaTemplate(context: Context, mbsComponentName: ComponentName): Boolean {
+ return mediaTemplateComponents.contains(mbsComponentName)
+ }
+
+ @JvmStatic
+ fun isAudioMediaSource(context: Context, mbsComponentName: ComponentName): Boolean {
+ return customComponents.contains(mbsComponentName) ||
+ mediaTemplateComponents.contains(mbsComponentName)
+ }
+
+ fun setMediaTemplates(mediaTemplateComponents: List<ComponentName>) {
+ this.mediaTemplateComponents.clear()
+ this.mediaTemplateComponents.addAll(mediaTemplateComponents)
+ }
+
+ fun setCustomTemplates(customComponents: List<ComponentName>) {
+ this.customComponents.clear()
+ this.customComponents.addAll(customComponents)
+ }
+ }
+}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/AppGridActivity.java b/libs/appgrid/lib/src/com/android/car/carlauncher/AppGridActivity.java
index 7d24e6b..4f6827b 100644
--- a/libs/appgrid/lib/src/com/android/car/carlauncher/AppGridActivity.java
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/AppGridActivity.java
@@ -16,343 +16,47 @@
package com.android.car.carlauncher;
-import static android.car.settings.CarSettings.Secure.KEY_UNACCEPTED_TOS_DISABLED_APPS;
-import static android.car.settings.CarSettings.Secure.KEY_USER_TOS_ACCEPTED;
-import static android.content.Intent.URI_INTENT_SCHEME;
+import static com.android.car.carlauncher.AppGridFragment.MODE_INTENT_EXTRA;
-import static com.android.car.carlauncher.AppGridConstants.AppItemBoundDirection;
-import static com.android.car.carlauncher.AppGridConstants.PageOrientation;
-import static com.android.car.carlauncher.AppLauncherUtils.APP_TYPE_LAUNCHABLES;
-import static com.android.car.carlauncher.AppLauncherUtils.APP_TYPE_MEDIA_SERVICES;
-import static com.android.car.carlauncher.hidden.HiddenApiAccess.getDragSurface;
-
-import android.animation.ValueAnimator;
-import android.app.AlertDialog;
-import android.app.usage.UsageStats;
-import android.app.usage.UsageStatsManager;
-import android.car.Car;
-import android.car.CarNotConnectedException;
-import android.car.content.pm.CarPackageManager;
-import android.car.drivingstate.CarUxRestrictions;
-import android.car.drivingstate.CarUxRestrictionsManager;
-import android.car.media.CarMediaManager;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.ServiceConnection;
-import android.content.pm.LauncherApps;
-import android.content.pm.PackageManager;
-import android.database.ContentObserver;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
-import android.os.Messenger;
-import android.os.RemoteException;
-import android.provider.Settings;
-import android.text.TextUtils;
-import android.text.format.DateUtils;
-import android.util.Log;
-import android.view.DragEvent;
-import android.view.SurfaceControl;
-import android.view.View;
-import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.StringRes;
-import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AppCompatActivity;
-import androidx.lifecycle.Observer;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.recyclerview.widget.RecyclerView;
+import androidx.fragment.app.Fragment;
-import com.android.car.carlauncher.AppLauncherUtils.LauncherAppsInfo;
-import com.android.car.carlauncher.pagination.PageMeasurementHelper;
-import com.android.car.carlauncher.pagination.PaginationController;
-import com.android.car.carlauncher.recyclerview.AppGridAdapter;
-import com.android.car.carlauncher.recyclerview.AppGridItemAnimator;
-import com.android.car.carlauncher.recyclerview.AppGridLayoutManager;
-import com.android.car.carlauncher.recyclerview.AppItemViewHolder;
-import com.android.car.ui.AlertDialogBuilder;
-import com.android.car.ui.FocusArea;
-import com.android.car.ui.baselayout.Insets;
-import com.android.car.ui.baselayout.InsetsChangedListener;
+import com.android.car.carlauncher.AppGridFragment.Mode;
import com.android.car.ui.core.CarUi;
-import com.android.car.ui.shortcutspopup.CarUiShortcutsPopup;
import com.android.car.ui.toolbar.MenuItem;
import com.android.car.ui.toolbar.NavButtonMode;
import com.android.car.ui.toolbar.ToolbarController;
-import java.net.URISyntaxException;
-import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
/**
* Launcher activity that shows a grid of apps.
*/
-public class AppGridActivity extends AppCompatActivity implements InsetsChangedListener,
- AppGridPageSnapper.PageSnapListener, AppItemViewHolder.AppItemDragListener,
- AppLauncherUtils.ShortcutsListener, PaginationController.DimensionUpdateListener {
+public class AppGridActivity extends AppCompatActivity {
private static final String TAG = "AppGridActivity";
+ boolean mShowToolbar = false;
+ boolean mShowAllApps = true;
private static final boolean DEBUG_BUILD = false;
- private static final String MODE_INTENT_EXTRA = "com.android.car.carlauncher.mode";
- private static CarUiShortcutsPopup sCarUiShortcutsPopup;
-
- private boolean mShowAllApps = true;
- private boolean mShowToolbar = true;
- private final Set<String> mHiddenApps = new HashSet<>();
- private PackageManager mPackageManager;
- private UsageStatsManager mUsageStatsManager;
- private AppInstallUninstallReceiver mInstallUninstallReceiver;
- private Car mCar;
- private CarUxRestrictionsManager mCarUxRestrictionsManager;
- private CarPackageManager mCarPackageManager;
- private CarMediaManager mCarMediaManager;
- private Mode mMode;
- private AlertDialog mStopAppAlertDialog;
- private LauncherAppsInfo mAppsInfo;
- private LauncherViewModel mLauncherModel;
- private AppGridAdapter mAdapter;
- private AppGridRecyclerView mRecyclerView;
- private PageIndicator mPageIndicator;
- private AppGridLayoutManager mLayoutManager;
- private boolean mIsCurrentlyDragging;
- private long mOffPageHoverBeforeScrollMs;
- private Banner mBanner;
-
- private AppGridDragController mAppGridDragController;
- private PaginationController mPaginationController;
-
- private int mNumOfRows;
- private int mNumOfCols;
- private int mAppGridMarginHorizontal;
- private int mAppGridMarginVertical;
- private int mAppGridWidth;
- private int mAppGridHeight;
- @PageOrientation
- private int mPageOrientation;
-
- private int mCurrentScrollOffset;
- private int mCurrentScrollState;
- private int mNextScrollDestination;
- private RecyclerView.ItemDecoration mPageMarginDecorator;
- private AppGridPageSnapper.AppGridPageSnapCallback mSnapCallback;
- private AppItemViewHolder.AppItemDragCallback mDragCallback;
-
- private Messenger mMirroringService;
- private Messenger mMessenger;
- private String mMirroringPackageName;
- private Intent mMirroringIntentRedirect;
- @VisibleForTesting
- ContentObserver mTosContentObserver;
- @VisibleForTesting
- ContentObserver mTosDisabledAppsContentObserver;
-
- /**
- * enum to define the state of display area possible.
- * CONTROL_BAR state is when only control bar is visible.
- * FULL state is when display area hosting default apps cover the screen fully.
- * DEFAULT state where maps are shown above DA for default apps.
- */
- public enum CAR_LAUNCHER_STATE {
- CONTROL_BAR, DEFAULT, FULL
- }
-
- public enum Mode {
- ALL_APPS(R.string.app_launcher_title_all_apps,
- APP_TYPE_LAUNCHABLES + APP_TYPE_MEDIA_SERVICES,
- true),
- MEDIA_ONLY(R.string.app_launcher_title_media_only,
- APP_TYPE_MEDIA_SERVICES,
- true),
- MEDIA_POPUP(R.string.app_launcher_title_media_only,
- APP_TYPE_MEDIA_SERVICES,
- false),
- ;
- public final @StringRes int mTitleStringId;
- public final @AppLauncherUtils.AppTypes int mAppTypes;
- public final boolean mOpenMediaCenter;
-
- Mode(@StringRes int titleStringId, @AppLauncherUtils.AppTypes int appTypes,
- boolean openMediaCenter) {
- mTitleStringId = titleStringId;
- mAppTypes = appTypes;
- mOpenMediaCenter = openMediaCenter;
- }
- }
-
- private ServiceConnection mCarConnectionListener = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- try {
- mCarUxRestrictionsManager = (CarUxRestrictionsManager) mCar.getCarManager(
- Car.CAR_UX_RESTRICTION_SERVICE);
- CarUxRestrictions carUxRestrictions = mCarUxRestrictionsManager
- .getCurrentCarUxRestrictions();
- boolean isDistractionOptimizationRequired;
- if (carUxRestrictions == null) {
- Log.v(TAG, "No CarUxRestrictions on display");
- isDistractionOptimizationRequired = false;
- } else {
- isDistractionOptimizationRequired = carUxRestrictions
- .isRequiresDistractionOptimization();
- }
- mAdapter.setIsDistractionOptimizationRequired(isDistractionOptimizationRequired);
- mAdapter.setMode(mMode);
- // set listener to update the app grid components and apply interaction restrictions
- // when driving state changes
- mCarUxRestrictionsManager.registerListener(restrictionInfo -> {
- handleDistractionOptimization(/* requiresDistractionOptimization */
- restrictionInfo.isRequiresDistractionOptimization());
- });
- mCarPackageManager = (CarPackageManager) mCar.getCarManager(Car.PACKAGE_SERVICE);
- mCarMediaManager = (CarMediaManager) mCar.getCarManager(Car.CAR_MEDIA_SERVICE);
- reinitializeLauncherModel();
- } catch (CarNotConnectedException e) {
- Log.e(TAG, "Car not connected in CarConnectionListener", e);
- }
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- mCarUxRestrictionsManager = null;
- mCarPackageManager = null;
- }
- };
-
- /**
- * Updates the state of the app grid components depending on the driving state.
- */
- private void handleDistractionOptimization(boolean requiresDistractionOptimization) {
- mAdapter.setIsDistractionOptimizationRequired(requiresDistractionOptimization);
- if (requiresDistractionOptimization) {
- // if the user start driving while drag is in action, we cancel existing drag operations
- if (mIsCurrentlyDragging) {
- mIsCurrentlyDragging = false;
- mLayoutManager.setShouldLayoutChildren(true);
- mRecyclerView.cancelDragAndDrop();
- }
- dismissForceStopMenus();
- }
- }
-
- private void reinitializeLauncherModel() {
- ExecutorService fetchOrderExecutorService = Executors.newSingleThreadExecutor();
- fetchOrderExecutorService.execute(() -> {
- // first, we fetch apps order from data store file into memory
- mLauncherModel.loadAppsOrderFromFile();
- fetchOrderExecutorService.shutdown();
- });
- ExecutorService alphabetizeExecutorService = Executors.newSingleThreadExecutor();
- alphabetizeExecutorService.execute(() -> {
- Set<String> appsToHide = mShowAllApps ? Collections.emptySet() : mHiddenApps;
- mAppsInfo = AppLauncherUtils.getLauncherApps(getApplicationContext(),
- appsToHide,
- mMode.mAppTypes,
- mMode.mOpenMediaCenter,
- getSystemService(LauncherApps.class),
- mCarPackageManager,
- mPackageManager,
- mCarMediaManager,
- AppGridActivity.this,
- mMirroringPackageName,
- mMirroringIntentRedirect);
- // then, we ingest all apps info
- mLauncherModel.processAppsInfoFromPlatform(mAppsInfo);
- alphabetizeExecutorService.shutdown();
- });
- }
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// TODO (b/267548246) deprecate toolbar and find another way to hide debug apps
- mShowToolbar = false;
if (mShowToolbar) {
setTheme(R.style.Theme_Launcher_AppGridActivity);
} else {
setTheme(R.style.Theme_Launcher_AppGridActivity_NoToolbar);
}
super.onCreate(savedInstanceState);
-
- mPackageManager = getPackageManager();
- mUsageStatsManager = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
- mLauncherModel = new ViewModelProvider(AppGridActivity.this,
- new LauncherViewModelFactory(getFilesDir())).get(
- LauncherViewModel.class);
- mLauncherModel.getCurrentLauncher().observe(
- AppGridActivity.this, new Observer<List<LauncherItem>>() {
- @Override
- public void onChanged(List<LauncherItem> launcherItems) {
- mAdapter.setLauncherItems(launcherItems);
- mNextScrollDestination = mSnapCallback.getSnapPosition();
- updateScrollState();
- if (mMode == Mode.ALL_APPS) {
- mLauncherModel.handleAppListChange();
- }
- }
- }
- );
- mCar = Car.createCar(this, mCarConnectionListener);
- mHiddenApps.addAll(Arrays.asList(getResources().getStringArray(R.array.hidden_apps)));
- setContentView(R.layout.app_grid_activity);
- updateMode();
-
- ServiceConnection mirroringConnectionListener = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- Log.d(TAG, "Mirroring service connected");
- mMirroringService = new Messenger(service);
- mMessenger = new Messenger(new IncomingHandler(Looper.getMainLooper()));
- Message msg = Message.obtain(null, getResources()
- .getInteger(R.integer.config_msg_register_mirroring_pkg_code));
- msg.replyTo = mMessenger;
- try {
- mMirroringService.send(msg);
- } catch (RemoteException e) {
- Log.d(TAG, "Exception sending message to mirroring service: " + e);
- }
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- Log.d(TAG, "Mirroring service disconnected");
- mMirroringPackageName = null;
- mMirroringIntentRedirect = null;
- }
- };
-
- // Bind to service that will inform about apps that are being mirrored
- try {
- Intent intent = new Intent();
- intent.setComponent(new ComponentName(
- getString(R.string.config_msg_mirroring_service_pkg_name),
- getString(R.string.config_msg_mirroring_service_class_name)));
- if (mPackageManager.resolveService(intent, /* flags = */ 0) != null) {
- bindService(intent, mirroringConnectionListener,
- BIND_AUTO_CREATE | BIND_IMPORTANT);
- }
- } catch (SecurityException e) {
- Log.e(TAG, "Error binding to mirroring service: " + e);
- }
+ setContentView(R.layout.app_grid_container_activity);
if (mShowToolbar) {
ToolbarController toolbar = CarUi.requireToolbar(this);
-
toolbar.setNavButtonMode(NavButtonMode.CLOSE);
-
if (DEBUG_BUILD) {
toolbar.setMenuItems(Collections.singletonList(MenuItem.builder(this)
.setDisplayBehavior(MenuItem.DisplayBehavior.NEVER)
@@ -366,132 +70,23 @@
.build()));
}
}
-
- mSnapCallback = new AppGridPageSnapper.AppGridPageSnapCallback(this);
- mDragCallback = new AppItemViewHolder.AppItemDragCallback(this);
-
- mNumOfCols = getResources().getInteger(R.integer.car_app_selector_column_number);
- mNumOfRows = getResources().getInteger(R.integer.car_app_selector_row_number);
- mAppGridDragController = new AppGridDragController();
- mOffPageHoverBeforeScrollMs = getResources().getInteger(
- R.integer.ms_off_page_hover_before_scroll);
-
- mPageOrientation = getResources().getBoolean(R.bool.use_vertical_app_grid)
- ? PageOrientation.VERTICAL : PageOrientation.HORIZONTAL;
-
- mRecyclerView = requireViewById(R.id.apps_grid);
- mRecyclerView.setFocusable(false);
- mLayoutManager = new AppGridLayoutManager(this, mNumOfCols, mNumOfRows, mPageOrientation);
- mRecyclerView.setLayoutManager(mLayoutManager);
-
- AppGridPageSnapper pageSnapper = new AppGridPageSnapper(
- this,
- mNumOfCols,
- mNumOfRows,
- mSnapCallback);
- pageSnapper.attachToRecyclerView(mRecyclerView);
-
- mRecyclerView.setItemAnimator(new AppGridItemAnimator());
-
- // hide the default scrollbar and replace it with a visual page indicator
- mRecyclerView.setVerticalScrollBarEnabled(false);
- mRecyclerView.setHorizontalScrollBarEnabled(false);
- mRecyclerView.addOnScrollListener(new AppGridOnScrollListener());
-
- // TODO: (b/271637411) move this to be contained in a scroll controller
- mPageIndicator = requireViewById(R.id.page_indicator);
- FrameLayout pageIndicatorContainer = requireViewById(R.id.page_indicator_container);
- mPageIndicator.setContainer(pageIndicatorContainer);
-
- // recycler view is set to LTR to prevent layout manager from reassigning layout direction.
- // instead, PageIndexinghelper will determine the grid index based on the system layout
- // direction and provide LTR mapping at adapter level.
- mRecyclerView.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
- pageIndicatorContainer.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
-
- // we create but do not attach the adapter to recyclerview until view tree layout is
- // complete and the total size of the app grid is measureable.
- mAdapter = new AppGridAdapter(this, mNumOfCols, mNumOfRows,
- /* dataModel */ mLauncherModel, /* dragCallback */ mDragCallback,
- /* snapCallback */ mSnapCallback);
- mAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
- @Override
- public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
- // scroll state will need to be updated after item has been dropped
- mNextScrollDestination = mSnapCallback.getSnapPosition();
- updateScrollState();
- }
- });
- mRecyclerView.setAdapter(mAdapter);
-
- // set drag listener and global layout listener, which will dynamically adjust app grid
- // height and width depending on device screen size.
- if (getResources().getBoolean(R.bool.config_allow_reordering)) {
- mRecyclerView.setOnDragListener(new AppGridDragListener());
- }
-
- // since some measurements for window size may not be available yet during onCreate or may
- // later change, we add a listener that redraws the app grid when window size changes.
- LinearLayout windowBackground = requireViewById(R.id.apps_grid_background);
- windowBackground.setOrientation(
- isHorizontal() ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL);
- PaginationController.DimensionUpdateCallback dimensionUpdateCallback =
- new PaginationController.DimensionUpdateCallback();
- dimensionUpdateCallback.addListener(mRecyclerView);
- dimensionUpdateCallback.addListener(mPageIndicator);
- dimensionUpdateCallback.addListener(this);
- mPaginationController = new PaginationController(windowBackground, dimensionUpdateCallback);
-
- mBanner = requireViewById(R.id.tos_banner);
- updateTosBanner();
-
- setupContentObserversForTos();
+ getSupportFragmentManager().beginTransaction().replace(R.id.fragmentContainer,
+ AppGridFragment.newInstance(parseMode(getIntent()))).commit();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
- updateMode();
- if (mCar.isConnected()) {
- reinitializeLauncherModel();
- }
- }
-
- @Override
- protected void onDestroy() {
- if (mCar != null && mCar.isConnected()) {
- mCar.disconnect();
- mCar = null;
- }
-
- if (mMirroringService != null) {
- Message msg = Message.obtain(null,
- getResources().getInteger(R.integer.config_msg_unregister_mirroring_pkg_code));
- msg.replyTo = mMessenger;
- try {
- mMirroringService.send(msg);
- } catch (RemoteException e) {
- Log.d(TAG, "Exception sending message to mirroring service: " + e);
- }
- }
-
- unregisterContentObserversForTos();
-
- super.onDestroy();
- }
-
- private void updateMode() {
- mMode = parseMode(getIntent());
- setTitle(mMode.mTitleStringId);
+ Mode mode = parseMode(intent);
+ setTitle(mode.getTitleStringId());
if (mShowToolbar) {
- CarUi.requireToolbar(this).setTitle(mMode.mTitleStringId);
+ CarUi.requireToolbar(this).setTitle(mode.getTitleStringId());
}
- }
-
- @VisibleForTesting
- boolean isHorizontal() {
- return AppGridConstants.isHorizontal(mPageOrientation);
+ Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragmentContainer);
+ if (fragment instanceof AppGridFragment) {
+ ((AppGridFragment) fragment).updateMode(mode);
+ }
}
/**
@@ -508,517 +103,4 @@
}
}
- @Override
- protected void onResume() {
- super.onResume();
- updateTosBannerVisibility();
- updateScrollState();
- mAdapter.setLayoutDirection(getResources().getConfiguration().getLayoutDirection());
- }
-
- @Override
- public void onDimensionsUpdated(PageMeasurementHelper.PageDimensions pageDimens,
- PageMeasurementHelper.GridDimensions gridDimens) {
- // TODO(b/271637411): move this method into a scroll controller
- mAppGridMarginHorizontal = pageDimens.marginHorizontalPx;
- mAppGridMarginVertical = pageDimens.marginVerticalPx;
- mAppGridWidth = gridDimens.gridWidthPx;
- mAppGridHeight = gridDimens.gridHeightPx;
- }
-
- /**
- * Updates the scroll state after receiving data changes, such as new apps being added or
- * reordered, and when user returns to launcher onResume.
- *
- * Additionally, notify page indicator to handle resizing in case new app addition creates a
- * new page or deleted a page.
- */
- void updateScrollState() {
- // TODO(b/271637411): move this method into a scroll controller
- // to calculate how many pages we need to offset, we use the scroll offset anchor position
- // as item count and map to the page which the anchor is on.
- int offsetPageCount = mAdapter.getPageCount(mNextScrollDestination + 1) - 1;
- mRecyclerView.suppressLayout(false);
- mCurrentScrollOffset = offsetPageCount * (isHorizontal()
- ? (mAppGridWidth + 2 * mAppGridMarginHorizontal)
- : (mAppGridHeight + 2 * mAppGridMarginVertical));
- mLayoutManager.scrollToPositionWithOffset(/* position */
- offsetPageCount * mNumOfRows * mNumOfCols, /* offset */ 0);
-
- mPageIndicator.updateOffset(mCurrentScrollOffset);
- mPageIndicator.updatePageCount(mAdapter.getPageCount());
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- // register broadcast receiver for package installation and uninstallation
- mInstallUninstallReceiver = new AppInstallUninstallReceiver();
- IntentFilter filter = new IntentFilter();
- filter.addAction(Intent.ACTION_PACKAGE_ADDED);
- filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
- filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
- filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
- filter.addDataScheme("package");
- registerReceiver(mInstallUninstallReceiver, filter);
-
- // Connect to car service
- mCar.connect();
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- // disconnect from app install/uninstall receiver
- if (mInstallUninstallReceiver != null) {
- unregisterReceiver(mInstallUninstallReceiver);
- mInstallUninstallReceiver = null;
- }
- // disconnect from car listeners
- try {
- if (mCarUxRestrictionsManager != null) {
- mCarUxRestrictionsManager.unregisterListener();
- }
- } catch (CarNotConnectedException e) {
- Log.e(TAG, "Error unregistering listeners", e);
- }
- if (mCar != null) {
- mCar.disconnect();
- }
- }
-
- @Override
- protected void onPause() {
- dismissForceStopMenus();
- super.onPause();
- }
-
- @Override
- public void onSnapToPosition(int position) {
- mNextScrollDestination = position;
- }
-
- @Override
- public void onItemLongPressed(boolean isLongPressed) {
- // after the user long presses the app icon, scrolling should be disabled until long press
- // is canceled as to allow MotionEvent to be interpreted as attempt to drag the app icon.
- mRecyclerView.suppressLayout(isLongPressed);
- }
-
- @Override
- public void onItemSelected(int gridPositionFrom) {
- mIsCurrentlyDragging = true;
- mLayoutManager.setShouldLayoutChildren(false);
- mAdapter.setDragStartPoint(gridPositionFrom);
- dismissShortcutPopup();
- }
-
- @Override
- public void onItemDragged() {
- mAppGridDragController.cancelDelayedPageFling();
- }
-
- @Override
- public void onDragExited(int gridPosition, @AppItemBoundDirection int exitDirection) {
- if (mAdapter.getOffsetBoundDirection(gridPosition) == exitDirection) {
- mAppGridDragController.postDelayedPageFling(exitDirection);
- }
- }
-
- @Override
- public void onItemDropped(int gridPositionFrom, int gridPositionTo) {
- mLayoutManager.setShouldLayoutChildren(true);
- mAdapter.moveAppItem(gridPositionFrom, gridPositionTo);
- }
-
- /**
- * Note that in order to obtain usage stats from the previous boot,
- * the device must have gone through a clean shut down process.
- */
- private List<AppMetaData> getMostRecentApps(LauncherAppsInfo appsInfo) {
- ArrayList<AppMetaData> apps = new ArrayList<>();
- if (appsInfo.isEmpty()) {
- return apps;
- }
-
- // get the usage stats starting from 1 year ago with a INTERVAL_YEARLY granularity
- // returning entries like:
- // "During 2017 App A is last used at 2017/12/15 18:03"
- // "During 2017 App B is last used at 2017/6/15 10:00"
- // "During 2018 App A is last used at 2018/1/1 15:12"
- List<UsageStats> stats =
- mUsageStatsManager.queryUsageStats(
- UsageStatsManager.INTERVAL_YEARLY,
- System.currentTimeMillis() - DateUtils.YEAR_IN_MILLIS,
- System.currentTimeMillis());
-
- if (stats == null || stats.size() == 0) {
- return apps; // empty list
- }
-
- stats.sort(new LastTimeUsedComparator());
-
- int currentIndex = 0;
- int itemsAdded = 0;
- int statsSize = stats.size();
- int itemCount = Math.min(mNumOfCols, statsSize);
- while (itemsAdded < itemCount && currentIndex < statsSize) {
- UsageStats usageStats = stats.get(currentIndex);
- String packageName = usageStats.getPackageName();
- currentIndex++;
-
- // do not include self
- if (packageName.equals(getPackageName())) {
- continue;
- }
-
- // TODO(b/136222320): UsageStats is obtained per package, but a package may contain
- // multiple media services. We need to find a way to get the usage stats per service.
- ComponentName componentName = AppLauncherUtils.getMediaSource(mPackageManager,
- packageName);
- // Exempt media services from background and launcher checks
- if (!appsInfo.isMediaService(componentName)) {
- // do not include apps that only ran in the background
- if (usageStats.getTotalTimeInForeground() == 0) {
- continue;
- }
-
- // do not include apps that don't support starting from launcher
- Intent intent = getPackageManager().getLaunchIntentForPackage(packageName);
- if (intent == null || !intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
- continue;
- }
- }
-
- AppMetaData app = appsInfo.getAppMetaData(componentName);
- // Prevent duplicated entries
- // e.g. app is used at 2017/12/31 23:59, and 2018/01/01 00:00
- if (app != null && !apps.contains(app)) {
- apps.add(app);
- itemsAdded++;
- }
- }
- return apps;
- }
-
- @Override
- public void onCarUiInsetsChanged(Insets insets) {
- requireViewById(R.id.apps_grid)
- .setPadding(0, insets.getTop(), 0, insets.getBottom());
- FocusArea focusArea = requireViewById(R.id.focus_area);
- focusArea.setHighlightPadding(0, insets.getTop(), 0, insets.getBottom());
- focusArea.setBoundsOffset(0, insets.getTop(), 0, insets.getBottom());
-
- requireViewById(android.R.id.content)
- .setPadding(insets.getLeft(), 0, insets.getRight(), 0);
- }
-
- @Override
- public void onShortcutsShow(CarUiShortcutsPopup carUiShortcutsPopup) {
- sCarUiShortcutsPopup = carUiShortcutsPopup;
- }
-
- @Override
- public void onShortcutsItemClick(String packageName, CharSequence displayName,
- boolean allowStopApp) {
- AlertDialogBuilder builder = new AlertDialogBuilder(this)
- .setTitle(R.string.app_launcher_stop_app_dialog_title);
-
- if (allowStopApp) {
- builder.setMessage(R.string.app_launcher_stop_app_dialog_text)
- .setPositiveButton(android.R.string.ok,
- (d, w) -> AppLauncherUtils.forceStop(packageName, AppGridActivity.this,
- displayName, mCarMediaManager, mAppsInfo.getMediaServices(),
- this))
- .setNegativeButton(android.R.string.cancel, /* onClickListener= */ null);
- } else {
- builder.setMessage(R.string.app_launcher_stop_app_cant_stop_text)
- .setNeutralButton(android.R.string.ok, /* onClickListener= */ null);
- }
- mStopAppAlertDialog = builder.show();
- }
-
- @Override
- public void onStopAppSuccess(String message) {
- Toast.makeText(this, message, Toast.LENGTH_LONG).show();
- }
-
- private void dismissShortcutPopup() {
- // TODO (b/268563442): shortcut popup is set to be static since its
- // sometimes recreated when taskview is present, find out why
- if (sCarUiShortcutsPopup != null) {
- sCarUiShortcutsPopup.dismiss();
- sCarUiShortcutsPopup = null;
- }
- }
-
- private void dismissForceStopMenus() {
- if (sCarUiShortcutsPopup != null) {
- sCarUiShortcutsPopup.dismissImmediate();
- sCarUiShortcutsPopup = null;
- }
- if (mStopAppAlertDialog != null) {
- mStopAppAlertDialog.dismiss();
- }
- }
-
- /**
- * Comparator for {@link UsageStats} that sorts the list by the "last time used" property
- * in descending order.
- */
- private static class LastTimeUsedComparator implements Comparator<UsageStats> {
- @Override
- public int compare(UsageStats stat1, UsageStats stat2) {
- Long time1 = stat1.getLastTimeUsed();
- Long time2 = stat2.getLastTimeUsed();
- return time2.compareTo(time1);
- }
- }
-
- private class AppInstallUninstallReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- String packageName = intent.getData().getSchemeSpecificPart();
- if (TextUtils.isEmpty(packageName)) {
- Log.e(TAG, "System sent an empty app install/uninstall broadcast");
- return;
- }
- // TODO b/256684061: find better way to get AppInfo from package name.
- reinitializeLauncherModel();
- }
- }
-
- private class AppGridOnScrollListener extends RecyclerView.OnScrollListener {
- @Override
- public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
- mCurrentScrollOffset = mCurrentScrollOffset + (isHorizontal() ? dx : dy);
- mPageIndicator.updateOffset(mCurrentScrollOffset);
- }
-
- @Override
- public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
- mCurrentScrollState = newState;
- mSnapCallback.setScrollState(mCurrentScrollState);
- switch (newState) {
- case RecyclerView.SCROLL_STATE_DRAGGING:
- if (!mIsCurrentlyDragging) {
- mDragCallback.cancelDragTasks();
- }
- dismissShortcutPopup();
- mPageIndicator.animateAppearance();
- break;
-
- case RecyclerView.SCROLL_STATE_SETTLING:
- mPageIndicator.animateAppearance();
- break;
-
- case RecyclerView.SCROLL_STATE_IDLE:
- if (mIsCurrentlyDragging) {
- mLayoutManager.setShouldLayoutChildren(false);
- }
- mPageIndicator.animateFading();
- // in case the recyclerview was scrolled by rotary input, we need to handle
- // focusing the correct element: either on the first or last element on page
- mRecyclerView.maybeHandleRotaryFocus();
- }
- }
- }
-
- private class AppGridDragController {
- // TODO: (b/271320404) move DragController to separate directory called dragndrop and
- // migrate logic this class and AppItemViewHolder there.
- private final Handler mHandler;
-
- AppGridDragController() {
- mHandler = new Handler(getMainLooper());
- }
-
- void cancelDelayedPageFling() {
- mHandler.removeCallbacksAndMessages(null);
- }
-
- void postDelayedPageFling(@AppItemBoundDirection int exitDirection) {
- boolean scrollToNextPage = isHorizontal()
- ? exitDirection == AppItemBoundDirection.RIGHT
- : exitDirection == AppItemBoundDirection.BOTTOM;
- mHandler.removeCallbacksAndMessages(null);
- mHandler.postDelayed(new Runnable() {
- public void run() {
- if (mCurrentScrollState == RecyclerView.SCROLL_STATE_IDLE) {
- mAdapter.updatePageScrollDestination(scrollToNextPage);
- mNextScrollDestination = mSnapCallback.getSnapPosition();
-
- mLayoutManager.setShouldLayoutChildren(true);
- mRecyclerView.smoothScrollToPosition(mNextScrollDestination);
- }
- // another delayed scroll will be queued to enable the user to input multiple
- // page scrolls by holding the recyclerview at the app grid margin
- postDelayedPageFling(exitDirection);
- }
- }, mOffPageHoverBeforeScrollMs);
- }
- }
-
- /**
- * Private onDragListener for handling dispatching off page scroll event when user holds the app
- * icon at the page margin.
- */
- private class AppGridDragListener implements View.OnDragListener {
- @Override
- public boolean onDrag(View v, DragEvent event) {
- int action = event.getAction();
- if (action == DragEvent.ACTION_DROP || action == DragEvent.ACTION_DRAG_ENDED) {
- mIsCurrentlyDragging = false;
- mAppGridDragController.cancelDelayedPageFling();
- mDragCallback.resetCallbackState();
- mLayoutManager.setShouldLayoutChildren(true);
- if (action == DragEvent.ACTION_DROP) {
- return false;
- } else {
- animateDropEnded(getDragSurface(event));
- }
- }
- return true;
- }
- }
-
- private void animateDropEnded(@Nullable SurfaceControl dragSurface) {
- if (dragSurface == null) {
- Log.d(TAG, "animateDropEnded, dragSurface unavailable");
- return;
- }
- // update default animation for the drag shadow after user lifts their finger
- SurfaceControl.Transaction txn = new SurfaceControl.Transaction();
- // set an animator to animate a delay before clearing the dragSurface
- ValueAnimator delayedDismissAnimator = ValueAnimator.ofFloat(0f, 1f);
- delayedDismissAnimator.setStartDelay(
- getResources().getInteger(R.integer.ms_drop_animation_delay));
- delayedDismissAnimator.addUpdateListener(
- new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- txn.setAlpha(dragSurface, 0);
- txn.apply();
- }
- });
- delayedDismissAnimator.start();
- }
-
- private void updateTosBanner() {
- mBanner.setFirstButtonOnClickListener(v -> {
- Intent tosIntent = AppLauncherUtils.getIntentForTosAcceptanceFlow(v.getContext());
- AppLauncherUtils.launchApp(v.getContext(), tosIntent);
- });
- mBanner.setSecondButtonOnClickListener(
- v -> mBanner.setVisibility(View.GONE));
- }
-
- private void updateTosBannerVisibility() {
-
- if (AppLauncherUtils.showTosBanner(this)) {
- runOnUiThread(() -> {
- mBanner.setVisibility(View.VISIBLE);
- });
- } else {
- mBanner.setVisibility(View.GONE);
- }
- }
-
- private void setupContentObserversForTos() {
- if (AppLauncherUtils.tosStatusUninitialized(/* context = */ this)
- || !AppLauncherUtils.tosAccepted(/* context = */ this)) {
- Log.i(TAG, "TOS not accepted, setting up content observers for TOS state");
- } else {
- Log.i(TAG, "TOS accepted, state will remain accepted, "
- + "don't need to observe this value");
- return;
- }
- mTosContentObserver = new ContentObserver(new Handler()) {
- @Override
- public void onChange(boolean selfChange) {
- super.onChange(selfChange);
- boolean tosState = AppLauncherUtils.tosAccepted(getBaseContext());
- Log.i(TAG, "TOS state updated:" + tosState);
- reinitializeLauncherModel();
- if (tosState) {
- unregisterContentObserversForTos();
- }
- }
- };
- mTosDisabledAppsContentObserver = new ContentObserver(new Handler()) {
- @Override
- public void onChange(boolean selfChange) {
- super.onChange(selfChange);
- reinitializeLauncherModel();
- }
- };
- getContentResolver().registerContentObserver(
- Settings.Secure.getUriFor(KEY_USER_TOS_ACCEPTED),
- /* notifyForDescendants*/ false,
- mTosContentObserver);
- getContentResolver().registerContentObserver(
- Settings.Secure.getUriFor(KEY_UNACCEPTED_TOS_DISABLED_APPS),
- /* notifyForDescendants*/ false,
- mTosDisabledAppsContentObserver
- );
- }
-
- private void unregisterContentObserversForTos() {
- if (mTosContentObserver != null) {
- Log.i(TAG, "Unregister content observer for tos state");
- getContentResolver().unregisterContentObserver(mTosContentObserver);
- mTosContentObserver = null;
- }
- if (mTosDisabledAppsContentObserver != null) {
- Log.i(TAG, "Unregister content observer for tos disabled apps");
- getContentResolver().unregisterContentObserver(mTosDisabledAppsContentObserver);
- mTosDisabledAppsContentObserver = null;
- }
- }
-
- @VisibleForTesting
- void setCarUxRestrictionsManager(CarUxRestrictionsManager carUxRestrictionsManager) {
- mCarUxRestrictionsManager = carUxRestrictionsManager;
- }
-
- @VisibleForTesting
- void setPageIndicator(PageIndicator pageIndicator) {
- mPageIndicator = pageIndicator;
- }
-
- class IncomingHandler extends Handler {
-
- int mSendMirroringPkgCode = getResources()
- .getInteger(R.integer.config_msg_send_mirroring_pkg_code);
- String mMirroringPkgNameKey = getString(R.string.config_msg_mirroring_pkg_name_key);
- String mMirroringRedirectUriKey = getString(R.string.config_msg_mirroring_redirect_uri_key);
-
- IncomingHandler(Looper looper) {
- super(looper);
- }
-
- @Override
- public void handleMessage(Message msg) {
- Log.d(TAG, "Message received: " + msg);
- if (msg.what
- == mSendMirroringPkgCode) {
- Bundle bundle = (Bundle) msg.obj;
- mMirroringPackageName =
- bundle.getString(mMirroringPkgNameKey);
- Log.d(TAG, "message received with package name = " + mMirroringPackageName);
- try {
- mMirroringIntentRedirect = Intent.parseUri(
- bundle.getString(mMirroringRedirectUriKey),
- URI_INTENT_SCHEME);
- Log.d(TAG, "intent is: " + mMirroringIntentRedirect);
- mLauncherModel.updateMirroringItem(mMirroringPackageName,
- mMirroringIntentRedirect);
- } catch (URISyntaxException e) {
- Log.d(TAG, "Error parsing mirroring redirect intent " + e);
- }
- } else {
- super.handleMessage(msg);
- }
- }
- }
}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/AppGridFragment.kt b/libs/appgrid/lib/src/com/android/car/carlauncher/AppGridFragment.kt
new file mode 100644
index 0000000..62735c0
--- /dev/null
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/AppGridFragment.kt
@@ -0,0 +1,632 @@
+/*
+ * Copyright (C) 2024 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.carlauncher
+
+import android.animation.ValueAnimator
+import android.car.Car
+import android.car.content.pm.CarPackageManager
+import android.car.drivingstate.CarUxRestrictionsManager
+import android.car.media.CarMediaManager
+import android.content.BroadcastReceiver
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.LauncherApps
+import android.content.pm.PackageManager
+import android.media.session.MediaSessionManager
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper.getMainLooper
+import android.os.Process
+import android.os.UserManager
+import android.util.Log
+import android.view.DragEvent
+import android.view.LayoutInflater
+import android.view.SurfaceControl
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import androidx.annotation.StringRes
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.asLiveData
+import androidx.recyclerview.widget.RecyclerView
+import com.android.car.carlauncher.AppGridConstants.AppItemBoundDirection
+import com.android.car.carlauncher.AppGridConstants.PageOrientation
+import com.android.car.carlauncher.AppGridConstants.isHorizontal
+import com.android.car.carlauncher.AppGridFragment.AppTypes.Companion.APP_TYPE_LAUNCHABLES
+import com.android.car.carlauncher.AppGridFragment.AppTypes.Companion.APP_TYPE_MEDIA_SERVICES
+import com.android.car.carlauncher.AppGridPageSnapper.AppGridPageSnapCallback
+import com.android.car.carlauncher.AppGridPageSnapper.PageSnapListener
+import com.android.car.carlauncher.AppGridViewModel.Companion.provideFactory
+import com.android.car.carlauncher.datasources.AppOrderDataSource
+import com.android.car.carlauncher.datasources.AppOrderProtoDataSourceImpl
+import com.android.car.carlauncher.datasources.ControlCenterMirroringDataSource
+import com.android.car.carlauncher.datasources.ControlCenterMirroringDataSourceImpl
+import com.android.car.carlauncher.datasources.ControlCenterMirroringDataSourceImpl.MirroringServiceConnection
+import com.android.car.carlauncher.datasources.LauncherActivitiesDataSource
+import com.android.car.carlauncher.datasources.LauncherActivitiesDataSourceImpl
+import com.android.car.carlauncher.datasources.MediaTemplateAppsDataSource
+import com.android.car.carlauncher.datasources.MediaTemplateAppsDataSourceImpl
+import com.android.car.carlauncher.datasources.UXRestrictionDataSource
+import com.android.car.carlauncher.datasources.UXRestrictionDataSourceImpl
+import com.android.car.carlauncher.datasources.restricted.DisabledAppsDataSource
+import com.android.car.carlauncher.datasources.restricted.DisabledAppsDataSourceImpl
+import com.android.car.carlauncher.datasources.restricted.TosDataSource
+import com.android.car.carlauncher.datasources.restricted.TosDataSourceImpl
+import com.android.car.carlauncher.datastore.launcheritem.LauncherItemListSource
+import com.android.car.carlauncher.pagination.PageMeasurementHelper
+import com.android.car.carlauncher.pagination.PaginationController
+import com.android.car.carlauncher.pagination.PaginationController.DimensionUpdateCallback
+import com.android.car.carlauncher.pagination.PaginationController.DimensionUpdateListener
+import com.android.car.carlauncher.recyclerview.AppGridAdapter
+import com.android.car.carlauncher.recyclerview.AppGridAdapter.AppGridAdapterListener
+import com.android.car.carlauncher.recyclerview.AppGridItemAnimator
+import com.android.car.carlauncher.recyclerview.AppGridLayoutManager
+import com.android.car.carlauncher.recyclerview.AppItemViewHolder.AppItemDragCallback
+import com.android.car.carlauncher.recyclerview.AppItemViewHolder.AppItemDragListener
+import com.android.car.carlauncher.repositories.AppGridRepository
+import com.android.car.carlauncher.repositories.AppGridRepositoryImpl
+import com.android.car.carlauncher.repositories.appactions.AppLaunchProviderFactory
+import com.android.car.carlauncher.repositories.appactions.AppShortcutsFactory
+import com.android.car.carlauncher.repositories.appactions.AppShortcutsFactory.ShortcutsListener
+import com.android.car.hidden.apis.HiddenApiAccess
+import com.android.car.ui.shortcutspopup.CarUiShortcutsPopup
+import kotlinx.coroutines.Dispatchers.Default
+import kotlinx.coroutines.Dispatchers.IO
+
+/**
+ * Fragment which renders the Apps based on the [Mode] provided in the [setArguments]
+ *
+ * To create an instance of this Fragment use [newInstance]
+ */
+class AppGridFragment : Fragment(), PageSnapListener, AppItemDragListener, DimensionUpdateListener,
+ AppGridAdapterListener {
+
+ private lateinit var car: Car
+ private lateinit var mode: Mode
+ private lateinit var snapCallback: AppGridPageSnapCallback
+ private lateinit var dragCallback: AppItemDragCallback
+ private lateinit var appGridDragController: AppGridDragController
+ private lateinit var appGridRecyclerView: AppGridRecyclerView
+ private lateinit var layoutManager: AppGridLayoutManager
+ private lateinit var pageIndicator: PageIndicator
+ private lateinit var adapter: AppGridAdapter
+ private lateinit var paginationController: PaginationController
+ private lateinit var backgroundAnimationHelper: BackgroundAnimationHelper
+ private lateinit var appGridViewModel: AppGridViewModel
+ private lateinit var banner: Banner
+
+ private var appGridMarginHorizontal = 0
+ private var appGridMarginVertical = 0
+ private var appGridWidth = 0
+ private var appGridHeight = 0
+ private var offPageHoverBeforeScrollMs = 0L
+ private var numOfCols = 0
+ private var numOfRows = 0
+ private var nextScrollDestination = 0
+ private var currentScrollOffset = 0
+ private var currentScrollState = 0
+ private var isCurrentlyDragging = false
+ private var carUiShortcutsPopup: CarUiShortcutsPopup? = null
+
+ @PageOrientation
+ private var pageOrientation = 0
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ super.onCreateView(inflater, container, savedInstanceState)
+ return inflater.inflate(R.layout.app_grid_fragment, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ car = Car.createCar(requireContext()) ?: throw IllegalStateException("Car not initialized")
+ mode = Mode.valueOf(requireArguments().getString(MODE_INTENT_EXTRA, Mode.ALL_APPS.name))
+ initViewModel()
+ updateMode(mode)
+
+ snapCallback = AppGridPageSnapCallback(this)
+ dragCallback = AppItemDragCallback(this)
+
+ numOfCols = resources.getInteger(R.integer.car_app_selector_column_number)
+ numOfRows = resources.getInteger(R.integer.car_app_selector_row_number)
+ appGridDragController = AppGridDragController()
+ offPageHoverBeforeScrollMs = resources.getInteger(
+ R.integer.ms_off_page_hover_before_scroll
+ ).toLong()
+
+ pageOrientation =
+ if (resources.getBoolean(R.bool.use_vertical_app_grid)) {
+ PageOrientation.VERTICAL
+ } else {
+ PageOrientation.HORIZONTAL
+ }
+
+ appGridRecyclerView = view.requireViewById(R.id.apps_grid)
+ appGridRecyclerView.isFocusable = false
+ layoutManager =
+ AppGridLayoutManager(requireContext(), numOfCols, numOfRows, pageOrientation)
+ appGridRecyclerView.layoutManager = layoutManager
+
+ val pageSnapper = AppGridPageSnapper(
+ requireContext(),
+ numOfCols,
+ numOfRows,
+ snapCallback
+ )
+ pageSnapper.attachToRecyclerView(appGridRecyclerView)
+
+ appGridRecyclerView.itemAnimator = AppGridItemAnimator()
+
+ // hide the default scrollbar and replace it with a visual page indicator
+ appGridRecyclerView.isVerticalScrollBarEnabled = false
+ appGridRecyclerView.isHorizontalScrollBarEnabled = false
+ appGridRecyclerView.addOnScrollListener(AppGridOnScrollListener())
+
+ // TODO: (b/271637411) move this to be contained in a scroll controller
+ pageIndicator = view.requireViewById(R.id.page_indicator)
+ val pageIndicatorContainer: FrameLayout =
+ view.requireViewById(R.id.page_indicator_container)
+ pageIndicator.setContainer(pageIndicatorContainer)
+
+ // recycler view is set to LTR to prevent layout manager from reassigning layout direction.
+ // instead, PageIndexinghelper will determine the grid index based on the system layout
+ // direction and provide LTR mapping at adapter level.
+ appGridRecyclerView.layoutDirection = View.LAYOUT_DIRECTION_LTR
+ pageIndicatorContainer.layoutDirection = View.LAYOUT_DIRECTION_LTR
+
+ // we create but do not attach the adapter to recyclerview until view tree layout is
+ // complete and the total size of the app grid is measureable.
+ adapter = AppGridAdapter(
+ requireContext(), numOfCols, numOfRows, dragCallback, snapCallback, this, mode
+ )
+
+ adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
+ override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
+ // scroll state will need to be updated after item has been dropped
+ nextScrollDestination = snapCallback.snapPosition
+ updateScrollState()
+ }
+ })
+ appGridRecyclerView.adapter = adapter
+
+ appGridViewModel.getAppList().asLiveData().observe(
+ viewLifecycleOwner
+ ) { appItems: List<AppItem?>? ->
+ adapter.setLauncherItems(appItems)
+ nextScrollDestination = snapCallback.snapPosition
+ updateScrollState()
+ }
+
+ appGridViewModel.requiresDistractionOptimization().asLiveData().observe(
+ viewLifecycleOwner
+ ) { uxRestrictions: Boolean ->
+ handleDistractionOptimization(
+ uxRestrictions
+ )
+ }
+
+ // set drag listener and global layout listener, which will dynamically adjust app grid
+ // height and width depending on device screen size. ize.
+ if (resources.getBoolean(R.bool.config_allow_reordering)) {
+ appGridRecyclerView.setOnDragListener(AppGridDragListener())
+ }
+
+ // since some measurements for window size may not be available yet during onCreate or may
+ // later change, we add a listener that redraws the app grid when window size changes.
+ val windowBackground: LinearLayout = view.requireViewById(R.id.apps_grid_background)
+ windowBackground.orientation =
+ if (isHorizontal(pageOrientation)) LinearLayout.VERTICAL else LinearLayout.HORIZONTAL
+ val dimensionUpdateCallback = DimensionUpdateCallback()
+ dimensionUpdateCallback.addListener(appGridRecyclerView)
+ dimensionUpdateCallback.addListener(pageIndicator)
+ dimensionUpdateCallback.addListener(this)
+ paginationController = PaginationController(windowBackground, dimensionUpdateCallback)
+
+ banner = view.requireViewById(R.id.tos_banner)
+
+ backgroundAnimationHelper = BackgroundAnimationHelper(windowBackground, banner)
+
+ setupTosBanner()
+ }
+
+ /**
+ * Updates the state of the app grid components depending on the driving state.
+ */
+ private fun handleDistractionOptimization(requiresDistractionOptimization: Boolean) {
+ adapter.setIsDistractionOptimizationRequired(requiresDistractionOptimization)
+ if (requiresDistractionOptimization) {
+ // if the user start driving while drag is in action, we cancel existing drag operations
+ if (isCurrentlyDragging) {
+ isCurrentlyDragging = false
+ layoutManager.setShouldLayoutChildren(true)
+ appGridRecyclerView.cancelDragAndDrop()
+ }
+ dismissShortcutPopup()
+ }
+ }
+
+ private fun initViewModel() {
+ val launcherActivities: LauncherActivitiesDataSource = LauncherActivitiesDataSourceImpl(
+ requireContext().getSystemService(LauncherApps::class.java),
+ { broadcastReceiver: BroadcastReceiver?, intentFilter: IntentFilter? ->
+ requireContext().registerReceiver(broadcastReceiver, intentFilter)
+ },
+ { broadcastReceiver: BroadcastReceiver? ->
+ requireContext().unregisterReceiver(broadcastReceiver)
+ },
+ Process.myUserHandle(),
+ requireContext().applicationContext.resources,
+ Default
+ )
+ val mediaTemplateApps: MediaTemplateAppsDataSource = MediaTemplateAppsDataSourceImpl(
+ requireContext().packageManager,
+ requireContext().applicationContext,
+ Default
+ )
+ val disabledApps: DisabledAppsDataSource = DisabledAppsDataSourceImpl(
+ requireContext().contentResolver,
+ requireContext().packageManager,
+ IO
+ )
+ val tosApps: TosDataSource = TosDataSourceImpl(
+ requireContext().contentResolver,
+ requireContext().packageManager,
+ IO
+ )
+ val controlCenterMirroringDataSource: ControlCenterMirroringDataSource =
+ ControlCenterMirroringDataSourceImpl(
+ requireContext().applicationContext.resources,
+ { intent: Intent, serviceConnection: MirroringServiceConnection, flags: Int ->
+ requireContext().bindService(intent, serviceConnection, flags)
+ },
+ { serviceConnection: MirroringServiceConnection ->
+ requireContext().unbindService(serviceConnection)
+ },
+ requireContext().packageManager,
+ IO
+ )
+ val uxRestrictionDataSource: UXRestrictionDataSource = UXRestrictionDataSourceImpl(
+ requireContext(),
+ requireNotNull(car.getCarManager(CarUxRestrictionsManager::class.java)),
+ requireNotNull(car.getCarManager(CarPackageManager::class.java)),
+ requireContext().getSystemService(MediaSessionManager::class.java),
+ requireContext().applicationContext.resources,
+ Default
+ )
+ val appOrderDataSource: AppOrderDataSource = AppOrderProtoDataSourceImpl(
+ LauncherItemListSource(requireContext().filesDir, "order.data"),
+ IO
+ )
+ val packageManager: PackageManager = requireContext().packageManager
+ val launchProviderFactory = AppLaunchProviderFactory(
+ requireNotNull(car.getCarManager(CarMediaManager::class.java)),
+ mode.openMediaCenter,
+ {
+ activity?.finish()
+ },
+ requireContext().packageManager
+ )
+
+ val appShortcutsFactory = AppShortcutsFactory(
+ requireNotNull(car.getCarManager(CarMediaManager::class.java)),
+ emptySet(),
+ object : ShortcutsListener {
+ override fun onShortcutsShow(carUiShortcutsPopup: CarUiShortcutsPopup) {
+ [email protected] = carUiShortcutsPopup
+ }
+ }
+ )
+ val bgDispatcher = Default
+ val repo: AppGridRepository = AppGridRepositoryImpl(
+ launcherActivities, mediaTemplateApps,
+ disabledApps, tosApps, controlCenterMirroringDataSource, uxRestrictionDataSource,
+ appOrderDataSource, packageManager, launchProviderFactory, appShortcutsFactory,
+ requireContext().getSystemService(UserManager::class.java), bgDispatcher
+ )
+
+ appGridViewModel = ViewModelProvider(
+ this,
+ provideFactory(repo, requireActivity().application, this, null)
+ )[AppGridViewModel::class.java]
+ }
+
+ private fun animateDropEnded(dragSurface: SurfaceControl?) {
+ if (dragSurface == null) {
+ if (DEBUG_BUILD) {
+ Log.d(TAG, "animateDropEnded, dragSurface unavailable")
+ }
+ return
+ }
+ // update default animation for the drag shadow after user lifts their finger
+ val txn = SurfaceControl.Transaction()
+ // set an animator to animate a delay before clearing the dragSurface
+ val delayedDismissAnimator = ValueAnimator.ofFloat(0f, 1f)
+ delayedDismissAnimator.startDelay =
+ resources.getInteger(R.integer.ms_drop_animation_delay).toLong()
+ delayedDismissAnimator.addUpdateListener {
+ txn.setAlpha(dragSurface, 0f)
+ txn.apply()
+ }
+ delayedDismissAnimator.start()
+ }
+
+ private fun setupTosBanner() {
+ appGridViewModel.getShouldShowTosBanner().asLiveData()
+ .observe(
+ viewLifecycleOwner
+ ) { showBanner: Boolean ->
+ if (showBanner) {
+ banner.visibility = View.VISIBLE
+ // Pre draw is required for animation to work.
+ banner.viewTreeObserver.addOnPreDrawListener(
+ object : ViewTreeObserver.OnPreDrawListener {
+ override fun onPreDraw(): Boolean {
+ banner.viewTreeObserver.removeOnPreDrawListener(this)
+ backgroundAnimationHelper.showBanner()
+ return true
+ }
+ }
+ )
+ } else {
+ banner.visibility = View.GONE
+ }
+ }
+ banner.setFirstButtonOnClickListener { v: View ->
+ val tosIntent =
+ AppLauncherUtils.getIntentForTosAcceptanceFlow(v.context)
+ AppLauncherUtils.launchApp(v.context, tosIntent)
+ }
+ banner.setSecondButtonOnClickListener { _ ->
+ backgroundAnimationHelper.hideBanner()
+ appGridViewModel.saveTosBannerDismissalTime()
+ }
+ }
+
+ /**
+ * Updates the scroll state after receiving data changes, such as new apps being added or
+ * reordered, and when user returns to launcher onResume.
+ *
+ * Additionally, notify page indicator to handle resizing in case new app addition creates a
+ * new page or deleted a page.
+ */
+ fun updateScrollState() {
+ // TODO(b/271637411): move this method into a scroll controller
+ // to calculate how many pages we need to offset, we use the scroll offset anchor position
+ // as item count and map to the page which the anchor is on.
+ val offsetPageCount = adapter.getPageCount(nextScrollDestination + 1) - 1
+ appGridRecyclerView.suppressLayout(false)
+ currentScrollOffset =
+ offsetPageCount * if (isHorizontal(pageOrientation)) {
+ appGridWidth + 2 * appGridMarginHorizontal
+ } else {
+ appGridHeight + 2 * appGridMarginVertical
+ }
+ layoutManager.scrollToPositionWithOffset(offsetPageCount * numOfRows * numOfCols, 0)
+ pageIndicator.updateOffset(currentScrollOffset)
+ pageIndicator.updatePageCount(adapter.pageCount)
+ }
+
+ /**
+ * Change the mode of the apps shown in the AppGrid
+ * @see [Mode]
+ */
+ fun updateMode(mode: Mode) {
+ this.mode = mode
+ appGridViewModel.updateMode(mode)
+ }
+
+ private inner class AppGridOnScrollListener : RecyclerView.OnScrollListener() {
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ currentScrollOffset += if (isHorizontal(pageOrientation)) dx else dy
+ pageIndicator.updateOffset(currentScrollOffset)
+ }
+
+ override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
+ currentScrollState = newState
+ snapCallback.scrollState = currentScrollState
+ when (newState) {
+ RecyclerView.SCROLL_STATE_DRAGGING -> {
+ if (!isCurrentlyDragging) {
+ dragCallback.cancelDragTasks()
+ }
+ dismissShortcutPopup()
+ pageIndicator.animateAppearance()
+ }
+
+ RecyclerView.SCROLL_STATE_SETTLING -> pageIndicator.animateAppearance()
+ RecyclerView.SCROLL_STATE_IDLE -> {
+ if (isCurrentlyDragging) {
+ layoutManager.setShouldLayoutChildren(false)
+ }
+ pageIndicator.animateFading()
+ // in case the recyclerview was scrolled by rotary input, we need to handle
+ // focusing the correct element: either on the first or last element on page
+ appGridRecyclerView.maybeHandleRotaryFocus()
+ }
+ }
+ }
+ }
+
+ private fun dismissShortcutPopup() {
+ carUiShortcutsPopup?.let {
+ it.dismiss()
+ carUiShortcutsPopup = null
+ }
+ }
+
+ override fun onPause() {
+ dismissShortcutPopup()
+ super.onPause()
+ }
+
+ override fun onDestroy() {
+ if (car.isConnected) {
+ car.disconnect()
+ }
+ super.onDestroy()
+ }
+
+ override fun onSnapToPosition(gridPosition: Int) {
+ nextScrollDestination = gridPosition
+ }
+
+ override fun onDimensionsUpdated(
+ pageDimens: PageMeasurementHelper.PageDimensions,
+ gridDimens: PageMeasurementHelper.GridDimensions
+ ) {
+ // TODO(b/271637411): move this method into a scroll controller
+ appGridMarginHorizontal = pageDimens.marginHorizontalPx
+ appGridMarginVertical = pageDimens.marginVerticalPx
+ appGridWidth = gridDimens.gridWidthPx
+ appGridHeight = gridDimens.gridHeightPx
+ }
+
+ override fun onAppPositionChanged(newPosition: Int, appItem: AppItem) {
+ appGridViewModel.saveAppOrder(newPosition, appItem)
+ }
+
+ override fun onItemLongPressed(longPressed: Boolean) {
+ // after the user long presses the app icon, scrolling should be disabled until long press
+ // is canceled as to allow MotionEvent to be interpreted as attempt to drag the app icon.
+ appGridRecyclerView.suppressLayout(longPressed)
+ }
+
+ override fun onItemSelected(gridPositionFrom: Int) {
+ isCurrentlyDragging = true
+ layoutManager.setShouldLayoutChildren(false)
+ adapter.setDragStartPoint(gridPositionFrom)
+ dismissShortcutPopup()
+ }
+
+ override fun onItemDragged() {
+ appGridDragController.cancelDelayedPageFling()
+ }
+
+ override fun onDragExited(gridPosition: Int, exitDirection: Int) {
+ if (adapter.getOffsetBoundDirection(gridPosition) == exitDirection) {
+ appGridDragController.postDelayedPageFling(exitDirection)
+ }
+ }
+
+ override fun onItemDropped(gridPositionFrom: Int, gridPositionTo: Int) {
+ layoutManager.setShouldLayoutChildren(true)
+ adapter.moveAppItem(gridPositionFrom, gridPositionTo)
+ }
+
+ private inner class AppGridDragController() {
+ // TODO: (b/271320404) move DragController to separate directory called dragndrop and
+ // migrate logic this class and AppItemViewHolder there.
+ private val handler: Handler = Handler(getMainLooper())
+
+ fun cancelDelayedPageFling() {
+ handler.removeCallbacksAndMessages(null)
+ }
+
+ fun postDelayedPageFling(@AppItemBoundDirection exitDirection: Int) {
+ val scrollToNextPage =
+ if (isHorizontal(pageOrientation)) {
+ exitDirection == AppItemBoundDirection.RIGHT
+ } else {
+ exitDirection == AppItemBoundDirection.BOTTOM
+ }
+ handler.removeCallbacksAndMessages(null)
+ handler.postDelayed({
+ if (currentScrollState == RecyclerView.SCROLL_STATE_IDLE) {
+ adapter.updatePageScrollDestination(scrollToNextPage)
+ nextScrollDestination = snapCallback.snapPosition
+ layoutManager.setShouldLayoutChildren(true)
+ appGridRecyclerView.smoothScrollToPosition(nextScrollDestination)
+ }
+ // another delayed scroll will be queued to enable the user to input multiple
+ // page scrolls by holding the recyclerview at the app grid margin
+ postDelayedPageFling(exitDirection)
+ }, offPageHoverBeforeScrollMs)
+ }
+ }
+
+ /**
+ * Private onDragListener for handling dispatching off page scroll event when user holds the app
+ * icon at the page margin.
+ */
+ private inner class AppGridDragListener : View.OnDragListener {
+ override fun onDrag(v: View, event: DragEvent): Boolean {
+ val action = event.action
+ if (action == DragEvent.ACTION_DROP || action == DragEvent.ACTION_DRAG_ENDED) {
+ isCurrentlyDragging = false
+ appGridDragController.cancelDelayedPageFling()
+ dragCallback.resetCallbackState()
+ layoutManager.setShouldLayoutChildren(true)
+ if (action == DragEvent.ACTION_DROP) {
+ return false
+ } else {
+ animateDropEnded(HiddenApiAccess.getDragSurface(event))
+ }
+ }
+ return true
+ }
+ }
+
+ annotation class AppTypes {
+ companion object {
+ const val APP_TYPE_LAUNCHABLES = 1
+ const val APP_TYPE_MEDIA_SERVICES = 2
+ }
+ }
+
+ enum class Mode(
+ @field:StringRes @param:StringRes val titleStringId: Int,
+ @field:AppTypes @param:AppTypes val appTypes: Int,
+ val openMediaCenter: Boolean
+ ) {
+ ALL_APPS(
+ R.string.app_launcher_title_all_apps,
+ APP_TYPE_LAUNCHABLES + APP_TYPE_MEDIA_SERVICES,
+ true
+ ),
+ MEDIA_ONLY(
+ R.string.app_launcher_title_media_only,
+ APP_TYPE_MEDIA_SERVICES,
+ true
+ ),
+ MEDIA_POPUP(
+ R.string.app_launcher_title_media_only,
+ APP_TYPE_MEDIA_SERVICES,
+ false
+ )
+ }
+
+ companion object {
+ const val TAG = "AppGridFragment"
+ const val DEBUG_BUILD = false
+ const val MODE_INTENT_EXTRA = "com.android.car.carlauncher.mode"
+
+ @JvmStatic
+ fun newInstance(mode: Mode): AppGridFragment {
+ return AppGridFragment().apply {
+ arguments = Bundle().apply {
+ putString(MODE_INTENT_EXTRA, mode.name)
+ }
+ }
+ }
+ }
+}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/AppGridViewModel.kt b/libs/appgrid/lib/src/com/android/car/carlauncher/AppGridViewModel.kt
new file mode 100644
index 0000000..3787111
--- /dev/null
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/AppGridViewModel.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2024 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.carlauncher
+
+import android.app.Application
+import android.os.Bundle
+import android.os.SystemClock
+import androidx.lifecycle.AbstractSavedStateViewModelFactory
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.preference.PreferenceManager
+import androidx.savedstate.SavedStateRegistryOwner
+import com.android.car.carlauncher.AppGridFragment.AppTypes.Companion.APP_TYPE_LAUNCHABLES
+import com.android.car.carlauncher.AppGridFragment.Mode
+import com.android.car.carlauncher.repositories.AppGridRepository
+import java.time.Clock
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.emitAll
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.transformLatest
+import kotlinx.coroutines.launch
+
+/**
+ * This ViewModel manages the main application grid within the car launcher. It provides
+ * methods to retrieve app lists, handle app reordering, determine distraction
+ * optimization requirements, and manage the Terms of Service (TOS) banner display.
+ */
+class AppGridViewModel(
+ private val appGridRepository: AppGridRepository,
+ private val application: Application
+) : AndroidViewModel(application) {
+
+ /**
+ * A Kotlin Flow containing a complete list of applications obtained from the repository.
+ * This Flow is shared for efficiency within the ViewModel.
+ */
+ private val allAppsItemList = appGridRepository.getAllAppsList()
+ .shareIn(viewModelScope, SharingStarted.WhileSubscribed(STOP_TIME_OUT_FLOW_SUBSCRIPTION), 1)
+
+ /**
+ * A Kotlin Flow containing a list of media-focused applications obtained from the repository,
+ * shared for efficiency within the ViewModel.
+ */
+ private val mediaOnlyList = appGridRepository.getMediaAppsList()
+ .shareIn(viewModelScope, SharingStarted.WhileSubscribed(STOP_TIME_OUT_FLOW_SUBSCRIPTION), 1)
+
+ /**
+ * A MutableStateFlow indicating the current application display mode in the app grid.
+ */
+ private val appMode: MutableStateFlow<Mode> = MutableStateFlow(Mode.ALL_APPS)
+
+ /**
+ * Provides a Flow of application lists (AppItem). The returned Flow dynamically switches
+ * between the complete app list (`allAppsItemList`) and a filtered list
+ * of media apps (`mediaOnlyList`) based on the current `appMode`.
+ *
+ * @return A Flow of AppItem lists
+ */
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun getAppList(): Flow<List<AppItem>> {
+ return appMode.transformLatest {
+ val sourceList = if (it.appTypes and APP_TYPE_LAUNCHABLES == 1) {
+ allAppsItemList
+ } else {
+ mediaOnlyList
+ }
+ emitAll(sourceList)
+ }.distinctUntilChanged()
+ }
+
+ /**
+ * Updates the application order in the repository.
+ *
+ * @param newPosition The intended new index position for the app.
+ * @param appItem The AppItem to be repositioned.
+ */
+ fun saveAppOrder(newPosition: Int, appItem: AppItem) {
+ viewModelScope.launch {
+ allAppsItemList.replayCache.lastOrNull()?.toMutableList()?.apply {
+ // Remove original occurrence
+ remove(appItem)
+ // Add to new position
+ add(newPosition, appItem)
+ }?.let {
+ appGridRepository.saveAppOrder(it)
+ }
+ }
+ }
+
+ /**
+ * Provides a flow indicating whether distraction optimization should be applied
+ * in the car launcher UI.
+ *
+ * @return A Flow emitting Boolean values where 'true' signifies a need for distraction optimization.
+ */
+ fun requiresDistractionOptimization(): Flow<Boolean> {
+ return appGridRepository.requiresDistractionOptimization()
+ }
+
+ /**
+ * Returns a flow that determines whether the Terms of Service (TOS) banner should be displayed.
+ * The logic considers if the TOS requires acceptance and the banner resurfacing interval.
+ *
+ * @return A Flow emitting Boolean values where 'true' indicates the banner should be displayed.
+ */
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun getShouldShowTosBanner(): Flow<Boolean> {
+ return appGridRepository.getTosState().mapLatest {
+ if (!it.shouldBlockTosApps) {
+ return@mapLatest false
+ }
+ return@mapLatest shouldShowTos()
+ }
+ }
+
+ /**
+ * Checks if we need to show the Banner based when it was previously dismissed.
+ */
+ private fun shouldShowTos(): Boolean {
+ // Convert days to seconds
+ val bannerResurfaceTimeInSeconds = TimeUnit.DAYS.toSeconds(
+ application.resources
+ .getInteger(R.integer.config_tos_banner_resurface_time_days).toLong()
+ )
+ val bannerDismissTime = PreferenceManager.getDefaultSharedPreferences(application)
+ .getLong(TOS_BANNER_DISMISS_TIME_KEY, 0)
+
+ val systemBootTime = Clock.systemUTC()
+ .instant().epochSecond - TimeUnit.MILLISECONDS.toSeconds(SystemClock.elapsedRealtime())
+ // Show on next drive / reboot, when banner has not been dismissed in current session
+ return if (bannerResurfaceTimeInSeconds == 0L) {
+ // If banner is dismissed in current drive session, it will have a timestamp greater
+ // than the system boot time timestamp.
+ bannerDismissTime < systemBootTime
+ } else {
+ Clock.systemUTC()
+ .instant().epochSecond - bannerDismissTime > bannerResurfaceTimeInSeconds
+ }
+ }
+
+ /**
+ * Saves the current timestamp to Preferences, marking the time when the Terms of Service (TOS)
+ * banner was dismissed by the user.
+ */
+ fun saveTosBannerDismissalTime() {
+ val dismissTime: Long = Clock.systemUTC().instant().epochSecond
+ PreferenceManager.getDefaultSharedPreferences(application)
+ .edit().putLong(TOS_BANNER_DISMISS_TIME_KEY, dismissTime).apply()
+ }
+
+ /**
+ * Updates the current application display mode. This triggers UI updates in the app grid.
+ * @param mode The new Mode to set for the application grid.
+ */
+ fun updateMode(mode: Mode) {
+ appMode.value = mode
+ }
+
+ companion object {
+ const val TOS_BANNER_DISMISS_TIME_KEY = "TOS_BANNER_DISMISS_TIME"
+ const val STOP_TIME_OUT_FLOW_SUBSCRIPTION = 5_000L
+ fun provideFactory(
+ myRepository: AppGridRepository,
+ application: Application,
+ owner: SavedStateRegistryOwner,
+ defaultArgs: Bundle? = null,
+ ): AbstractSavedStateViewModelFactory =
+ object : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
+ override fun <T : ViewModel> create(
+ key: String,
+ modelClass: Class<T>,
+ handle: SavedStateHandle
+ ): T {
+ return AppGridViewModel(myRepository, application) as T
+ }
+ }
+ }
+}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/AppItemDragShadowBuilder.java b/libs/appgrid/lib/src/com/android/car/carlauncher/AppItemDragShadowBuilder.java
index 1ea40e2..8f2a4e4 100644
--- a/libs/appgrid/lib/src/com/android/car/carlauncher/AppItemDragShadowBuilder.java
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/AppItemDragShadowBuilder.java
@@ -18,24 +18,28 @@
import android.graphics.Canvas;
import android.graphics.Point;
+import android.graphics.drawable.Drawable;
import android.view.View;
+import androidx.annotation.NonNull;
+
/**
* Custom View.DragShadowBuilder that handles the drawing and deploying of drag shadow when an
* app icon is long pressed and dragged.
*/
public class AppItemDragShadowBuilder extends View.DragShadowBuilder{
- private final View mAppIcon;
- private final int mSize;
+ private final Drawable mIcon;
private final int mScaledSize;
private final float mTouchPointX;
private final float mTouchPointY;
- public AppItemDragShadowBuilder(View view, float touchPointX, float touchPointY,
- int size, int scaledSize) {
- super(view);
- mAppIcon = view;
- mSize = size;
+ /**
+ * @param icon Drawable to be drawn as the drag shadow to represent the view being dragged.
+ */
+ public AppItemDragShadowBuilder(Drawable icon, float touchPointX, float touchPointY,
+ int scaledSize) {
+ super();
+ mIcon = icon;
mScaledSize = scaledSize;
mTouchPointX = touchPointX;
mTouchPointY = touchPointY;
@@ -43,16 +47,13 @@
@Override
public void onProvideShadowMetrics(Point outShadowSize, Point outShadowTouchPoint) {
- if (mAppIcon != null) {
- outShadowSize.set(mScaledSize, mScaledSize);
- outShadowTouchPoint.set((int) mTouchPointX, (int) mTouchPointY);
- }
+ outShadowSize.set(mScaledSize, mScaledSize);
+ outShadowTouchPoint.set((int) mTouchPointX, (int) mTouchPointY);
}
@Override
- public void onDrawShadow(Canvas canvas) {
- canvas.scale(/* scaleX */ mScaledSize / (float) mSize,
- /* scaleY */ mScaledSize / (float) mSize);
- getView().draw(canvas);
+ public void onDrawShadow(@NonNull Canvas canvas) {
+ mIcon.setBounds(0, 0, mScaledSize, mScaledSize);
+ mIcon.draw(canvas);
}
}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/AppLauncherUtils.java b/libs/appgrid/lib/src/com/android/car/carlauncher/AppLauncherUtils.java
index dcf7c93..a597ce0 100644
--- a/libs/appgrid/lib/src/com/android/car/carlauncher/AppLauncherUtils.java
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/AppLauncherUtils.java
@@ -16,104 +16,41 @@
package com.android.car.carlauncher;
-import static android.car.settings.CarSettings.Secure.KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE;
import static android.car.settings.CarSettings.Secure.KEY_UNACCEPTED_TOS_DISABLED_APPS;
import static android.car.settings.CarSettings.Secure.KEY_USER_TOS_ACCEPTED;
-import static com.android.car.carlauncher.hidden.HiddenApiAccess.hasBaseUserRestriction;
+import static com.android.car.carlauncher.datasources.restricted.TosDataSourceImpl.TOS_DISABLED_APPS_SEPARATOR;
+import static com.android.car.carlauncher.datasources.restricted.TosDataSourceImpl.TOS_NOT_ACCEPTED;
+import static com.android.car.carlauncher.datasources.restricted.TosDataSourceImpl.TOS_UNINITIALIZED;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import android.app.Activity;
-import android.app.ActivityManager;
import android.app.ActivityOptions;
-import android.app.admin.DevicePolicyManager;
-import android.car.Car;
-import android.car.CarNotConnectedException;
-import android.car.content.pm.CarPackageManager;
-import android.car.media.CarMediaManager;
-import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.LauncherActivityInfo;
-import android.content.pm.LauncherApps;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.net.Uri;
import android.os.Process;
import android.os.UserHandle;
-import android.os.UserManager;
import android.provider.Settings;
-import android.service.media.MediaBrowserService;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
-import android.util.Pair;
-import android.view.View;
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import com.android.car.dockutil.events.DockEventSenderHelper;
-import com.android.car.dockutil.shortcuts.PinShortcutItem;
-import com.android.car.media.common.source.MediaSourceUtil;
-import com.android.car.ui.shortcutspopup.CarUiShortcutsPopup;
-
-import com.google.common.collect.Sets;
-
-import java.lang.annotation.Retention;
import java.net.URISyntaxException;
-import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
import java.util.Objects;
import java.util.Set;
-import java.util.function.Consumer;
/**
* Util class that contains helper method used by app launcher classes.
*/
public class AppLauncherUtils {
private static final String TAG = "AppLauncherUtils";
- private static final String ANDROIDX_CAR_APP_LAUNCHABLE = "androidx.car.app.launchable";
-
- @Retention(SOURCE)
- @IntDef({APP_TYPE_LAUNCHABLES, APP_TYPE_MEDIA_SERVICES})
- @interface AppTypes {}
-
- static final int APP_TYPE_LAUNCHABLES = 1;
- static final int APP_TYPE_MEDIA_SERVICES = 2;
-
- // This value indicates if TOS has not been accepted by the user
- private static final String TOS_NOT_ACCEPTED = "1";
- // This value indicates if TOS is in uninitialized state
- private static final String TOS_UNINITIALIZED = "0";
- static final String TOS_DISABLED_APPS_SEPARATOR = ",";
- static final String PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR = ";";
-
- // Max no. of uses tags in automotiveApp XML. This is an arbitrary limit to be defensive
- // to bad input.
- private static final int MAX_APP_TYPES = 64;
- private static final String PACKAGE_URI_PREFIX = "package:";
private AppLauncherUtils() {
}
/**
- * Comparator for {@link AppMetaData} that sorts the list
- * by the "displayName" property in ascending order.
- */
- static final Comparator<AppMetaData> ALPHABETICAL_COMPARATOR = Comparator
- .comparing(AppMetaData::getDisplayName, String::compareToIgnoreCase);
-
- /**
* Helper method that launches the app given the app's AppMetaData.
*/
public static void launchApp(Context context, Intent intent) {
@@ -122,285 +59,6 @@
context.startActivity(intent, options.toBundle());
}
- /** Bundles application and services info. */
- static class LauncherAppsInfo {
- /*
- * Map of all car launcher components' (including launcher activities and media services)
- * metadata keyed by ComponentName.
- */
- private final Map<ComponentName, AppMetaData> mLaunchables;
-
- /** Map of all the media services keyed by ComponentName. */
- private final Map<ComponentName, ResolveInfo> mMediaServices;
-
- LauncherAppsInfo(@NonNull Map<ComponentName, AppMetaData> launchablesMap,
- @NonNull Map<ComponentName, ResolveInfo> mediaServices) {
- mLaunchables = launchablesMap;
- mMediaServices = mediaServices;
- }
-
- /** Returns true if all maps are empty. */
- boolean isEmpty() {
- return mLaunchables.isEmpty() && mMediaServices.isEmpty();
- }
-
- /**
- * Returns whether the given componentName is a media service.
- */
- boolean isMediaService(ComponentName componentName) {
- return mMediaServices.containsKey(componentName);
- }
-
- /** Returns the {@link AppMetaData} for the given componentName. */
- @Nullable
- AppMetaData getAppMetaData(ComponentName componentName) {
- return mLaunchables.get(componentName);
- }
-
- /** Returns a new list of all launchable components' {@link AppMetaData}. */
- @NonNull
- List<AppMetaData> getLaunchableComponentsList() {
- return new ArrayList<>(mLaunchables.values());
- }
-
- /** Returns list of Media Services for the launcher **/
- @NonNull
- Map<ComponentName, ResolveInfo> getMediaServices() {
- return mMediaServices;
- }
- }
-
- private static final LauncherAppsInfo EMPTY_APPS_INFO = new LauncherAppsInfo(
- Collections.emptyMap(), Collections.emptyMap());
-
- /*
- * Gets the media source in a given package. If there are multiple sources in the package,
- * returns the first one.
- */
- static ComponentName getMediaSource(@NonNull PackageManager packageManager,
- @NonNull String packageName) {
- Intent mediaIntent = new Intent();
- mediaIntent.setPackage(packageName);
- mediaIntent.setAction(MediaBrowserService.SERVICE_INTERFACE);
-
- List<ResolveInfo> mediaServices = packageManager.queryIntentServices(mediaIntent,
- PackageManager.GET_RESOLVED_FILTER);
-
- if (mediaServices == null || mediaServices.isEmpty()) {
- return null;
- }
- String defaultService = mediaServices.get(0).serviceInfo.name;
- if (!TextUtils.isEmpty(defaultService)) {
- return new ComponentName(packageName, defaultService);
- }
- return null;
- }
-
- /**
- * Gets all the components that we want to see in the launcher in unsorted order, including
- * launcher activities and media services.
- *
- * @param appsToHide A (possibly empty) list of apps (package names) to hide
- * @param appTypes Types of apps to show (e.g.: all, or media sources only)
- * @param openMediaCenter Whether launcher should navigate to media center when the
- * user selects a media source.
- * @param launcherApps The {@link LauncherApps} system service
- * @param carPackageManager The {@link CarPackageManager} system service
- * @param packageManager The {@link PackageManager} system service
- * of such apps are always excluded.
- * @param carMediaManager The {@link CarMediaManager} system service
- * @return a new {@link LauncherAppsInfo}
- */
- @NonNull
- static LauncherAppsInfo getLauncherApps(
- Context context,
- @NonNull Set<String> appsToHide,
- @AppTypes int appTypes,
- boolean openMediaCenter,
- LauncherApps launcherApps,
- CarPackageManager carPackageManager,
- PackageManager packageManager,
- CarMediaManager carMediaManager,
- ShortcutsListener shortcutsListener,
- String mirroringAppPkgName,
- Intent mirroringAppRedirect) {
-
- if (launcherApps == null || carPackageManager == null || packageManager == null
- || carMediaManager == null) {
- return EMPTY_APPS_INFO;
- }
-
- boolean isDockEnabled = context.getResources().getBoolean(R.bool.config_enableDock);
-
- // Using new list since we require a mutable list to do removeIf.
- List<ResolveInfo> mediaServices = new ArrayList<>();
- mediaServices.addAll(
- packageManager.queryIntentServices(
- new Intent(MediaBrowserService.SERVICE_INTERFACE),
- PackageManager.GET_RESOLVED_FILTER));
-
- List<LauncherActivityInfo> availableActivities =
- launcherApps.getActivityList(null, Process.myUserHandle());
-
- int launchablesSize = mediaServices.size() + availableActivities.size();
- Map<ComponentName, AppMetaData> launchablesMap = new HashMap<>(launchablesSize);
- Map<ComponentName, ResolveInfo> mediaServicesMap = new HashMap<>(mediaServices.size());
- Set<String> mEnabledPackages = new ArraySet<>(launchablesSize);
- Set<String> tosDisabledPackages = getTosDisabledPackages(context);
-
- Set<String> customMediaComponents = Sets.newHashSet(
- context.getResources().getStringArray(
- com.android.car.media.common.R.array.custom_media_packages));
-
- // Process media services
- if ((appTypes & APP_TYPE_MEDIA_SERVICES) != 0) {
- for (ResolveInfo info : mediaServices) {
- String packageName = info.serviceInfo.packageName;
- String className = info.serviceInfo.name;
- ComponentName componentName = new ComponentName(packageName, className);
- mediaServicesMap.put(componentName, info);
- mEnabledPackages.add(packageName);
- if (shouldAddToLaunchables(context, componentName, appsToHide,
- customMediaComponents, appTypes, APP_TYPE_MEDIA_SERVICES)) {
- CharSequence displayName = info.serviceInfo.loadLabel(packageManager);
- AppMetaData appMetaData = new AppMetaData(
- displayName,
- componentName,
- info.serviceInfo.loadIcon(packageManager),
- /* isDistractionOptimized= */ true,
- /* isMirroring = */ false,
- /* isDisabledByTos= */ tosDisabledPackages.contains(packageName),
- contextArg -> {
- if (openMediaCenter) {
- AppLauncherUtils.launchApp(contextArg,
- createMediaLaunchIntent(componentName));
- } else {
- selectMediaSourceAndFinish(contextArg, componentName,
- carMediaManager);
- }
- },
- buildShortcuts(componentName, displayName, shortcutsListener,
- isDockEnabled));
- launchablesMap.put(componentName, appMetaData);
- }
- }
- }
-
- // Process activities
- if ((appTypes & APP_TYPE_LAUNCHABLES) != 0) {
- for (LauncherActivityInfo info : availableActivities) {
- ComponentName componentName = info.getComponentName();
- mEnabledPackages.add(componentName.getPackageName());
- if (shouldAddToLaunchables(context, componentName, appsToHide,
- customMediaComponents, appTypes, APP_TYPE_LAUNCHABLES)) {
- boolean isDistractionOptimized =
- isActivityDistractionOptimized(carPackageManager,
- componentName.getPackageName(), info.getName());
- boolean isDisabledByTos = tosDisabledPackages
- .contains(componentName.getPackageName());
-
- CharSequence displayName = info.getLabel();
- boolean isMirroring = componentName.getPackageName()
- .equals(mirroringAppPkgName);
- AppMetaData appMetaData = new AppMetaData(
- displayName,
- componentName,
- info.getBadgedIcon(0),
- isDistractionOptimized,
- isMirroring,
- isDisabledByTos,
- contextArg -> {
- if (componentName.getPackageName().equals(mirroringAppPkgName)) {
- Log.d(TAG, "non-media service package name "
- + "equals mirroring pkg name");
- }
- AppLauncherUtils.launchApp(contextArg,
- isMirroring ? mirroringAppRedirect :
- createAppLaunchIntent(componentName));
- },
- buildShortcuts(componentName, displayName, shortcutsListener,
- isDockEnabled));
- launchablesMap.put(componentName, appMetaData);
- }
- }
-
- List<ResolveInfo> disabledActivities = getDisabledActivities(context, packageManager,
- mEnabledPackages);
- for (ResolveInfo info : disabledActivities) {
- String packageName = info.activityInfo.packageName;
- String className = info.activityInfo.name;
- ComponentName componentName = new ComponentName(packageName, className);
- if (!shouldAddToLaunchables(context, componentName, appsToHide,
- customMediaComponents, appTypes, APP_TYPE_LAUNCHABLES)) {
- continue;
- }
- boolean isDistractionOptimized =
- isActivityDistractionOptimized(carPackageManager, packageName, className);
- boolean isDisabledByTos = tosDisabledPackages.contains(packageName);
-
- CharSequence displayName = info.activityInfo.loadLabel(packageManager);
- AppMetaData appMetaData = new AppMetaData(
- displayName,
- componentName,
- info.activityInfo.loadIcon(packageManager),
- isDistractionOptimized,
- /* isMirroring = */ false,
- isDisabledByTos,
- contextArg -> {
- packageManager.setApplicationEnabledSetting(packageName,
- PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0);
- // Fetch the current enabled setting to make sure the setting is synced
- // before launching the activity. Otherwise, the activity may not
- // launch.
- if (packageManager.getApplicationEnabledSetting(packageName)
- != PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
- throw new IllegalStateException(
- "Failed to enable the disabled package [" + packageName
- + "]");
- }
- Log.i(TAG, "Successfully enabled package [" + packageName + "]");
- AppLauncherUtils.launchApp(contextArg,
- createAppLaunchIntent(componentName));
- },
- buildShortcuts(componentName, displayName, shortcutsListener,
- isDockEnabled));
- launchablesMap.put(componentName, appMetaData);
- }
-
- List<ResolveInfo> restrictedActivities = getTosDisabledActivities(
- context,
- packageManager,
- mEnabledPackages
- );
- for (ResolveInfo info: restrictedActivities) {
- String packageName = info.activityInfo.packageName;
- String className = info.activityInfo.name;
- ComponentName componentName = new ComponentName(packageName, className);
-
- boolean isDistractionOptimized =
- isActivityDistractionOptimized(carPackageManager, packageName, className);
- boolean isDisabledByTos = tosDisabledPackages.contains(packageName);
-
- AppMetaData appMetaData = new AppMetaData(
- info.activityInfo.loadLabel(packageManager),
- componentName,
- info.activityInfo.loadIcon(packageManager),
- isDistractionOptimized,
- /* isMirroring = */ false,
- isDisabledByTos,
- contextArg -> {
- Intent tosIntent = getIntentForTosAcceptanceFlow(contextArg);
- launchApp(contextArg, tosIntent);
- },
- null
- );
- launchablesMap.put(componentName, appMetaData);
- }
- }
-
- return new LauncherAppsInfo(launchablesMap, mediaServicesMap);
- }
-
/**
* Gets the intent for launching the TOS acceptance flow
*
@@ -419,357 +77,6 @@
}
}
- private static Consumer<Pair<Context, View>> buildShortcuts(
- ComponentName componentName, CharSequence displayName,
- ShortcutsListener shortcutsListener, boolean isDockEnabled) {
- return pair -> {
- CarUiShortcutsPopup.Builder carUiShortcutsPopupBuilder =
- new CarUiShortcutsPopup.Builder()
- .addShortcut(buildForceStopShortcut(componentName.getPackageName(),
- displayName, pair.first, shortcutsListener))
- .addShortcut(buildAppInfoShortcut(componentName.getPackageName(),
- pair.first));
- if (isDockEnabled) {
- carUiShortcutsPopupBuilder
- .addShortcut(buildPinToDockShortcut(componentName, pair.first));
- }
- CarUiShortcutsPopup carUiShortcutsPopup = carUiShortcutsPopupBuilder
- .build(pair.first, pair.second);
-
- carUiShortcutsPopup.show();
- shortcutsListener.onShortcutsShow(carUiShortcutsPopup);
- };
- }
-
- private static CarUiShortcutsPopup.ShortcutItem buildForceStopShortcut(String packageName,
- CharSequence displayName,
- Context context,
- ShortcutsListener shortcutsListener) {
- return new CarUiShortcutsPopup.ShortcutItem() {
- @Override
- public CarUiShortcutsPopup.ItemData data() {
- return new CarUiShortcutsPopup.ItemData(
- R.drawable.ic_force_stop_caution_icon,
- context.getResources().getString(
- R.string.app_launcher_stop_app_action));
- }
-
- @Override
- public boolean onClick() {
- shortcutsListener.onShortcutsItemClick(packageName, displayName,
- /* allowStopApp= */ true);
- return true;
- }
-
- @Override
- public boolean isEnabled() {
- return shouldAllowStopApp(packageName, context);
- }
- };
- }
-
- private static CarUiShortcutsPopup.ShortcutItem buildAppInfoShortcut(String packageName,
- Context context) {
- return new CarUiShortcutsPopup.ShortcutItem() {
- @Override
- public CarUiShortcutsPopup.ItemData data() {
- return new CarUiShortcutsPopup.ItemData(
- R.drawable.ic_app_info,
- context.getResources().getString(
- R.string.app_launcher_app_info_action));
- }
-
- @Override
- public boolean onClick() {
- Uri packageURI = Uri.parse(PACKAGE_URI_PREFIX + packageName);
- Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
- packageURI);
- context.startActivity(intent);
- return true;
- }
-
- @Override
- public boolean isEnabled() {
- return true;
- }
- };
- }
-
- private static CarUiShortcutsPopup.ShortcutItem buildPinToDockShortcut(
- ComponentName componentName, Context context) {
- DockEventSenderHelper mHelper = new DockEventSenderHelper(context);
- return new PinShortcutItem(context.getResources(), /* isItemPinned= */ false,
- /* pinItemClickDelegate= */ () -> mHelper.sendPinEvent(componentName),
- /* unpinItemClickDelegate= */ () -> mHelper.sendUnpinEvent(componentName)
- );
- }
-
- /**
- * Force stops an app
- * <p>Note: Uses hidden apis<p/>
- */
- public static void forceStop(String packageName, Context context, CharSequence displayName,
- CarMediaManager carMediaManager, Map<ComponentName, ResolveInfo> mediaServices,
- ShortcutsListener listener) {
- ActivityManager activityManager = context.getSystemService(ActivityManager.class);
- if (activityManager != null) {
- maybeReplaceMediaSource(carMediaManager, packageName, mediaServices,
- CarMediaManager.MEDIA_SOURCE_MODE_BROWSE);
- maybeReplaceMediaSource(carMediaManager, packageName, mediaServices,
- CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK);
- activityManager.forceStopPackage(packageName);
- String message = context.getResources()
- .getString(R.string.app_launcher_stop_app_success_toast_text, displayName);
- listener.onStopAppSuccess(message);
- }
- }
-
- private static boolean isCurrentMediaSource(CarMediaManager carMediaManager,
- String packageName, int mode) {
- ComponentName componentName = carMediaManager.getMediaSource(mode);
- if (componentName == null) {
- //There is no current media source.
- return false;
- }
- return Objects.equals(componentName.getPackageName(), packageName);
- }
-
- /***
- * Updates the MediaSource to second most recent if {@code packageName} is current media source
- * Sets to MediaSource to null if no previous MediaSource exists.
- */
- private static void maybeReplaceMediaSource(CarMediaManager carMediaManager, String packageName,
- Map<ComponentName, ResolveInfo> allMediaServices,
- int mode) {
- if (!isCurrentMediaSource(carMediaManager, packageName, mode)) {
- return;
- }
- //find the most recent source from history not equal to force-stopping package.
- List<ComponentName> mediaSources = carMediaManager.getLastMediaSources(mode);
- ComponentName componentName = mediaSources.stream().filter(c-> (!c.getPackageName()
- .equals(packageName))).findFirst().orElse(null);
- if (componentName == null) {
- //no recent package found, find from all available media services.
- componentName = allMediaServices.keySet().stream().filter(
- c -> (!c.getPackageName().equals(packageName))).findFirst().orElse(null);
- if (componentName == null) {
- Log.e(TAG, "Stop-app, no alternative media service found");
- }
- }
- carMediaManager.setMediaSource(componentName, mode);
- }
-
- /**
- * <p>Note: Uses hidden apis<p/>
- * @return true if the user has restrictions to force stop an app with {@code appInfo}
- */
- private static boolean hasUserRestriction(ApplicationInfo appInfo, Context context) {
- String restriction = UserManager.DISALLOW_APPS_CONTROL;
- UserManager userManager = context.getSystemService(UserManager.class);
- if (userManager == null) {
- Log.e(TAG, " Disabled because , UserManager is null");
- return true;
- }
- if (!userManager.hasUserRestriction(restriction)) {
- return false;
- }
- UserHandle user = UserHandle.getUserHandleForUid(appInfo.uid);
- if (hasBaseUserRestriction(userManager, restriction, user)) {
- Log.d(TAG, " Disabled because " + user + " has " + restriction
- + " restriction");
- return true;
- }
- // Not disabled for this User
- return false;
- }
-
- /**
- * <p>Note: uses hidden apis</p>
- *
- * @param packageName name of the package to stop the app
- * @param context app context
- * @return true if an app should show the Stop app action
- */
- private static boolean shouldAllowStopApp(String packageName, Context context) {
- DevicePolicyManager dm = context.getSystemService(DevicePolicyManager.class);
- if (dm == null || dm.packageHasActiveAdmins(packageName)) {
- return false;
- }
- try {
- ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(packageName,
- PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA));
- // Show only if the User has no restrictions to force stop this app
- if (hasUserRestriction(appInfo, context)) {
- return false;
- }
- // Show only if the app is running
- if ((appInfo.flags & ApplicationInfo.FLAG_STOPPED) == 0) {
- return true;
- }
- } catch (PackageManager.NameNotFoundException e) {
- Log.d(TAG, "shouldAllowStopApp() Package " + packageName + " was not found");
- }
- return false;
- }
-
- private static List<ResolveInfo> getDisabledActivities(Context context,
- PackageManager packageManager, Set<String> enabledPackages) {
- return getActivitiesFromSystemPreferences(
- context,
- packageManager,
- enabledPackages,
- KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE,
- PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS,
- PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR);
- }
-
- private static List<ResolveInfo> getTosDisabledActivities(
- Context context,
- PackageManager packageManager,
- Set<String> enabledPackages) {
- return getActivitiesFromSystemPreferences(
- context,
- packageManager,
- enabledPackages,
- KEY_UNACCEPTED_TOS_DISABLED_APPS,
- PackageManager.MATCH_DISABLED_COMPONENTS,
- TOS_DISABLED_APPS_SEPARATOR);
- }
-
- /**
- * Get a list of activities from packages in system preferences by key
- * @param context the app context
- * @param packageManager The PackageManager
- * @param enabledPackages Set of packages enabled by system
- * @param settingsKey Key to read from system preferences
- * @param sep Separator
- *
- * @return List of activities read from system preferences
- */
- private static List<ResolveInfo> getActivitiesFromSystemPreferences(
- Context context,
- PackageManager packageManager,
- Set<String> enabledPackages,
- String settingsKey,
- int filter,
- String sep) {
- ContentResolver contentResolverForUser = context.createContextAsUser(
- UserHandle.getUserHandleForUid(Process.myUid()), /* flags= */ 0)
- .getContentResolver();
- String settingsValue = Settings.Secure.getString(contentResolverForUser, settingsKey);
- Set<String> packages = TextUtils.isEmpty(settingsValue) ? new ArraySet<>()
- : new ArraySet<>(Arrays.asList(settingsValue.split(
- sep)));
-
- if (packages.isEmpty()) {
- return Collections.emptyList();
- }
-
- List<ResolveInfo> allActivities = packageManager.queryIntentActivities(
- new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER),
- PackageManager.ResolveInfoFlags.of(PackageManager.GET_RESOLVED_FILTER
- | filter));
-
- List<ResolveInfo> activities = new ArrayList<>();
- for (int i = 0; i < allActivities.size(); ++i) {
- ResolveInfo info = allActivities.get(i);
- if (!enabledPackages.contains(info.activityInfo.packageName)
- && packages.contains(info.activityInfo.packageName)) {
- activities.add(info);
- }
- }
- return activities;
- }
-
- private static boolean shouldAddToLaunchables(Context context,
- @NonNull ComponentName componentName,
- @NonNull Set<String> appsToHide,
- @NonNull Set<String> customMediaComponents,
- @AppTypes int appTypesToShow,
- @AppTypes int componentAppType) {
- if (appsToHide.contains(componentName.getPackageName())) {
- return false;
- }
- switch (componentAppType) {
- // Process media services
- case APP_TYPE_MEDIA_SERVICES:
- // For a media service in customMediaComponents, if its application's launcher
- // activity will be shown in the Launcher, don't show the service's icon in the
- // Launcher.
- if (customMediaComponents.contains(componentName.flattenToString())) {
- if ((appTypesToShow & APP_TYPE_LAUNCHABLES) != 0) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "MBS for custom media app " + componentName
- + " is skipped in app launcher");
- }
- return false;
- }
- // Media switcher use case should still show
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "MBS for custom media app " + componentName
- + " is included in media switcher");
- }
- return true;
- }
- // Only Keep MBS that is a media template
- return new MediaSourceUtil(context).isMediaTemplate(componentName);
- // Process activities
- case APP_TYPE_LAUNCHABLES:
- return true;
- default:
- Log.e(TAG, "Invalid componentAppType : " + componentAppType);
- return false;
- }
- }
-
- private static void selectMediaSourceAndFinish(Context context, ComponentName componentName,
- CarMediaManager carMediaManager) {
- try {
- carMediaManager.setMediaSource(componentName, CarMediaManager.MEDIA_SOURCE_MODE_BROWSE);
- if (context instanceof Activity) {
- ((Activity) context).finish();
- }
- } catch (CarNotConnectedException e) {
- Log.e(TAG, "Car not connected", e);
- }
- }
-
- /**
- * Gets if an activity is distraction optimized.
- *
- * @param carPackageManager The {@link CarPackageManager} system service
- * @param packageName The package name of the app
- * @param activityName The requested activity name
- * @return true if the supplied activity is distraction optimized
- */
- static boolean isActivityDistractionOptimized(
- CarPackageManager carPackageManager, String packageName, String activityName) {
- boolean isDistractionOptimized = false;
- // try getting distraction optimization info
- try {
- if (carPackageManager != null) {
- isDistractionOptimized =
- carPackageManager.isActivityDistractionOptimized(packageName, activityName);
- }
- } catch (CarNotConnectedException e) {
- Log.e(TAG, "Car not connected when getting DO info", e);
- }
- return isDistractionOptimized;
- }
-
- /**
- * Callback when a ShortcutsPopup View is shown
- */
- protected interface ShortcutsListener {
-
- void onShortcutsShow(CarUiShortcutsPopup carUiShortcutsPopup);
-
- void onShortcutsItemClick(String packageName, CharSequence displayName,
- boolean allowStopApp);
-
- void onStopAppSuccess(String message);
- }
-
/**
* Returns a set of packages that are disabled by tos
*
@@ -787,28 +94,6 @@
TOS_DISABLED_APPS_SEPARATOR)));
}
- private static Intent createMediaLaunchIntent(ComponentName componentName) {
- return new Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE)
- .putExtra(Car.CAR_EXTRA_MEDIA_COMPONENT, componentName.flattenToString());
- }
-
- private static Intent createAppLaunchIntent(ComponentName componentName) {
- return new Intent(Intent.ACTION_MAIN)
- .setComponent(componentName)
- .addCategory(Intent.CATEGORY_LAUNCHER)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- }
-
- /**
- * Check if the tos banner has to be displayed
- * @param context The application context
- * @return true if the banner needs to be displayed, false otherwise
- */
- static boolean showTosBanner(Context context) {
- // TODO (b/277235742): Add backoff strategy to dismiss banner
- return !tosAccepted(context);
- }
-
/**
* Check if a user has accepted TOS
*
@@ -829,7 +114,6 @@
* Check if TOS status is uninitialized
*
* @param context The application context
- *
* @return true if tos is uninitialized, false otherwise
*/
static boolean tosStatusUninitialized(Context context) {
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/AppMetaData.java b/libs/appgrid/lib/src/com/android/car/carlauncher/AppMetaData.java
index bd57b4e..1e65e59 100644
--- a/libs/appgrid/lib/src/com/android/car/carlauncher/AppMetaData.java
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/AppMetaData.java
@@ -127,7 +127,9 @@
return false;
} else {
return ((AppMetaData) o).getComponentName().equals(mComponentName)
- && ((AppMetaData) o).getIsMirroring() == mIsMirroring;
+ && ((AppMetaData) o).getIsMirroring() == mIsMirroring
+ && ((AppMetaData) o).getIsDisabledByTos() == mIsDisabledByTos
+ && ((AppMetaData) o).getIsDistractionOptimized() == mIsDistractionOptimized;
}
}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/BackgroundAnimationHelper.kt b/libs/appgrid/lib/src/com/android/car/carlauncher/BackgroundAnimationHelper.kt
new file mode 100644
index 0000000..23c834c
--- /dev/null
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/BackgroundAnimationHelper.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024 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.carlauncher
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
+import android.view.View
+import android.widget.LinearLayout
+
+/**
+ * Animation Helper for animating elements on the appGrid
+ */
+class BackgroundAnimationHelper(
+ private val appsGridBackground: LinearLayout,
+ private val banner: Banner
+) {
+ private var bannerAnimatorSet = AnimatorSet()
+
+ fun showBanner() {
+ banner.visibility = View.VISIBLE
+ val bannerTranslateAnimator = ValueAnimator.ofInt(-banner.measuredHeight, 0)
+ val bannerAlphaAnimator = ObjectAnimator.ofFloat(banner, "alpha", 1f)
+ val appsGridTranslateAnimator = ValueAnimator.ofInt(0, banner.measuredHeight)
+
+ animateBanner(bannerTranslateAnimator, bannerAlphaAnimator, appsGridTranslateAnimator)
+ }
+ fun hideBanner() {
+ val bannerTranslateAnimator = ValueAnimator.ofInt(0, -banner.measuredHeight)
+ val bannerAlphaAnimator = ObjectAnimator.ofFloat(banner, "alpha", 0f)
+ val appsGridTranslateAnimator = ValueAnimator.ofInt(banner.measuredHeight, 0)
+
+ bannerTranslateAnimator.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ super.onAnimationEnd(animation)
+ banner.visibility = View.GONE
+ }
+ })
+
+ animateBanner(bannerTranslateAnimator, bannerAlphaAnimator, appsGridTranslateAnimator)
+ }
+
+ private fun animateBanner(
+ bannerTranslateAnimator: ValueAnimator,
+ bannerAlphaAnimator: ObjectAnimator,
+ appsGridTranslateAnimator: ValueAnimator
+ ) {
+ bannerTranslateAnimator.addUpdateListener { banner.y = (it.animatedValue as Int).toFloat() }
+ appsGridTranslateAnimator
+ .addUpdateListener { appsGridBackground.y = (it.animatedValue as Int).toFloat() }
+
+ bannerAnimatorSet.cancel()
+ bannerAnimatorSet = AnimatorSet().apply {
+ duration = BANNER_ANIMATION_DURATION_MS
+ startDelay = BANNER_ANIMATION_START_DELAY_MS
+ playTogether(
+ bannerTranslateAnimator,
+ bannerAlphaAnimator,
+ appsGridTranslateAnimator
+ )
+ }
+ bannerAnimatorSet.start()
+ }
+
+ private companion object {
+ const val BANNER_ANIMATION_DURATION_MS = 300L
+ const val BANNER_ANIMATION_START_DELAY_MS = 400L
+ }
+}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/Banner.java b/libs/appgrid/lib/src/com/android/car/carlauncher/Banner.java
index ebfd7f2..3382bf4 100644
--- a/libs/appgrid/lib/src/com/android/car/carlauncher/Banner.java
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/Banner.java
@@ -58,8 +58,8 @@
LayoutInflater inflater = LayoutInflater.from(getContext());
inflater.inflate(R.layout.banner, this);
- mFirstButton = requireViewById(R.id.first_button);
- mSecondButton = requireViewById(R.id.second_button);
+ mFirstButton = requireViewById(R.id.banner_first_button);
+ mSecondButton = requireViewById(R.id.banner_second_button);
mTitleTextView = requireViewById(R.id.banner_title);
TypedArray attrArray = context.getTheme().obtainStyledAttributes(
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/LauncherItemMessageHelper.java b/libs/appgrid/lib/src/com/android/car/carlauncher/LauncherItemMessageHelper.java
deleted file mode 100644
index 6548725..0000000
--- a/libs/appgrid/lib/src/com/android/car/carlauncher/LauncherItemMessageHelper.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2023 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.carlauncher;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.carlauncher.LauncherItemProto.LauncherItemListMessage;
-import com.android.car.carlauncher.LauncherItemProto.LauncherItemMessage;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
-/**
- * Helper class that provides method used by LauncherModel
- */
-public class LauncherItemMessageHelper {
- /**
- * Convert a List of {@link LauncherItemMessage} to a single {@link LauncherItemListMessage}.
- */
- @Nullable
- public LauncherItemListMessage convertToMessage(List<LauncherItemMessage> msgList) {
- if (msgList == null) {
- return null;
- }
- LauncherItemListMessage.Builder builder =
- LauncherItemListMessage.newBuilder().addAllLauncherItemMessage(msgList);
- return builder.build();
- }
-
- /**
- * Converts {@link LauncherItemListMessage} to a List of {@link LauncherItemMessage},
- * sorts the LauncherItemList based on their relative order in the file, then return the list.
- */
- @NonNull
- public List<LauncherItemMessage> getSortedList(@Nullable LauncherItemListMessage protoLstMsg) {
- if (protoLstMsg == null) {
- return new ArrayList<>();
- }
- List<LauncherItemMessage> itemMsgList = protoLstMsg.getLauncherItemMessageList();
- List<LauncherItemMessage> sortedItemMsgList = new ArrayList<>();
- if (!itemMsgList.isEmpty() && itemMsgList.size() > 0) {
- // need to create a new list for sorting purposes since ProtobufArrayList is not mutable
- sortedItemMsgList.addAll(itemMsgList);
- Collections.sort(sortedItemMsgList,
- Comparator.comparingInt(LauncherItemMessage::getRelativePosition));
- }
- return sortedItemMsgList;
- }
-}
-
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/LauncherViewModel.java b/libs/appgrid/lib/src/com/android/car/carlauncher/LauncherViewModel.java
deleted file mode 100644
index 80602ab..0000000
--- a/libs/appgrid/lib/src/com/android/car/carlauncher/LauncherViewModel.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2023 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.carlauncher;
-
-import android.content.ComponentName;
-import android.content.Intent;
-
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.ViewModel;
-
-import com.android.car.carlauncher.apporder.AppOrderController;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * A launcher model decides how the apps are displayed.
- */
-public class LauncherViewModel extends ViewModel {
- private final AppOrderController mAppOrderController;
-
- public LauncherViewModel(File launcherFileDir) {
- mAppOrderController = new AppOrderController(launcherFileDir);
- }
-
- public static final Comparator<LauncherItem> ALPHABETICAL_COMPARATOR = Comparator.comparing(
- LauncherItem::getDisplayName, String::compareToIgnoreCase);
-
- public LiveData<List<LauncherItem>> getCurrentLauncher() {
- return mAppOrderController.getAppOrderObservable();
- }
-
- /**
- * Read in apps order from file if exists, then publish app order to UI if valid.
- */
- public void loadAppsOrderFromFile() {
- mAppOrderController.loadAppOrderFromFile();
- }
-
- /**
- * Populate the apps based on alphabetical order and create mapping from packageName to
- * LauncherItem. Each item in the current launcher is AppItem.
- */
- public void processAppsInfoFromPlatform(AppLauncherUtils.LauncherAppsInfo launcherAppsInfo) {
- Map<ComponentName, LauncherItem> launcherItemsMap = new HashMap<>();
- List<LauncherItem> launcherItems = new ArrayList<>();
- List<AppMetaData> appMetaDataList = launcherAppsInfo.getLaunchableComponentsList();
- for (AppMetaData appMetaData : appMetaDataList) {
- LauncherItem nextItem = new AppItem(appMetaData);
- launcherItems.add(nextItem);
- launcherItemsMap.put(appMetaData.getComponentName(), nextItem);
- }
- Collections.sort(launcherItems, LauncherViewModel.ALPHABETICAL_COMPARATOR);
- mAppOrderController.loadAppListFromPlatform(launcherItemsMap, launcherItems);
- }
-
- /**
- * Notifies the controller that a change in the data model has been observed by the user
- * interface (e.g. platform apps list has been updated, user has updated the app order.)
- *
- * The controller should ONLY handle writing to disk in this method. This will ensure that all
- * changes to the data model is consistent with the user interface.
- */
- public void handleAppListChange() {
- mAppOrderController.handleAppListChange();
- }
-
- /**
- * Notifies the controller to move the given AppItem to a new position in the data model.
- */
- public void setAppPosition(int position, AppMetaData app) {
- mAppOrderController.setAppPosition(position, app);
- }
-
- /**
- * Updates the launcher data model when app mirroring intent is received.
- */
- public void updateMirroringItem(String packageName, Intent mirroringIntent) {
- mAppOrderController.updateMirroringItem(packageName, mirroringIntent);
- }
-}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/LauncherViewModelFactory.java b/libs/appgrid/lib/src/com/android/car/carlauncher/LauncherViewModelFactory.java
deleted file mode 100644
index 0abd622..0000000
--- a/libs/appgrid/lib/src/com/android/car/carlauncher/LauncherViewModelFactory.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2023 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.carlauncher;
-
-import androidx.lifecycle.ViewModel;
-import androidx.lifecycle.ViewModelProvider;
-
-import java.io.File;
-
-/** A factory class to allow creation of LauncherViewModel by ViewModelProvider. */
-public class LauncherViewModelFactory implements ViewModelProvider.Factory{
- private File mLauncherFileDir;
-
- public LauncherViewModelFactory(File launcherFileDir) {
- mLauncherFileDir = launcherFileDir;
- }
-
- @Override
- public <T extends ViewModel> T create(Class<T> modelClass) {
- return (T) new LauncherViewModel(mLauncherFileDir);
- }
-}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/MediaSessionUtils.java b/libs/appgrid/lib/src/com/android/car/carlauncher/MediaSessionUtils.java
new file mode 100644
index 0000000..b296f62
--- /dev/null
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/MediaSessionUtils.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 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.carlauncher;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.RemoteException;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+
+import com.android.car.media.common.source.MediaModels;
+import com.android.car.media.common.source.MediaSessionHelper;
+
+/** Utility class that handles common MediaSession related logic*/
+public class MediaSessionUtils {
+ private static final String TAG = "MediaSessionUtils";
+
+ private MediaSessionUtils() {}
+
+ /** Create a MediaModels object */
+ public static MediaModels getMediaModels(Context context) {
+ return new MediaModels(context.getApplicationContext(),
+ createNotificationProvider(context));
+ }
+
+ /** Create a MediaSessionHelper object */
+ public static MediaSessionHelper getMediaSessionHelper(Context context) {
+ return new MediaSessionHelper(context.getApplicationContext(),
+ createNotificationProvider(context));
+ }
+
+ private static MediaSessionHelper.NotificationProvider createNotificationProvider(
+ Context context) {
+ return new MediaSessionHelper.NotificationProvider() {
+ @Override
+ public StatusBarNotification[] getActiveNotifications() {
+ try {
+ return NotificationManager.getService()
+ .getActiveNotificationsWithAttribution(
+ context.getPackageName(), null);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Exception trying to get active notifications " + e);
+ return new StatusBarNotification[0];
+ }
+ }
+
+ @Override
+ public boolean isMediaNotification(Notification notification) {
+ return notification.isMediaNotification();
+ }
+ };
+ }
+}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/RecentAppsRowViewHolder.java b/libs/appgrid/lib/src/com/android/car/carlauncher/RecentAppsRowViewHolder.java
index e5a1c9d..c1f7776 100644
--- a/libs/appgrid/lib/src/com/android/car/carlauncher/RecentAppsRowViewHolder.java
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/RecentAppsRowViewHolder.java
@@ -88,4 +88,3 @@
}
}
-
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/ResetLauncherActivity.java b/libs/appgrid/lib/src/com/android/car/carlauncher/ResetLauncherActivity.java
index efd098a..75c5157 100644
--- a/libs/appgrid/lib/src/com/android/car/carlauncher/ResetLauncherActivity.java
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/ResetLauncherActivity.java
@@ -18,7 +18,6 @@
import android.app.AlertDialog;
import android.os.Bundle;
-import com.android.car.carlauncher.apporder.AppOrderController;
import com.android.car.ui.AlertDialogBuilder;
import java.io.File;
@@ -39,7 +38,7 @@
.setTitle(getString(R.string.reset_appgrid_title))
.setMessage(getString(R.string.reset_appgrid_dialogue_message))
.setPositiveButton(getString(android.R.string.ok), (dialogInterface, which) -> {
- File order = new File(filesDir, AppOrderController.ORDER_FILE_NAME);
+ File order = new File(filesDir, "order.data");
order.delete();
finish();
})
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/apporder/AppOrderController.java b/libs/appgrid/lib/src/com/android/car/carlauncher/apporder/AppOrderController.java
deleted file mode 100644
index 2bc0750..0000000
--- a/libs/appgrid/lib/src/com/android/car/carlauncher/apporder/AppOrderController.java
+++ /dev/null
@@ -1,264 +0,0 @@
-/*
- * Copyright (C) 2023 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.carlauncher.apporder;
-
-import android.content.ComponentName;
-import android.content.Intent;
-
-import androidx.annotation.VisibleForTesting;
-import androidx.lifecycle.MutableLiveData;
-
-import com.android.car.carlauncher.AppItem;
-import com.android.car.carlauncher.AppLauncherUtils;
-import com.android.car.carlauncher.AppMetaData;
-import com.android.car.carlauncher.LauncherItem;
-import com.android.car.carlauncher.LauncherItemMessageHelper;
-import com.android.car.carlauncher.LauncherItemProto.LauncherItemListMessage;
-import com.android.car.carlauncher.LauncherItemProto.LauncherItemMessage;
-import com.android.car.carlauncher.datastore.DataSourceController;
-import com.android.car.carlauncher.datastore.launcheritem.LauncherItemListSource;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-/**
- * Controller that manages the ordering of the app items in app grid.
- */
-public class AppOrderController implements DataSourceController {
- // file name holding the user customized app order
- public static final String ORDER_FILE_NAME = "order.data";
- private final LauncherItemMessageHelper mItemHelper = new LauncherItemMessageHelper();
- // The app order of launcher items displayed to users
- private final MutableLiveData<List<LauncherItem>> mCurrentAppList;
- private final Map<ComponentName, LauncherItem> mLauncherItemMap = new HashMap<>();
- private final List<ComponentName> mProtoComponentNames = new ArrayList<>();
- private final List<LauncherItem> mDefaultOrder;
- private final List<LauncherItem> mCustomizedOrder;
- private final LauncherItemListSource mDataSource;
- private boolean mPlatformAppListLoaded;
- private boolean mCustomAppOrderFetched;
- private boolean mIsUserCustomized;
-
- public AppOrderController(File dataFileDirectory) {
- this(/* dataSource */ new LauncherItemListSource(dataFileDirectory, ORDER_FILE_NAME),
- /* appList */ new MutableLiveData<>(new ArrayList<>()),
- /* defaultOrder */ new ArrayList<>(),
- /* customizedOrder*/ new ArrayList<>());
- }
-
- public AppOrderController(LauncherItemListSource dataSource,
- MutableLiveData<List<LauncherItem>> appList, List<LauncherItem> defaultOrder,
- List<LauncherItem> customizedOrder) {
- mDataSource = dataSource;
- mCurrentAppList = appList;
- mDefaultOrder = defaultOrder;
- mCustomizedOrder = customizedOrder;
- }
-
- @Override
- public boolean checkDataSourceExists() {
- return mDataSource.exists();
- }
-
- public MutableLiveData<List<LauncherItem>> getAppOrderObservable() {
- return mCurrentAppList;
- }
-
- /**
- * Loads the full app list to be displayed in the app grid.
- */
- public void loadAppListFromPlatform(Map<ComponentName, LauncherItem> launcherItemsMap,
- List<LauncherItem> defaultItemOrder) {
- mDefaultOrder.clear();
- mDefaultOrder.addAll(defaultItemOrder);
- mLauncherItemMap.clear();
- mLauncherItemMap.putAll(launcherItemsMap);
- mPlatformAppListLoaded = true;
- maybePublishAppList();
- }
-
- /**
- * Loads any preexisting app order from the proto datastore on disk.
- */
- public void loadAppOrderFromFile() {
- // handle the app order reset case, where the proto file is removed from file system
- maybeHandleAppOrderReset();
- mProtoComponentNames.clear();
- List<LauncherItemMessage> protoItemMessage = mItemHelper.getSortedList(
- mDataSource.readFromFile());
- if (!protoItemMessage.isEmpty()) {
- mIsUserCustomized = true;
- for (LauncherItemMessage itemMessage : protoItemMessage) {
- ComponentName itemComponent = new ComponentName(
- itemMessage.getPackageName(), itemMessage.getClassName());
- mProtoComponentNames.add(itemComponent);
- }
- }
- mCustomAppOrderFetched = true;
- maybePublishAppList();
- }
-
- @VisibleForTesting
- void maybeHandleAppOrderReset() {
- if (!checkDataSourceExists()) {
- mIsUserCustomized = false;
- mCustomizedOrder.clear();
- }
- }
-
- /**
- * Combine the proto order read from proto with any additional apps read from the platform, then
- * publish the new list to user interface.
- *
- * Prior to publishing the app list to the LiveData (and subsequently to the UI), both (1) the
- * default platform mapping and (2) user customized order must be read into memory. These
- * pre-fetch methods may be executed on different threads, so we should only publish the final
- * ordering when both steps have completed.
- */
- @VisibleForTesting
- void maybePublishAppList() {
- if (!appsDataLoadingCompleted()) {
- return;
- }
- // app names found in order proto file will be displayed first
- mCustomizedOrder.clear();
- List<LauncherItem> customOrder = new ArrayList<>();
- Set<ComponentName> namesFoundInProto = new HashSet<>();
- for (ComponentName name: mProtoComponentNames) {
- if (mLauncherItemMap.containsKey(name)) {
- customOrder.add(mLauncherItemMap.get(name));
- namesFoundInProto.add(name);
- }
- }
- mCustomizedOrder.addAll(customOrder);
- if (shouldUseCustomOrder()) {
- // new apps from platform not found in proto will be added to the end
- mCustomizedOrder.clear();
- List<ComponentName> newPlatformApps = mLauncherItemMap.keySet()
- .stream()
- .filter(element -> !namesFoundInProto.contains(element))
- .collect(Collectors.toList());
- if (!newPlatformApps.isEmpty()) {
- Collections.sort(newPlatformApps);
- for (ComponentName newAppName: newPlatformApps) {
- customOrder.add(mLauncherItemMap.get(newAppName));
- }
- }
- mCustomizedOrder.addAll(customOrder);
- mCurrentAppList.postValue(customOrder);
- } else {
- mCurrentAppList.postValue(mDefaultOrder);
- mCustomizedOrder.clear();
- }
- // reset apps data loading flags
- mPlatformAppListLoaded = mCustomAppOrderFetched = false;
- }
-
- @VisibleForTesting
- boolean appsDataLoadingCompleted() {
- return mPlatformAppListLoaded && mCustomAppOrderFetched;
- }
-
- @VisibleForTesting
- boolean shouldUseCustomOrder() {
- return mIsUserCustomized && mCustomizedOrder.size() != 0;
- }
-
- /**
- * Persistently writes the current in memory app order into disk.
- */
- public void handleAppListChange() {
- if (mIsUserCustomized) {
- List<LauncherItem> currentItems = mCurrentAppList.getValue();
- List<LauncherItemMessage> msgList = new ArrayList<LauncherItemMessage>();
- for (int i = 0; i < currentItems.size(); i++) {
- msgList.add(currentItems.get(i).convertToMessage(i, -1));
- }
- LauncherItemListMessage appOrderListMessage = mItemHelper.convertToMessage(msgList);
- mDataSource.writeToFile(appOrderListMessage);
- }
- }
-
- /**
- * Move an app to a specified index and post the value to LiveData.
- */
- public void setAppPosition(int position, AppMetaData app) {
- List<LauncherItem> current = mCurrentAppList.getValue();
- LauncherItem item = mLauncherItemMap.get(app.getComponentName());
- if (current != null && current.size() != 0 && position < current.size() && item != null) {
- mIsUserCustomized = true;
- current.remove(item);
- current.add(position, item);
- mCurrentAppList.postValue(current);
- }
- }
-
- /**
- * Handles the incoming mirroring intent from ViewModel.
- *
- * Update an AppItem's AppMetaData isMirroring state and its launch callback then post the
- * updated to LiveData.
- */
- public void updateMirroringItem(String packageName, Intent mirroringIntent) {
- List<LauncherItem> launcherList = mCurrentAppList.getValue();
- if (launcherList == null) {
- return;
- }
- List<LauncherItem> launcherListCopy = new ArrayList<>();
- for (LauncherItem item : launcherList) {
- if (item instanceof AppItem) {
- // TODO (b/272796126): move deep copying to inside DiffUtil
- AppMetaData metaData = ((AppItem) item).getAppMetaData();
- if (item.getPackageName().equals(packageName)) {
- launcherListCopy.add(new AppItem(item.getPackageName(), item.getClassName(),
- item.getDisplayName(), new AppMetaData(metaData.getDisplayName(),
- metaData.getComponentName(), metaData.getIcon(),
- metaData.getIsDistractionOptimized(), /* isMirroring= */ true,
- metaData.getIsDisabledByTos(),
- contextArg ->
- AppLauncherUtils.launchApp(contextArg, mirroringIntent),
- metaData.getAlternateLaunchCallback())));
- } else if (metaData.getIsMirroring()) {
- Intent intent = new Intent(Intent.ACTION_MAIN)
- .setComponent(metaData.getComponentName())
- .addCategory(Intent.CATEGORY_LAUNCHER)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- launcherListCopy.add(new AppItem(item.getPackageName(), item.getClassName(),
- item.getDisplayName(), new AppMetaData(metaData.getDisplayName(),
- metaData.getComponentName(), metaData.getIcon(),
- metaData.getIsDistractionOptimized(), /* isMirroring= */ false,
- metaData.getIsDisabledByTos(),
- contextArg ->
- AppLauncherUtils.launchApp(contextArg, intent),
- metaData.getAlternateLaunchCallback())));
- } else {
- launcherListCopy.add(item);
- }
- } else {
- launcherListCopy.add(item);
- }
- }
- mCurrentAppList.postValue(launcherListCopy);
- }
-}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/AppOrderDataSource.kt b/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/AppOrderDataSource.kt
new file mode 100644
index 0000000..2ad0902
--- /dev/null
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/AppOrderDataSource.kt
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.datasources
+
+import android.util.Log
+import com.android.car.carlauncher.LauncherItemProto
+import com.android.car.carlauncher.LauncherItemProto.LauncherItemMessage
+import com.android.car.carlauncher.datasources.AppOrderDataSource.AppOrderInfo
+import com.android.car.carlauncher.datastore.launcheritem.LauncherItemListSource
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.emitAll
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
+
+/**
+ * DataSource for managing the persisted order of apps. This class encapsulates all
+ * interactions with the persistent storage (e.g., Files, Proto, Database), acting as
+ * the single source of truth.
+ *
+ * Important: To ensure consistency, avoid modifying the persistent storage directly.
+ * Use the methods provided by this DataSource.
+ */
+interface AppOrderDataSource {
+
+ /**
+ * Saves the provided app order to persistent storage.
+ *
+ * @param appOrderInfoList The new order of apps to be saved, represented as a list of
+ * LauncherItemMessage objects.
+ */
+ suspend fun saveAppOrder(appOrderInfoList: List<AppOrderInfo>)
+
+ /**
+ * Returns a Flow of the saved app order. The Flow will emit the latest saved order
+ * and any subsequent updates.
+ *
+ * @return A Flow of [AppOrderInfo] lists, representing the saved app order.
+ */
+ fun getSavedAppOrder(): Flow<List<AppOrderInfo>>
+
+ /**
+ * Returns a Flow of comparators for sorting app lists. The comparators will prioritize the
+ * saved app order, and may fall back to other sorting logic if necessary.
+ *
+ * @return A Flow of Comparator objects, used to sort [AppOrderInfo] lists.
+ */
+ fun getSavedAppOrderComparator(): Flow<Comparator<AppOrderInfo>>
+
+ /**
+ * Clears the saved app order from persistent storage.
+ *
+ * @return `true` if the operation was successful, `false` otherwise.
+ */
+ suspend fun clearAppOrder(): Boolean
+
+ data class AppOrderInfo(val packageName: String, val className: String, val displayName: String)
+}
+
+/**
+ * Implementation of the [AppOrderDataSource] interface, responsible for managing app order
+ * persistence using a Proto file storage mechanism.
+ *
+ * @property launcherItemListSource The source for accessing and updating the raw Proto data.
+ * @property bgDispatcher (Optional) A CoroutineDispatcher specifying the thread pool for background
+ * operations (defaults to Dispatchers.IO for I/O-bound tasks).
+ */
+class AppOrderProtoDataSourceImpl(
+ private val launcherItemListSource: LauncherItemListSource,
+ private val bgDispatcher: CoroutineDispatcher = Dispatchers.IO,
+) : AppOrderDataSource {
+
+ private val appOrderFlow = MutableStateFlow(emptyList<AppOrderInfo>())
+
+ /**
+ * Saves the current app order to a Proto file for persistent storage.
+ * * Performs the save operation on the background dispatcher ([bgDispatcher]).
+ * * Updates all collectors of [getSavedAppOrderComparator] and [getSavedAppOrder] immediately,
+ * even before the write operation has completed.
+ * * In case of a write failure, the operation fails silently. This might lead to a
+ * temporarily inconsistent app order for the current session (until the app restarts).
+ */
+ override suspend fun saveAppOrder(appOrderInfoList: List<AppOrderInfo>) {
+ // Immediately update the cache.
+ appOrderFlow.value = appOrderInfoList
+ // Store the app order persistently.
+ withContext(bgDispatcher) {
+ // If it fails to write, it fails silently.
+ if (!launcherItemListSource.writeToFile(
+ LauncherItemProto.LauncherItemListMessage.newBuilder()
+ .addAllLauncherItemMessage(appOrderInfoList.mapIndexed { index, item ->
+ convertToMessage(item, index)
+ }).build()
+ )
+ ) {
+ Log.i(TAG, "saveAppOrder failed to writeToFile")
+ }
+ }
+ }
+
+ /**
+ * Gets the latest know saved order to sort the apps.
+ * Also check [getSavedAppOrderComparator] if you need comparator to sort the list of apps.
+ *
+ * * Emits a new list to all collectors whenever the app order is updated using the
+ * [saveAppOrder] function or when [clearAppOrder] is called.
+ *
+ * __Handling Apps with Unknown Positions:__
+ * The client should implement logic to handle apps whose positions are not
+ * specified in the saved order. A common strategy is to append them to the end of the list.
+ *
+ * __Handling Unavailable Apps:__
+ * The client can choose to exclude apps that are unavailable (e.g., uninstalled or disabled)
+ * from the sorted list.
+ */
+ override fun getSavedAppOrder(): Flow<List<AppOrderInfo>> = flow {
+ withContext(bgDispatcher) {
+ val appOrderFromFiles = launcherItemListSource.readFromFile()?.launcherItemMessageList
+ // Read from the persistent storage for pre-existing order.
+ // If no pre-existing order exists it initially returns an emptyList.
+ if (!appOrderFromFiles.isNullOrEmpty()) {
+ appOrderFlow.value =
+ appOrderFromFiles.sortedBy { it.relativePosition }
+ .map { AppOrderInfo(it.packageName, it.className, it.displayName) }
+ }
+ }
+ emitAll(appOrderFlow)
+ }.flowOn(bgDispatcher)
+
+ /**
+ * Provides a Flow of comparators to sort a list of apps.
+ *
+ * * Sorts apps based on a pre-defined order. If an app is not found in the pre-defined
+ * order, it falls back to alphabetical sorting with [AppOrderInfo.displayName].
+ * * Emits a new comparator to all collectors whenever the app order is updated using the
+ * [saveAppOrder] function or when [clearAppOrder] is called.
+ *
+ * @see getSavedAppOrder
+ */
+ override fun getSavedAppOrderComparator(): Flow<Comparator<AppOrderInfo>> {
+ return getSavedAppOrder().map { appOrderInfoList ->
+ val appOrderMap = appOrderInfoList.withIndex().associateBy({it.value}, {it.index})
+ Comparator<AppOrderInfo> { app1, app2 ->
+ when {
+ // Both present in predefined list.
+ appOrderMap.contains(app1) && appOrderMap.contains(app2) -> {
+ // Kotlin compiler complains for nullability, although this should not be.
+ appOrderMap[app1]!! - appOrderMap[app2]!!
+ }
+ // Prioritize predefined names.
+ appOrderMap.contains(app1) -> -1
+ appOrderMap.contains(app2) -> 1
+ // Fallback to alphabetical.
+ else -> app1.displayName.compareTo(app2.displayName)
+ }
+ }
+ }.flowOn(bgDispatcher)
+ }
+
+ /**
+ * Deletes the persisted app order data. Performs the file deletion operation on the
+ * background dispatcher ([bgDispatcher]).
+ *
+ * * Successful deletion will report empty/default order [emptyList] to collectors of
+ * [getSavedAppOrder] amd [getSavedAppOrderComparator]
+ *
+ * @return `true` if the deletion was successful, `false` otherwise.
+ */
+ override suspend fun clearAppOrder(): Boolean {
+ return withContext(bgDispatcher) {
+ launcherItemListSource.deleteFile()
+ }.also {
+ if (it) {
+ // If delete is successful report empty app order.
+ appOrderFlow.value = emptyList()
+ }
+ }
+ }
+
+ private fun convertToMessage(
+ appOrderInfo: AppOrderInfo,
+ relativePosition: Int
+ ): LauncherItemMessage? {
+ val builder = LauncherItemMessage.newBuilder().setPackageName(appOrderInfo.packageName)
+ .setClassName(appOrderInfo.className).setDisplayName(appOrderInfo.displayName)
+ .setRelativePosition(relativePosition).setContainerID(DOES_NOT_SUPPORT_CONTAINER)
+ return builder.build()
+ }
+
+ companion object {
+ val TAG: String = AppOrderDataSource::class.java.simpleName
+ private const val DOES_NOT_SUPPORT_CONTAINER = -1
+ }
+}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/ControlCenterMirroringDataSource.kt b/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/ControlCenterMirroringDataSource.kt
new file mode 100644
index 0000000..6d8f15b
--- /dev/null
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/ControlCenterMirroringDataSource.kt
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.datasources
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.content.pm.PackageManager
+import android.content.res.Resources
+import android.os.Bundle
+import android.os.Handler
+import android.os.IBinder
+import android.os.Looper
+import android.os.Message
+import android.os.Messenger
+import android.os.RemoteException
+import android.util.Log
+import com.android.car.carlauncher.R
+import com.android.car.carlauncher.datasources.ControlCenterMirroringDataSource.MirroringPackageData
+import java.net.URISyntaxException
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.channels.ProducerScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOn
+
+/**
+ * DataSource interface tells if there is an active Mirroring-Session.
+ */
+interface ControlCenterMirroringDataSource {
+
+ /**
+ * @return Flow of [MirroringPackageData] which sends the active Mirroring packageName and
+ * redirect launchIntent which launches the application in the
+ * MirroringActivity
+ */
+ fun getAppMirroringSession(): Flow<MirroringPackageData>
+
+ data class MirroringPackageData(
+ val packageName: String,
+ val launchIntent: Intent
+ ) {
+ companion object {
+ // signifies active mirroring session
+ val NO_MIRRORING = MirroringPackageData("", Intent())
+ }
+ }
+}
+
+/**
+ * Impl of [ControlCenterMirroringDataSource] to surface all the control center mirroring session
+ * All the operations in this class are non blocking.
+ *
+ * The Application using this Datasource is expected to define the following configs
+ * in its resources. The implementation uses [resources] to fetch these configs.
+ * These configs should be bound application's lifecycle and are not expected to change with
+ * Activity's lifecycle events.
+ *
+ * __config_msg_mirroring_service_pkg_name__: String value stating the package name of the
+ * mirroring service.
+ *
+ * __config_msg_mirroring_service_class_name__: String value stating the class name of the
+ * mirroring service.
+ *
+ * __config_msg_register_mirroring_pkg_code__: Integer unique key to register the service.
+ *
+ * __config_msg_unregister_mirroring_pkg_code__: Integer unique key to unregister the service.
+ *
+ * __config_msg_send_mirroring_pkg_code__: Integer unique key to send mirroring packet across the
+ * service.
+ *
+ * __config_msg_mirroring_pkg_name_key__: String unique key to send packageName of the active
+ * mirroring session.
+ *
+ * __config_msg_mirroring_redirect_uri_key__: String unique key to send the redirect uri of the
+ * mirroring activity.
+ *
+ * @property [resources] Application resources, not bound to activity's configuration changes.
+ * @property [bindService] Function to register service.
+ * Should be provided by an Android Component owning the [Context].
+ * @property [unBindService] Function to unregister the broadcast receiver.
+ * Should be provided by the Android Component owning the [Context].
+ * @property [packageManager] Used to resolve the bounded Service.
+ * @property [bgDispatcher] Executes all the operations on this background coroutine dispatcher.
+ *
+ */
+class ControlCenterMirroringDataSourceImpl(
+ private val resources: Resources,
+ private val bindService: (Intent, MirroringServiceConnection, flags: Int) -> Unit,
+ private val unBindService: (MirroringServiceConnection) -> Unit,
+ private val packageManager: PackageManager,
+ private val bgDispatcher: CoroutineDispatcher = Dispatchers.IO
+) : ControlCenterMirroringDataSource {
+
+ /**
+ * @return Flow of [MirroringPackageData] reporting current active mirroring session.
+ *
+ * Note: The producer sends an [MirroringPackageData.NO_MIRRORING] initially.
+ * This immediately tells the collector that there are no changes as of now with packages.
+ *
+ * When the scope in which this flow is collected is closed/canceled
+ * [unBindService] is triggered.
+ */
+ override fun getAppMirroringSession(): Flow<MirroringPackageData> {
+ return callbackFlow {
+ // Send empty mirroring packet to signify that no mirroring is ongoing
+ trySend(MirroringPackageData.NO_MIRRORING)
+ val looper = Looper.getMainLooper()
+ val clientMessenger = getReceiverMessenger(looper, this)
+ val serviceConnection = getMirroringConnectionService(clientMessenger, this)
+ registerReceiver(serviceConnection, this)
+
+ awaitClose {
+ unregisterReceiver(serviceConnection)
+ }
+ }.flowOn(bgDispatcher).conflate()
+ }
+
+ private fun getReceiverMessenger(
+ looper: Looper,
+ producerScope: ProducerScope<MirroringPackageData>
+ ): Messenger {
+ return Messenger(object : Handler(looper) {
+ private val senderMirroringPkgCode =
+ resources.getInteger(R.integer.config_msg_send_mirroring_pkg_code)
+ private val mirroringPkgNameKey =
+ resources.getString(R.string.config_msg_mirroring_pkg_name_key)
+ private val mirroringRedirectUriKey =
+ resources.getString(R.string.config_msg_mirroring_redirect_uri_key)
+
+ override fun handleMessage(msg: Message) {
+ if (msg.what != senderMirroringPkgCode) {
+ super.handleMessage(msg)
+ return
+ }
+ val bundle = msg.obj as Bundle
+ val mirroringPackageName = bundle.getString(mirroringPkgNameKey)
+ if (mirroringPackageName.isNullOrEmpty()) {
+ producerScope.trySend(MirroringPackageData.NO_MIRRORING)
+ return
+ }
+ try {
+ val mirroringIntentRedirect = Intent.parseUri(
+ bundle.getString(mirroringRedirectUriKey),
+ Intent.URI_INTENT_SCHEME
+ )
+ producerScope.trySend(
+ MirroringPackageData(
+ mirroringPackageName,
+ mirroringIntentRedirect
+ )
+ )
+ } catch (e: URISyntaxException) {
+ Log.d(TAG, "Error parsing mirroring redirect intent $e")
+ }
+ }
+ })
+ }
+
+ abstract class MirroringServiceConnection : ServiceConnection {
+ var mServiceMessenger: Messenger? = null
+ var mClientMessenger: Messenger? = null
+ }
+
+ private fun getMirroringConnectionService(
+ clientMessenger: Messenger,
+ producerScope: ProducerScope<MirroringPackageData>
+ ): MirroringServiceConnection {
+ return object : MirroringServiceConnection() {
+ init {
+ mClientMessenger = clientMessenger
+ }
+
+ override fun onServiceConnected(name: ComponentName, service: IBinder) {
+ mServiceMessenger = Messenger(service)
+ val msg: Message = Message.obtain(
+ null,
+ resources.getInteger(R.integer.config_msg_register_mirroring_pkg_code)
+ )
+ msg.replyTo = mClientMessenger
+ try {
+ mServiceMessenger?.send(msg)
+ } catch (e: RemoteException) {
+ Log.d(TAG, "Exception sending message to mirroring service: $e")
+ }
+ }
+
+ override fun onServiceDisconnected(name: ComponentName) {
+ producerScope.cancel("Mirroring Service disconnected")
+ }
+ }
+ }
+
+ private fun registerReceiver(
+ mirroringConnectionService: MirroringServiceConnection,
+ producerScope: ProducerScope<MirroringPackageData>
+ ) {
+ try {
+ val intent = Intent()
+ intent.component = ComponentName(
+ resources.getString(R.string.config_msg_mirroring_service_pkg_name),
+ resources.getString(R.string.config_msg_mirroring_service_class_name)
+ )
+ if (packageManager.resolveService(intent, 0) != null) {
+ bindService(
+ intent,
+ mirroringConnectionService,
+ Context.BIND_AUTO_CREATE or Context.BIND_IMPORTANT
+ )
+ }
+ } catch (e: SecurityException) {
+ Log.e(TAG, "Error binding to mirroring service: $e")
+ producerScope.close(e)
+ }
+ }
+
+ private fun unregisterReceiver(mirroringConnectionService: MirroringServiceConnection) {
+ val msg = Message.obtain(
+ null,
+ resources.getInteger(R.integer.config_msg_unregister_mirroring_pkg_code)
+ )
+ msg.replyTo = mirroringConnectionService.mClientMessenger
+ try {
+ mirroringConnectionService.mServiceMessenger?.send(msg)
+ } catch (e: RemoteException) {
+ Log.d(TAG, "Exception unregistering mirroring service $e")
+ }
+ if (mirroringConnectionService.mServiceMessenger != null) {
+ unBindService(mirroringConnectionService)
+ }
+ }
+
+ companion object {
+ val TAG: String = ControlCenterMirroringDataSourceImpl::class.java.simpleName
+ }
+}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/LauncherActivitiesDataSource.kt b/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/LauncherActivitiesDataSource.kt
new file mode 100644
index 0000000..d0613c5
--- /dev/null
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/LauncherActivitiesDataSource.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.datasources
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.LauncherActivityInfo
+import android.content.pm.LauncherApps
+import android.content.res.Resources
+import android.os.UserHandle
+import com.android.car.carlauncher.R
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.withContext
+
+interface LauncherActivitiesDataSource {
+
+ /**
+ * Gets all the Launchable activities for the user.
+ */
+ suspend fun getAllLauncherActivities(): List<LauncherActivityInfo>
+
+ /**
+ * Flow notifying changes if packages are changed.
+ */
+ fun getOnPackagesChanged(): Flow<String>
+
+ /**
+ * Get packages to hide explicitly
+ */
+ fun getAppsToHide(): List<String>
+
+ companion object {
+ val TAG: String = LauncherActivitiesDataSource::class.java.simpleName
+ }
+}
+
+/**
+ * Impl of [LauncherActivitiesDataSource] to surface all the launcher activities apis.
+ * All the operations in this class are non blocking.
+ *
+ * @property [launcherApps] Used to fetch launcher activities.
+ * @property [registerReceiverFunction] Function to register the broadcast receiver.
+ * Should be provided by the Android Component owning the [Context]
+ * @property [unregisterReceiverFunction] Function to unregister the broadcast receiver.
+ * Should be provided by the Android Component owning the [Context]
+ * @property [userHandle] Specified user's handle to fetch launcher activities.
+ * @param [resources] Application resources, not bound to activity's configuration changes.
+ * @property [bgDispatcher] Executes all the operations on this background coroutine dispatcher.
+ */
+class LauncherActivitiesDataSourceImpl(
+ private val launcherApps: LauncherApps,
+ private val registerReceiverFunction: (BroadcastReceiver, IntentFilter) -> Unit,
+ private val unregisterReceiverFunction: (BroadcastReceiver) -> Unit,
+ private val userHandle: UserHandle,
+ val resources: Resources,
+ private val bgDispatcher: CoroutineDispatcher = Dispatchers.Default
+) : LauncherActivitiesDataSource {
+
+ private val listOfApps = resources.getStringArray(R.array.hidden_apps).toList()
+
+ /**
+ * Gets all launcherActivities for a user with [userHandle]
+ */
+ override suspend fun getAllLauncherActivities(): List<LauncherActivityInfo> {
+ return withContext(bgDispatcher) {
+ launcherApps.getActivityList(
+ /* packageName = */
+ null,
+ userHandle
+ )
+ }
+ }
+
+ /**
+ * Gets a flow Producer which report changes in the packages with following actions:
+ * [Intent.ACTION_PACKAGE_ADDED], [Intent.ACTION_PACKAGE_CHANGED],
+ * [Intent.ACTION_PACKAGE_REPLACED] or [Intent.ACTION_PACKAGE_REMOVED].
+ *
+ * Note: The producer sends an `Empty String` initially. This immediately tells the collector
+ * that there are no changes as of now with packages.
+ *
+ * When the scope in which this flow is collected is closed/canceled
+ * [unregisterReceiverFunction] is triggered.
+ */
+ override fun getOnPackagesChanged(): Flow<String> {
+ return callbackFlow {
+ trySend("")
+ val filter = IntentFilter()
+ filter.addAction(Intent.ACTION_PACKAGE_ADDED)
+ filter.addAction(Intent.ACTION_PACKAGE_CHANGED)
+ filter.addAction(Intent.ACTION_PACKAGE_REPLACED)
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED)
+ filter.addDataScheme("package")
+ val receiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ val packageName = intent?.data?.schemeSpecificPart
+ if (packageName.isNullOrBlank()) {
+ return
+ }
+ trySend(packageName)
+ }
+ }
+ registerReceiverFunction(receiver, filter)
+ awaitClose {
+ unregisterReceiverFunction(receiver)
+ }
+ }.flowOn(bgDispatcher).conflate()
+ }
+
+ /**
+ * Gets packages that are explicitly required to be hidden.
+ *
+ * * Note: This packages are defined in [Resources] by name __hidden_apps__
+ */
+ override fun getAppsToHide(): List<String> {
+ return listOfApps
+ }
+}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/MediaTemplateAppsDataSource.kt b/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/MediaTemplateAppsDataSource.kt
new file mode 100644
index 0000000..c4f49bc
--- /dev/null
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/MediaTemplateAppsDataSource.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.datasources
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.service.media.MediaBrowserService
+import com.android.car.media.common.source.MediaSource.isAudioMediaSource
+import com.android.car.media.common.source.MediaSource.isMediaTemplate
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/**
+ * DataSource interface for MediaTemplate apps
+ */
+interface MediaTemplateAppsDataSource {
+
+ /**
+ * Get all media services on the device
+ * @param includeCustomMediaPackages
+ */
+ suspend fun getAllMediaServices(includeCustomMediaPackages: Boolean): List<ResolveInfo>
+}
+
+/**
+ * Impl of [MediaTemplateAppsDataSource], surfaces all media template apps related queries
+ *
+ * @property packageManager to query MediaServices.
+ * @param appContext application context, not bound to activity's configuration changes.
+ * @property [bgDispatcher] executes all the operations on this background coroutine dispatcher.
+ */
+class MediaTemplateAppsDataSourceImpl(
+ private val packageManager: PackageManager,
+ private val appContext: Context,
+ private val bgDispatcher: CoroutineDispatcher
+) : MediaTemplateAppsDataSource {
+
+ /**
+ * Gets all media services for MediaTemplateApps.
+ *
+ * @param includeCustomMediaPackages if false, only gets MediaTemplateApps.
+ * if true, in addition to MediaTemplateApps also include
+ * custom media components.
+ */
+ override suspend fun getAllMediaServices(
+ includeCustomMediaPackages: Boolean
+ ): List<ResolveInfo> {
+ val filterFunction = if (includeCustomMediaPackages) {
+ ::isAudioMediaSource
+ } else {
+ ::isMediaTemplate
+ }
+ return withContext(bgDispatcher) {
+ packageManager.queryIntentServices(
+ Intent(MediaBrowserService.SERVICE_INTERFACE),
+ PackageManager.GET_RESOLVED_FILTER
+ ).filter {
+ val componentName = ComponentName(it.serviceInfo.packageName, it.serviceInfo.name)
+ filterFunction(appContext, componentName)
+ }
+ }
+ }
+
+ companion object {
+ val TAG: String = MediaTemplateAppsDataSourceImpl::class.java.simpleName
+ }
+}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/UXRestrictionDataSource.kt b/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/UXRestrictionDataSource.kt
new file mode 100644
index 0000000..afb2ff7
--- /dev/null
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/UXRestrictionDataSource.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.datasources
+
+import android.car.content.pm.CarPackageManager
+import android.car.drivingstate.CarUxRestrictionsManager
+import android.content.ComponentName
+import android.content.Context
+import android.content.res.Resources
+import android.media.session.MediaSessionManager
+import android.util.Log
+import androidx.lifecycle.asFlow
+import com.android.car.carlauncher.Flags
+import com.android.car.carlauncher.MediaSessionUtils
+import com.android.car.carlauncher.R
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+/**
+ * DataSource interface for providing ux restriction state
+ */
+interface UXRestrictionDataSource {
+
+ /**
+ * Flow notifying if distraction optimization is required
+ */
+ fun requiresDistractionOptimization(): Flow<Boolean>
+
+ fun isDistractionOptimized(): Flow<(componentName: ComponentName, isMedia: Boolean) -> Boolean>
+}
+
+/**
+ * Impl of [UXRestrictionDataSource]
+ *
+ * @property [uxRestrictionsManager] Used to listen for distraction optimization changes.
+ * @property [carPackageManager]
+ * @property [mediaSessionManager]
+ * @property [resources] Application resources, not bound to activity's configuration changes.
+ * @property [bgDispatcher] Executes all the operations on this background coroutine dispatcher.
+ */
+class UXRestrictionDataSourceImpl(
+ private val context: Context,
+ private val uxRestrictionsManager: CarUxRestrictionsManager,
+ private val carPackageManager: CarPackageManager,
+ private val mediaSessionManager: MediaSessionManager,
+ private val resources: Resources,
+ private val bgDispatcher: CoroutineDispatcher = Dispatchers.Default,
+) : UXRestrictionDataSource {
+
+ /**
+ * Gets a flow producer which provides updates if distraction optimization is currently required
+ * This conveys if the foreground activity needs to be distraction optimized.
+ *
+ * When the scope in which this flow is collected is closed/canceled
+ * [CarUxRestrictionsManager.unregisterListener] is triggered.
+ */
+ override fun requiresDistractionOptimization(): Flow<Boolean> {
+ return callbackFlow {
+ val currentRestrictions = uxRestrictionsManager.currentCarUxRestrictions
+ if (currentRestrictions == null) {
+ Log.e(TAG, "CurrentCarUXRestrictions is not initialized")
+ trySend(false)
+ } else {
+ trySend(currentRestrictions.isRequiresDistractionOptimization)
+ }
+ uxRestrictionsManager.registerListener {
+ trySend(it.isRequiresDistractionOptimization)
+ }
+ awaitClose {
+ uxRestrictionsManager.unregisterListener()
+ }
+ }.flowOn(bgDispatcher).conflate()
+ }
+
+ override fun isDistractionOptimized():
+ Flow<(componentName: ComponentName, isMedia: Boolean) -> Boolean> {
+ if (!(Flags.mediaSessionCard() &&
+ resources.getBoolean(R.bool.config_enableMediaSessionAppsWhileDriving))
+ ) {
+ return flowOf(fun(componentName: ComponentName, isMedia: Boolean): Boolean {
+ return isMedia || (carPackageManager.isActivityDistractionOptimized(
+ componentName.packageName,
+ componentName.className
+ ))
+ })
+ }
+ return getActiveMediaPlaybackSessions().map {
+ fun(componentName: ComponentName, isMedia: Boolean): Boolean {
+ if (it.contains(componentName.packageName)) {
+ return true
+ }
+ return isMedia || (carPackageManager.isActivityDistractionOptimized(
+ componentName.packageName,
+ componentName.className
+ ))
+ }
+ }.distinctUntilChanged()
+ }
+
+ private fun getActiveMediaPlaybackSessions(): Flow<List<String>> {
+ return MediaSessionUtils.getMediaSessionHelper(context).activeOrPausedMediaSources.asFlow()
+ .map { mediaSources ->
+ mediaSources.mapNotNull {
+ it.packageName
+ }
+ }
+ }
+
+ companion object {
+ val TAG: String = UXRestrictionDataSourceImpl::class.java.simpleName
+ }
+}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/restricted/DisabledAppsDataSource.kt b/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/restricted/DisabledAppsDataSource.kt
new file mode 100644
index 0000000..7149b50
--- /dev/null
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/restricted/DisabledAppsDataSource.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.datasources.restricted
+
+import android.car.settings.CarSettings
+import android.content.ContentResolver
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
+import android.content.pm.ResolveInfo
+import android.database.ContentObserver
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+import android.util.Log
+import com.android.car.carlauncher.datasources.restricted.RestrictedAppsUtils.getLauncherActivitiesForRestrictedApps
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOn
+
+/**
+ * DataSource exposes a flow which tracks list of disabled apps.
+ */
+interface DisabledAppsDataSource {
+ fun getDisabledApps(): Flow<List<ResolveInfo>>
+}
+
+/**
+ * Impl of [DisabledAppsDataSource], to surface all DisabledApps apis.
+ *
+ * All the operations in this class are non blocking.
+ */
+class DisabledAppsDataSourceImpl(
+ private val contentResolver: ContentResolver,
+ private val packageManager: PackageManager,
+ private val bgDispatcher: CoroutineDispatcher
+) : DisabledAppsDataSource {
+
+ /**
+ * Gets a Flow producer which gets the current list of disabled apps installed for this user
+ * and found at [Settings.Secure.getString]
+ * for Key [CarSettings.Secure.KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE].
+ *
+ * The Flow also pushes new list if there is any updates in the list of disabled apps.
+ */
+ override fun getDisabledApps(): Flow<List<ResolveInfo>> {
+ return callbackFlow {
+ trySend(
+ getLauncherActivitiesForRestrictedApps(
+ packageManager,
+ contentResolver,
+ CarSettings.Secure.KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE,
+ PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR,
+ MATCH_DISABLED_UNTIL_USED_COMPONENTS
+ )
+ )
+ if (Looper.myLooper() == null) {
+ Looper.prepare()
+ }
+
+ val looper: Looper =
+ Looper.myLooper().takeIf { it != null } ?: Looper.getMainLooper().also {
+ Log.d(TAG, "Current thread looper is null, fallback to MainLooper")
+ }
+
+ val disabledAppsObserver = object : ContentObserver(Handler(looper)) {
+ override fun onChange(selfChange: Boolean) {
+ super.onChange(selfChange)
+ trySend(
+ getLauncherActivitiesForRestrictedApps(
+ packageManager,
+ contentResolver,
+ CarSettings.Secure.KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE,
+ PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR,
+ MATCH_DISABLED_UNTIL_USED_COMPONENTS
+ )
+ )
+ }
+ }
+
+ contentResolver.registerContentObserver(
+ Settings.Secure.getUriFor(
+ CarSettings.Secure.KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE
+ ),
+ false,
+ disabledAppsObserver
+ )
+
+ awaitClose {
+ // If MainLooper do not quit it. MainLooper always stays alive.
+ if (looper != Looper.getMainLooper()) {
+ looper.quitSafely()
+ }
+ contentResolver.unregisterContentObserver(disabledAppsObserver)
+ }
+ }.flowOn(bgDispatcher).conflate()
+ }
+
+ companion object {
+ val TAG: String = DisabledAppsDataSourceImpl::class.java.simpleName
+ const val PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR = ";"
+ }
+}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/restricted/RestrictedAppsUtils.kt b/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/restricted/RestrictedAppsUtils.kt
new file mode 100644
index 0000000..8ed4a3c
--- /dev/null
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/restricted/RestrictedAppsUtils.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.datasources.restricted
+
+import android.content.ContentResolver
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.GET_RESOLVED_FILTER
+import android.content.pm.ResolveInfo
+import android.provider.Settings
+import android.text.TextUtils
+import android.util.ArraySet
+
+/**
+ * Helper class for Restricted category of launcher apps.
+ */
+internal object RestrictedAppsUtils {
+
+ /**
+ * @param contentResolver required to retrieve secure strings from Settings.
+ * @param secureKey key used to store the list of packages.
+ * @param separator separator used for packages in the stored string.
+ *
+ * @return Set of packages stored in [Settings.Secure] with key [secureKey]
+ */
+ fun getRestrictedPackages(
+ contentResolver: ContentResolver,
+ secureKey: String,
+ separator: String
+ ): Set<String> {
+ val settingsValue = Settings.Secure.getString(
+ contentResolver,
+ secureKey
+ )
+
+ return if (TextUtils.isEmpty(settingsValue)) {
+ ArraySet()
+ } else {
+ ArraySet(
+ listOf(
+ *settingsValue.split(
+ separator.toRegex()
+ ).dropLastWhile { it.isEmpty() }.toTypedArray()
+ )
+ )
+ }
+ }
+
+ /**
+ * @param packageManager required to queryIntentActivities category [Intent.CATEGORY_LAUNCHER].
+ * @param contentResolver required to retrieve secure string from [Settings.Secure].
+ * @param secureKey key used to store the list of packages.
+ * @param separator separator used for packages in the stored string.
+ *
+ * @return List of ResolveInfo for restricted launcher activities filtered by packages found at
+ * [Settings.Secure] with key [secureKey].
+ */
+ fun getLauncherActivitiesForRestrictedApps(
+ packageManager: PackageManager,
+ contentResolver: ContentResolver,
+ secureKey: String,
+ separator: String,
+ filter: Int
+ ): List<ResolveInfo> {
+ return packageManager.queryIntentActivities(
+ Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER),
+ PackageManager.ResolveInfoFlags.of(
+ (GET_RESOLVED_FILTER or filter).toLong()
+ )
+ ).filter {
+ getRestrictedPackages(
+ contentResolver,
+ secureKey,
+ separator
+ ).contains(it.activityInfo.packageName)
+ }
+ }
+}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/restricted/TosDataSource.kt b/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/restricted/TosDataSource.kt
new file mode 100644
index 0000000..9111758
--- /dev/null
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/datasources/restricted/TosDataSource.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.datasources.restricted
+
+import android.car.settings.CarSettings
+import android.car.settings.CarSettings.Secure.KEY_UNACCEPTED_TOS_DISABLED_APPS
+import android.car.settings.CarSettings.Secure.KEY_USER_TOS_ACCEPTED
+import android.content.ContentResolver
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS
+import android.content.pm.ResolveInfo
+import android.database.ContentObserver
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+import android.util.Log
+import com.android.car.carlauncher.datasources.restricted.RestrictedAppsUtils.getLauncherActivitiesForRestrictedApps
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+
+/**
+ * DataSource exposes a flow which tracks list of tos(Terms of Service) disabled apps.
+ */
+interface TosDataSource {
+ fun getTosState(): Flow<TosState>
+}
+
+data class TosState(
+ val shouldBlockTosApps: Boolean,
+ val restrictedApps: List<ResolveInfo> = emptyList()
+)
+
+/**
+ * Impl of [TosDataSource], to surface all DisabledApps apis.
+ *
+ * All the operations in this class are non blocking.
+ */
+class TosDataSourceImpl(
+ private val contentResolver: ContentResolver,
+ private val packageManager: PackageManager,
+ private val bgDispatcher: CoroutineDispatcher,
+) : TosDataSource {
+
+ /**
+ * Gets a Flow producer which gets if TOS is accepted by the user and
+ * the current list of tos disabled apps installed for this user and found at
+ * [Settings.Secure.getString] for Key [CarSettings.Secure.KEY_UNACCEPTED_TOS_DISABLED_APPS]
+ * __only if__ [TosState.hasAccepted] is false which is reflected by
+ * [CarSettings.Secure.KEY_USER_TOS_ACCEPTED]
+ *
+ * The Flow also pushes new list if there is any updates in the list of disabled apps.
+ * @return Flow of [TosState]
+ */
+ override fun getTosState(): Flow<TosState> {
+ if (isTosAccepted()) {
+ // Tos is accepted, so we do not need to block the apps.
+ // We can assume the list of blocked apps as empty in this case.
+ return flowOf(
+ TosState(
+ false,
+ emptyList()
+ )
+ )
+ }
+
+ return callbackFlow {
+ trySend(
+ TosState(
+ shouldBlockTosApps(),
+ getLauncherActivitiesForRestrictedApps(
+ packageManager,
+ contentResolver,
+ KEY_UNACCEPTED_TOS_DISABLED_APPS,
+ TOS_DISABLED_APPS_SEPARATOR,
+ MATCH_DISABLED_COMPONENTS
+ )
+ )
+ )
+ if (Looper.myLooper() == null) {
+ Looper.prepare()
+ }
+
+ val looper: Looper =
+ Looper.myLooper().takeIf { it != null } ?: Looper.getMainLooper().also {
+ Log.d(TAG, "Current thread looper is null, fallback to MainLooper")
+ }
+ // Tos Accepted Observer
+ val tosStateObserver = object : ContentObserver(Handler(looper)) {
+ override fun onChange(selfChange: Boolean) {
+ super.onChange(selfChange)
+ val isAccepted = isTosAccepted()
+ val restrictedApps: List<ResolveInfo> = if (isAccepted) {
+ // We don't need to observe the changes once TOS is accepted
+ contentResolver.unregisterContentObserver(this)
+ // if TOS is accepted, we can assume that TOS disabled apps will be empty
+ emptyList()
+ } else {
+ getLauncherActivitiesForRestrictedApps(
+ packageManager,
+ contentResolver,
+ KEY_UNACCEPTED_TOS_DISABLED_APPS,
+ TOS_DISABLED_APPS_SEPARATOR,
+ MATCH_DISABLED_COMPONENTS
+ )
+ }
+ trySend(TosState(shouldBlockTosApps(), restrictedApps))
+ }
+ }
+
+ contentResolver.registerContentObserver(
+ Settings.Secure.getUriFor(KEY_UNACCEPTED_TOS_DISABLED_APPS),
+ false,
+ tosStateObserver
+ )
+
+ contentResolver.registerContentObserver(
+ Settings.Secure.getUriFor(KEY_USER_TOS_ACCEPTED),
+ false,
+ tosStateObserver
+ )
+
+ awaitClose {
+ // If MainLooper do not quit it. MainLooper always stays alive.
+ if (looper != Looper.getMainLooper()) {
+ looper.quitSafely()
+ }
+ contentResolver.unregisterContentObserver(tosStateObserver)
+ }
+ }.flowOn(bgDispatcher).conflate()
+ }
+
+ /**
+ * Check if a user has accepted TOS
+ * @return true if the user has accepted Tos, false otherwise
+ */
+ private fun isTosAccepted(): Boolean {
+ val settingsValue = Settings.Secure.getString(
+ contentResolver,
+ KEY_USER_TOS_ACCEPTED
+ )
+ return settingsValue == TOS_ACCEPTED
+ }
+
+ /**
+ * Only block the apps to the user when the TOS state is [TOS_NOT_ACCEPTED]
+ *
+ * Note: Even when TOS state is [TOS_UNINITIALIZED] we do not want to block tos apps.
+ */
+ private fun shouldBlockTosApps(): Boolean {
+ val settingsValue = Settings.Secure.getString(
+ contentResolver,
+ KEY_USER_TOS_ACCEPTED
+ )
+ return settingsValue == TOS_NOT_ACCEPTED
+ }
+
+ companion object {
+ // This value indicates if TOS is in uninitialized state
+ const val TOS_UNINITIALIZED = "0"
+
+ // This value indicates if TOS has not been accepted by the user
+ const val TOS_NOT_ACCEPTED = "1"
+
+ // This value indicates if TOS has been accepted by the user
+ const val TOS_ACCEPTED = "2"
+ const val TOS_DISABLED_APPS_SEPARATOR = ","
+
+ private val TAG = TosDataSourceImpl::class.java.simpleName
+ }
+}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/datastore/ProtoDataSource.java b/libs/appgrid/lib/src/com/android/car/carlauncher/datastore/ProtoDataSource.java
index bb1f348..16ea685 100644
--- a/libs/appgrid/lib/src/com/android/car/carlauncher/datastore/ProtoDataSource.java
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/datastore/ProtoDataSource.java
@@ -64,35 +64,46 @@
}
/**
- * Writes the {@link MessageLite} subclass T to the file represented by this object.
+ * Writes the {@link MessageLite} subclass T to the file represented by this object in the
+ * background thread.
*/
- public void writeToFile(T data) {
+ public void writeToFileInBackgroundThread(T data) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(() -> {
- try {
- if (mOutputStream == null) {
- mOutputStream = new FileOutputStream(getDataFile(), false);
- }
- writeDelimitedTo(data, mOutputStream);
- } catch (IOException e) {
- Log.e(TAG, "Launcher item list not written to file successfully.");
- } finally {
- try {
- if (mOutputStream != null) {
- mOutputStream.flush();
- mOutputStream.getFD().sync();
- mOutputStream.close();
- mOutputStream = null;
- }
- } catch (IOException e) {
- Log.e(TAG, "Unable to close output stream. ");
- }
- }
+ writeToFile(data);
executorService.shutdown();
});
}
/**
+ * Writes the {@link MessageLite} subclass T to the file represented by this object.
+ */
+ public boolean writeToFile(T data) {
+ boolean success = true;
+ try {
+ if (mOutputStream == null) {
+ mOutputStream = new FileOutputStream(getDataFile(), false);
+ }
+ writeDelimitedTo(data, mOutputStream);
+ } catch (IOException e) {
+ Log.e(TAG, "Launcher item list not written to file successfully.");
+ success = false;
+ } finally {
+ try {
+ if (mOutputStream != null) {
+ mOutputStream.flush();
+ mOutputStream.getFD().sync();
+ mOutputStream.close();
+ mOutputStream = null;
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to close output stream. ");
+ }
+ }
+ return success;
+ }
+
+ /**
* Reads the {@link MessageLite} subclass T from the file represented by this object.
*/
@Nullable
@@ -123,6 +134,21 @@
}
/**
+ * @return True if delete file was successful, false otherwise
+ */
+ public boolean deleteFile() {
+ boolean success = false;
+ try {
+ if (mFile.exists()) {
+ success = mFile.delete();
+ }
+ } catch (SecurityException ex) {
+ Log.e(TAG, "deleteFile - " + ex);
+ }
+ return success;
+ }
+
+ /**
* This method will be called by {@link ProtoDataSource#readFromFile}.
*
* Implementation is left to subclass since {@link MessageLite.parseDelimitedFrom(InputStream)}
@@ -137,7 +163,8 @@
protected abstract T parseDelimitedFrom(InputStream inputStream) throws IOException;
/**
- * This method will be called by {@link ProtoDataSource#writeToFile(MessageLite)}.
+ * This method will be called by
+ * {@link ProtoDataSource#writeToFileInBackgroundThread(MessageLite)}.
*
* Implementation is left to subclass since {@link MessageLite#writeDelimitedTo(OutputStream)}
* requires a defined class at compile time. Subclasses should implement this method by directly
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/pagination/PaginationController.java b/libs/appgrid/lib/src/com/android/car/carlauncher/pagination/PaginationController.java
index f12bf93..e144da5 100644
--- a/libs/appgrid/lib/src/com/android/car/carlauncher/pagination/PaginationController.java
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/pagination/PaginationController.java
@@ -19,8 +19,6 @@
import android.view.View;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
-import com.android.car.carlauncher.Banner;
-import com.android.car.carlauncher.R;
import com.android.car.carlauncher.pagination.PageMeasurementHelper.GridDimensions;
import com.android.car.carlauncher.pagination.PageMeasurementHelper.PageDimensions;
@@ -33,20 +31,15 @@
public class PaginationController {
private final PageMeasurementHelper mPageMeasurementHelper;
private final DimensionUpdateCallback mCallback;
- // Terms of Service Banner
- private final Banner mTosBanner;
public PaginationController(View windowBackground, DimensionUpdateCallback callback) {
mCallback = callback;
mPageMeasurementHelper = new PageMeasurementHelper(windowBackground);
- mTosBanner = windowBackground.findViewById(R.id.tos_banner);
windowBackground.getViewTreeObserver().addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
- // We need to subtract the banner height from the available window height
- // available to the app_grid
- int windowHeight = windowBackground.getMeasuredHeight() - getBannerHeight();
+ int windowHeight = windowBackground.getMeasuredHeight();
int windowWidth = windowBackground.getMeasuredWidth();
maybeHandleWindowResize(windowWidth, windowHeight);
}
@@ -61,13 +54,6 @@
}
}
- private int getBannerHeight() {
- if (mTosBanner.getVisibility() == View.VISIBLE) {
- return mTosBanner.getMeasuredHeight();
- }
- return 0;
- }
-
/**
* Callback contract between this controller and its {@link DimensionUpdateListener} classes.
*
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/recyclerview/AppGridAdapter.java b/libs/appgrid/lib/src/com/android/car/carlauncher/recyclerview/AppGridAdapter.java
index a7d0c89..6640570 100644
--- a/libs/appgrid/lib/src/com/android/car/carlauncher/recyclerview/AppGridAdapter.java
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/recyclerview/AppGridAdapter.java
@@ -29,12 +29,11 @@
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
-import com.android.car.carlauncher.AppGridActivity.Mode;
+import com.android.car.carlauncher.AppGridFragment.Mode;
import com.android.car.carlauncher.AppGridPageSnapper;
import com.android.car.carlauncher.AppItem;
import com.android.car.carlauncher.LauncherItem;
import com.android.car.carlauncher.LauncherItemDiffCallback;
-import com.android.car.carlauncher.LauncherViewModel;
import com.android.car.carlauncher.R;
import com.android.car.carlauncher.RecentAppsRowViewHolder;
import com.android.car.carlauncher.pagination.PageIndexingHelper;
@@ -59,7 +58,6 @@
private final int mNumOfRows;
private int mAppItemWidth;
private int mAppItemHeight;
- private final LauncherViewModel mLauncherViewModel;
// grid order of the mLauncherItems used by DiffUtils in dispatchUpdates to animate UI updates
private final List<LauncherItem> mGridOrderedLauncherItems;
@@ -70,40 +68,25 @@
private Rect mPageBound;
private Mode mAppGridMode;
- public AppGridAdapter(Context context, int numOfCols, int numOfRows,
- LauncherViewModel launcherViewModel, AppItemViewHolder.AppItemDragCallback dragCallback,
- AppGridPageSnapper.AppGridPageSnapCallback snapCallback) {
- this(context, numOfCols, numOfRows,
- context.getResources().getBoolean(R.bool.use_vertical_app_grid)
- ? PageOrientation.VERTICAL : PageOrientation.HORIZONTAL,
- LayoutInflater.from(context), launcherViewModel, dragCallback, snapCallback);
- }
+ private AppGridAdapterListener mAppGridAdapterListener;
public AppGridAdapter(Context context, int numOfCols, int numOfRows,
- @PageOrientation int pageOrientation,
- LayoutInflater layoutInflater, LauncherViewModel launcherViewModel,
AppItemViewHolder.AppItemDragCallback dragCallback,
- AppGridPageSnapper.AppGridPageSnapCallback snapCallback) {
- this(context, numOfCols, numOfRows, pageOrientation, layoutInflater,
- launcherViewModel, dragCallback, snapCallback, Mode.ALL_APPS);
- }
-
- public AppGridAdapter(Context context, int numOfCols, int numOfRows,
- @PageOrientation int pageOrientation,
- LayoutInflater layoutInflater, LauncherViewModel launcherViewModel,
- AppItemViewHolder.AppItemDragCallback dragCallback,
- AppGridPageSnapper.AppGridPageSnapCallback snapCallback, Mode mode) {
+ AppGridPageSnapper.AppGridPageSnapCallback snapCallback,
+ AppGridAdapterListener appGridAdapterListener,
+ Mode mode) {
mContext = context;
- mInflater = layoutInflater;
+ mInflater = LayoutInflater.from(context);
mNumOfCols = numOfCols;
mNumOfRows = numOfRows;
mDragCallback = dragCallback;
mSnapCallback = snapCallback;
-
+ int pageOrientation = context.getResources().getBoolean(R.bool.use_vertical_app_grid)
+ ? PageOrientation.VERTICAL : PageOrientation.HORIZONTAL;
mIndexingHelper = new PageIndexingHelper(numOfCols, numOfRows, pageOrientation);
mGridOrderedLauncherItems = new ArrayList<>();
- mLauncherViewModel = launcherViewModel;
mAppGridMode = mode;
+ mAppGridAdapterListener = appGridAdapterListener;
}
/**
@@ -142,8 +125,8 @@
* This should only be called by onChanged() in the observer as a response to data change in the
* adapter's LauncherViewModel.
*/
- public void setLauncherItems(List<LauncherItem> launcherItems) {
- mLauncherItems = launcherItems;
+ public void setLauncherItems(List<? extends LauncherItem> launcherItems) {
+ mLauncherItems = (List<LauncherItem>) launcherItems;
int newSnapPosition = mSnapCallback.getSnapPosition();
if (newSnapPosition != 0 && newSnapPosition >= getItemCount()) {
// in case user deletes the only app item on the last page, the page should snap to the
@@ -273,7 +256,7 @@
// we need to move package to target index even if the from and to index are the same to
// ensure dispatchLayout gets called to re-anchor the recyclerview to current page.
AppItem selectedApp = (AppItem) mLauncherItems.get(adaptorIndexFrom);
- mLauncherViewModel.setAppPosition(adaptorIndexTo, selectedApp.getAppMetaData());
+ mAppGridAdapterListener.onAppPositionChanged(adaptorIndexTo, selectedApp);
}
@@ -343,4 +326,8 @@
}
return mIndexingHelper.adaptorIndexToGridPosition(targetAdapterIndex);
}
+
+ public interface AppGridAdapterListener {
+ void onAppPositionChanged(int newPosition, AppItem appItem);
+ }
}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/recyclerview/AppItemViewHolder.java b/libs/appgrid/lib/src/com/android/car/carlauncher/recyclerview/AppItemViewHolder.java
index b7a40bb..92dc941 100644
--- a/libs/appgrid/lib/src/com/android/car/carlauncher/recyclerview/AppItemViewHolder.java
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/recyclerview/AppItemViewHolder.java
@@ -22,7 +22,7 @@
import static com.android.car.carlauncher.AppGridConstants.AppItemBoundDirection;
import static com.android.car.carlauncher.AppGridConstants.PageOrientation;
import static com.android.car.carlauncher.AppGridConstants.isHorizontal;
-import static com.android.car.carlauncher.hidden.HiddenApiAccess.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION;
+import static com.android.car.hidden.apis.HiddenApiAccess.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION;
import android.content.ClipData;
import android.content.ComponentName;
@@ -42,17 +42,17 @@
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
-import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
-import com.android.car.carlauncher.AppGridActivity;
+import com.android.car.carlauncher.AppGridFragment;
import com.android.car.carlauncher.AppGridPageSnapper.AppGridPageSnapCallback;
import com.android.car.carlauncher.AppItemDragShadowBuilder;
import com.android.car.carlauncher.AppMetaData;
import com.android.car.carlauncher.R;
+import com.android.car.carlaunchercommon.toasts.NonDrivingOptimizedLaunchFailedToast;
/**
* App item view holder that contains the app icon and name.
@@ -98,18 +98,18 @@
public static class BindInfo {
private final boolean mIsDistractionOptimizationRequired;
private final Rect mPageBound;
- private final AppGridActivity.Mode mMode;
+ private final AppGridFragment.Mode mMode;
public BindInfo(boolean isDistractionOptimizationRequired,
Rect pageBound,
- AppGridActivity.Mode mode) {
+ AppGridFragment.Mode mode) {
this.mIsDistractionOptimizationRequired = isDistractionOptimizationRequired;
this.mPageBound = pageBound;
this.mMode = mode;
}
public BindInfo(boolean isDistractionOptimizationRequired, Rect pageBound) {
- this(isDistractionOptimizationRequired, pageBound, AppGridActivity.Mode.ALL_APPS);
+ this(isDistractionOptimizationRequired, pageBound, AppGridFragment.Mode.ALL_APPS);
}
}
@@ -160,7 +160,7 @@
}
boolean isDistractionOptimizationRequired = bindInfo.mIsDistractionOptimizationRequired;
mPageBound = bindInfo.mPageBound;
- AppGridActivity.Mode mode = bindInfo.mMode;
+ AppGridFragment.Mode mode = bindInfo.mMode;
mHasAppMetadata = true;
mAppItemView.setFocusable(true);
@@ -182,8 +182,8 @@
// previous page, so we need to rebind the app with the correct visibility.
setStateSelected(mComponentName.equals(mDragCallback.mSelectedComponent));
- boolean isLaunchableDistractionOptimized =
- !isDistractionOptimizationRequired || app.getIsDistractionOptimized();
+ boolean isLaunchableDistractionOptimized = !isDistractionOptimizationRequired
+ || app.getIsDistractionOptimized();
boolean isDisabledByTos = app.getIsDisabledByTos();
boolean isLaunchable = isLaunchableDistractionOptimized || isDisabledByTos;
@@ -238,7 +238,7 @@
mActionDownX,
mActionDownY,
mode)) {
- startDragAndDrop(app.getComponentName(), event.getX(), event.getY());
+ startDragAndDrop(app, event.getX(), event.getY());
mCanStartDragAction = false;
} else if (action == MotionEvent.ACTION_UP
|| action == MotionEvent.ACTION_CANCEL) {
@@ -253,14 +253,9 @@
});
}
} else {
- String warningText = mContext.getResources()
- .getString(R.string.driving_toast_text, app.getDisplayName());
- View.OnClickListener appLaunchListener = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Toast.makeText(mContext, warningText, Toast.LENGTH_LONG).show();
- }
- };
+ View.OnClickListener appLaunchListener = v ->
+ NonDrivingOptimizedLaunchFailedToast.Companion.showToast(
+ mContext, app.getDisplayName());
mAppItemView.setOnClickListener(appLaunchListener);
mAppIcon.setOnClickListener(appLaunchListener);
@@ -379,9 +374,9 @@
private boolean shouldStartDragAndDrop(MotionEvent event, float actionDownX,
- float actionDownY, AppGridActivity.Mode mode) {
+ float actionDownY, AppGridFragment.Mode mode) {
// If App Grid is not in all apps mode, we should not allow drag and drop
- if (mode != AppGridActivity.Mode.ALL_APPS) {
+ if (mode != AppGridFragment.Mode.ALL_APPS) {
return false;
}
// the move event should be with in the bounds of the app icon
@@ -395,18 +390,21 @@
&& isDistancePastThreshold;
}
- private void startDragAndDrop(ComponentName componentName, float eventX, float eventY) {
+ private void startDragAndDrop(AppMetaData app, float eventX, float eventY) {
ClipData clipData = ClipData.newPlainText(/* label= */ APP_ITEM_DRAG_TAG,
- /* text= */ componentName.flattenToString());
+ /* text= */ app.getComponentName().flattenToString());
// since the app icon is scaled, the touch point that users should be holding when drag
// shadow is deployed should also be scaled
Point dragPoint = new Point(/* x */ (int) (eventX / mIconSize * mIconScaledSize),
/* y */ (int) (eventY / mIconSize * mIconScaledSize));
- AppItemDragShadowBuilder dragShadowBuilder = new AppItemDragShadowBuilder(mAppIcon,
+ Drawable appIcon = app.getIcon();
+ if (appIcon.getConstantState() == null) return;
+ AppItemDragShadowBuilder dragShadowBuilder = new AppItemDragShadowBuilder(
+ appIcon.getConstantState().newDrawable().mutate(),
/* touchPointX */ dragPoint.x, /* touchPointX */ dragPoint.y,
- /* size */ mIconSize, /* scaledSize */ mIconScaledSize);
+ /* scaledSize */ mIconScaledSize);
mAppIcon.startDragAndDrop(clipData, /* dragShadowBuilder */ dragShadowBuilder,
/* myLocalState */ null, /* flags */ DRAG_FLAG_OPAQUE | DRAG_FLAG_GLOBAL
| DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION);
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/repositories/AppGridRepository.kt b/libs/appgrid/lib/src/com/android/car/carlauncher/repositories/AppGridRepository.kt
new file mode 100644
index 0000000..f9376a6
--- /dev/null
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/repositories/AppGridRepository.kt
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.repositories
+
+import android.Manifest.permission.MANAGE_OWN_CALLS
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.NameNotFoundException
+import android.content.pm.ResolveInfo
+import android.graphics.drawable.Drawable
+import android.os.UserManager
+import android.util.Log
+import com.android.car.carlauncher.AppItem
+import com.android.car.carlauncher.AppMetaData
+import com.android.car.carlauncher.datasources.AppOrderDataSource
+import com.android.car.carlauncher.datasources.AppOrderDataSource.AppOrderInfo
+import com.android.car.carlauncher.datasources.ControlCenterMirroringDataSource
+import com.android.car.carlauncher.datasources.LauncherActivitiesDataSource
+import com.android.car.carlauncher.datasources.MediaTemplateAppsDataSource
+import com.android.car.carlauncher.datasources.UXRestrictionDataSource
+import com.android.car.carlauncher.datasources.restricted.DisabledAppsDataSource
+import com.android.car.carlauncher.datasources.restricted.TosDataSource
+import com.android.car.carlauncher.datasources.restricted.TosState
+import com.android.car.carlauncher.repositories.appactions.AppLaunchProviderFactory
+import com.android.car.carlauncher.repositories.appactions.AppLaunchProviderFactory.AppLauncherProviderType
+import com.android.car.carlauncher.repositories.appactions.AppLaunchProviderFactory.AppLauncherProviderType.DISABLED
+import com.android.car.carlauncher.repositories.appactions.AppLaunchProviderFactory.AppLauncherProviderType.LAUNCHER
+import com.android.car.carlauncher.repositories.appactions.AppLaunchProviderFactory.AppLauncherProviderType.MEDIA
+import com.android.car.carlauncher.repositories.appactions.AppLaunchProviderFactory.AppLauncherProviderType.MIRRORING
+import com.android.car.carlauncher.repositories.appactions.AppLaunchProviderFactory.AppLauncherProviderType.TOS_DISABLED
+import com.android.car.carlauncher.repositories.appactions.AppShortcutsFactory
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+interface AppGridRepository {
+
+ /**
+ * Returns a flow of all applications available in the app grid, including
+ * system apps, media apps, and potentially restricted apps.
+ *
+ * @return A Flow emitting a list of AppItem objects.
+ */
+ fun getAllAppsList(): Flow<List<AppItem>>
+
+ /**
+ * Provides a flow indicating whether distraction optimization is required for the device.
+ * Distraction optimization might limit the features or visibility of apps.
+ *
+ * @return A Flow emitting a Boolean value, where `true` indicates distraction optimization is
+ * needed.
+ */
+ fun requiresDistractionOptimization(): Flow<Boolean>
+
+ /**
+ * Returns a continuous flow representing the Terms of Service (ToS) state for apps.
+ * This state may determine the availability or restrictions of certain apps.
+ *
+ * @return A Flow emitting the current TosState.
+ */
+ fun getTosState(): Flow<TosState>
+
+ /**
+ * Suspends execution to save the provided app order to persistent storage.
+ * Used to maintain the arrangement of apps within the app grid.
+ *
+ * @param currentAppOrder A list of AppItem representing the desired app order.
+ */
+ suspend fun saveAppOrder(currentAppOrder: List<AppItem>)
+
+ /**
+ * Returns a flow of media-related apps installed on the device.
+ *
+ * @return A Flow emitting a list of AppItem representing media applications.
+ */
+ fun getMediaAppsList(): Flow<List<AppItem>>
+}
+
+/**
+ * The core implementation of the AppGridRepository interface. This class is responsible for:
+ *
+ * * Fetching and combining app information from various sources (launcher activities,
+ * media services, disabled apps, etc.)
+ * * Applying restrictions and filtering app lists based on distraction optimization, ToS status,
+ * and mirroring state.
+ * * Managing app order and saving it to persistent storage.
+ * * Providing real-time updates of the app grid as changes occur.
+ */
+class AppGridRepositoryImpl(
+ private val launcherActivities: LauncherActivitiesDataSource,
+ private val mediaTemplateApps: MediaTemplateAppsDataSource,
+ private val disabledApps: DisabledAppsDataSource,
+ private val tosApps: TosDataSource,
+ private val controlCenterMirroring: ControlCenterMirroringDataSource,
+ private val uxRestriction: UXRestrictionDataSource,
+ private val appOrder: AppOrderDataSource,
+ private val packageManager: PackageManager,
+ private val appLaunchFactory: AppLaunchProviderFactory,
+ private val appShortcutsFactory: AppShortcutsFactory,
+ userManager: UserManager,
+ private val bgDispatcher: CoroutineDispatcher
+) : AppGridRepository {
+
+ private val isVisibleBackgroundUser = !userManager.isUserForeground &&
+ userManager.isUserVisible && !userManager.isProfile
+
+ /**
+ * Provides a flow of all apps in the app grid.
+ * It combines data from multiple sources, filters apps based on restrictions, handles dynamic
+ * updates and returns the list in the last known savedOrder.
+ *
+ * @return A Flow emitting lists of AppItem objects.
+ */
+ override fun getAllAppsList(): Flow<List<AppItem>> {
+ return combine(
+ getAllLauncherAndMediaApps(),
+ getRestrictedApps(),
+ controlCenterMirroring.getAppMirroringSession(),
+ appOrder.getSavedAppOrderComparator(),
+ uxRestriction.isDistractionOptimized()
+ ) { apps, restrictedApps, mirroringSession, order, isDistractionOptimized ->
+ val alreadyAddedComponents = apps.map { it.componentName.packageName }.toSet()
+ return@combine (apps + restrictedApps.filterNot {
+ it.componentName.packageName in alreadyAddedComponents
+ }).sortedWith { a1, a2 ->
+ order.compare(a1.appOrderInfo, a2.appOrderInfo)
+ }.filter {
+ !shouldHideApp(it)
+ }.map {
+ if (mirroringSession.packageName == it.componentName.packageName) {
+ it.redirectIntent = mirroringSession.launchIntent
+ } else if (it.launchActionType == MIRRORING) {
+ it.redirectIntent = null
+ }
+ it.toAppItem(isDistractionOptimized(it.componentName, it.launchActionType == MEDIA))
+ }
+ }.flowOn(bgDispatcher).distinctUntilChanged()
+ }
+
+ /**
+ * Emitting distraction optimization status changes.
+ *
+ * @return A Flow of Boolean values, where `true` indicates distraction optimization is
+ * required.
+ */
+ override fun requiresDistractionOptimization(): Flow<Boolean> {
+ return uxRestriction.requiresDistractionOptimization()
+ }
+
+ /**
+ * Provides the Terms of Service state for apps.
+ *
+ * @return A Flow emitting the current TosState.
+ */
+ override fun getTosState(): Flow<TosState> {
+ return tosApps.getTosState()
+ }
+
+ /**
+ * Suspends saving the given app order to persistent storage.
+ * Updates to the app order are posted to the subscribers of
+ * [AppGridRepositoryImpl.getAllAppsList]
+ *
+ * @param currentAppOrder A list of AppItem representing the desired app order.
+ */
+ override suspend fun saveAppOrder(currentAppOrder: List<AppItem>) {
+ appOrder.saveAppOrder(currentAppOrder.toAppOrderInfoList())
+ }
+
+ /**
+ * Providing a flow of media-related apps.
+ * Handles dynamic updates to the list of media apps.
+ *
+ * @return A Flow emitting lists of AppItem objects representing media apps.
+ */
+ override fun getMediaAppsList(): Flow<List<AppItem>> {
+ return launcherActivities.getOnPackagesChanged().map {
+ mediaTemplateApps.getAllMediaServices(true).map {
+ it.toAppInfo(MEDIA).toAppItem(true)
+ }
+ }.flowOn(bgDispatcher).distinctUntilChanged()
+ }
+
+ private fun getAllLauncherAndMediaApps(): Flow<List<AppInfo>> {
+ return launcherActivities.getOnPackagesChanged().map {
+ val launcherApps = launcherActivities.getAllLauncherActivities().map {
+ AppInfo(it.label, it.componentName, it.getBadgedIcon(0), LAUNCHER)
+ }
+ val mediaTemplateApps = mediaTemplateApps.getAllMediaServices(false).map {
+ it.toAppInfo(MEDIA)
+ }
+ launcherApps + mediaTemplateApps
+ }.flowOn(bgDispatcher).distinctUntilChanged()
+ }
+
+ private fun getRestrictedApps(): Flow<List<AppInfo>> {
+ return disabledApps.getDisabledApps()
+ .combine(tosApps.getTosState()) { disabledApps, tosApps ->
+ return@combine disabledApps.map {
+ it.toAppInfo(DISABLED)
+ } + tosApps.restrictedApps.map {
+ it.toAppInfo(TOS_DISABLED)
+ }
+ }.flowOn(bgDispatcher).distinctUntilChanged()
+ }
+
+ private data class AppInfo(
+ val displayName: CharSequence,
+ val componentName: ComponentName,
+ val icon: Drawable,
+ private val _launchActionType: AppLauncherProviderType,
+ var redirectIntent: Intent? = null
+ ) {
+ val launchActionType get() = if (redirectIntent == null) {
+ _launchActionType
+ } else {
+ MIRRORING
+ }
+
+ val appOrderInfo =
+ AppOrderInfo(componentName.packageName, componentName.className, displayName.toString())
+ }
+
+ private fun AppInfo.toAppItem(isDistractionOptimized: Boolean): AppItem {
+ val metaData = AppMetaData(
+ displayName,
+ componentName,
+ icon,
+ isDistractionOptimized,
+ launchActionType == MIRRORING,
+ launchActionType == TOS_DISABLED,
+ { context ->
+ appLaunchFactory
+ .get(launchActionType)
+ ?.launch(context, componentName, redirectIntent)
+ },
+ { contextViewPair ->
+ appShortcutsFactory.showShortcuts(
+ componentName,
+ displayName,
+ contextViewPair.first,
+ contextViewPair.second
+ )
+ }
+ )
+ return AppItem(metaData)
+ }
+
+ private fun ResolveInfo.toAppInfo(launchActionType: AppLauncherProviderType): AppInfo {
+ val componentName: ComponentName
+ val icon: Drawable
+ if (launchActionType == MEDIA) {
+ componentName = ComponentName(serviceInfo.packageName, serviceInfo.name)
+ icon = serviceInfo.loadIcon(packageManager)
+ } else {
+ componentName = ComponentName(activityInfo.packageName, activityInfo.name)
+ icon = activityInfo.loadIcon(packageManager)
+ }
+ return AppInfo(
+ loadLabel(packageManager),
+ componentName,
+ icon,
+ launchActionType
+ )
+ }
+
+ private fun List<AppItem>.toAppOrderInfoList(): List<AppOrderInfo> {
+ return map { AppOrderInfo(it.packageName, it.className, it.displayName.toString()) }
+ }
+
+ private fun shouldHideApp(appInfo: AppInfo): Boolean {
+ // Disable telephony apps for MUMD passenger since accepting a call will
+ // drop the driver's call.
+ if (isVisibleBackgroundUser) {
+ return try {
+ packageManager.getPackageInfo(
+ appInfo.componentName.packageName, PackageManager.GET_PERMISSIONS)
+ .requestedPermissions?.any {it == MANAGE_OWN_CALLS} ?: false
+ } catch (e: NameNotFoundException) {
+ Log.e(TAG, "Unable to query app permissions for $appInfo $e")
+ false
+ }
+ }
+
+ return false
+ }
+
+ companion object {
+ const val TAG = "AppGridRepository"
+ }
+}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/repositories/appactions/AppLaunchProvider.kt b/libs/appgrid/lib/src/com/android/car/carlauncher/repositories/appactions/AppLaunchProvider.kt
new file mode 100644
index 0000000..7c78b80
--- /dev/null
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/repositories/appactions/AppLaunchProvider.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.repositories.appactions
+
+import android.app.ActivityOptions
+import android.car.Car
+import android.car.media.CarMediaManager
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.util.Log
+import com.android.car.carlauncher.R
+import java.net.URISyntaxException
+
+/**
+ * A sealed class representing various ways to launch applications within the system.
+ * Subclasses define specialized launch behaviors for different app types (launcher
+ * activities, media services, disabled apps, etc.).
+ *
+ * @see LauncherActivityLaunchProvider
+ * @see MediaServiceLaunchProvider
+ * @see DisabledAppLaunchProvider
+ * @see TosDisabledAppLaunchProvider
+ * @see MirroringAppLaunchProvider
+ */
+sealed class AppLaunchProvider {
+
+ /**
+ * Launches the app associated with the given ComponentName. This is the core
+ * method that all subclasses must implement.
+ *
+ * @param context Application context used for launching the activity.
+ * @param componentName The ComponentName identifying the app to launch.
+ * @param launchIntent An optional Intent that might contain additional launch instructions.
+ */
+ abstract fun launch(
+ context: Context,
+ componentName: ComponentName,
+ launchIntent: Intent? = null
+ )
+
+ /**
+ * Utility method to launch an Intent with consistent ActivityOptions
+ */
+ protected fun launchApp(context: Context, intent: Intent) {
+ val options = ActivityOptions.makeBasic()
+ options.launchDisplayId = context.display?.displayId ?: 0
+ context.startActivity(intent, options.toBundle())
+ }
+
+ /**
+ * Handles launching standard launcher activities. It constructs an Intent with
+ * the necessary flags for launching a new instance of the given app.
+ */
+ internal object LauncherActivityLaunchProvider : AppLaunchProvider() {
+ override fun launch(context: Context, componentName: ComponentName, launchIntent: Intent?) {
+ launchApp(
+ context,
+ Intent(Intent.ACTION_MAIN).setComponent(componentName)
+ .addCategory(Intent.CATEGORY_LAUNCHER).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ )
+ }
+ }
+
+ /**
+ * Launches media services, either by directly starting a media center template or
+ * switching active media sources. This provider works in conjunction with the CarMediaManager.
+ *
+ * @param carMediaManager Interface for controlling car media settings.
+ * @param launchMediaCenter If true, launches the media center template directly.
+ * @param closeScreen A callback function, likely used to close a prior UI screen.
+ */
+ internal class MediaServiceLaunchProvider(
+ private val carMediaManager: CarMediaManager,
+ private val launchMediaCenter: Boolean,
+ val closeScreen: () -> Unit
+ ) : AppLaunchProvider() {
+ override fun launch(context: Context, componentName: ComponentName, launchIntent: Intent?) {
+ if (launchMediaCenter) {
+ launchApp(
+ context,
+ Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE).putExtra(
+ Car.CAR_EXTRA_MEDIA_COMPONENT,
+ componentName.flattenToString()
+ )
+ )
+ return
+ }
+ carMediaManager.setMediaSource(componentName, CarMediaManager.MEDIA_SOURCE_MODE_BROWSE)
+ closeScreen()
+ }
+ }
+
+ /**
+ * Responsible for enabling and then launching disabled applications. Requires access
+ * to the PackageManager to manage application state.
+ */
+ internal class DisabledAppLaunchProvider(
+ private val packageManager: PackageManager,
+ ) : AppLaunchProvider() {
+ override fun launch(context: Context, componentName: ComponentName, launchIntent: Intent?) {
+ packageManager.setApplicationEnabledSetting(
+ componentName.packageName,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ 0
+ )
+ // Fetch the current enabled setting to make sure the setting is synced
+ // before launching the activity. Otherwise, the activity may not
+ // launch.
+ check(
+ packageManager.getApplicationEnabledSetting(componentName.packageName)
+ == PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ ) {
+ ("Failed to enable the disabled package [" + componentName.packageName + "]")
+ }
+ Log.i(TAG, "Successfully enabled package [${componentName.packageName}]")
+ launchApp(
+ context,
+ Intent(Intent.ACTION_MAIN).setComponent(componentName)
+ .addCategory(Intent.CATEGORY_LAUNCHER).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ )
+ }
+
+ companion object {
+ const val TAG = "DisabledAppLaunchProvider"
+ }
+ }
+
+ /**
+ * Handles launching the system activity for Terms of Service (TOS) acceptance. This
+ * provider redirects the user to the TOS flow for apps restricted by TOS.
+ */
+ internal object TosDisabledAppLaunchProvider : AppLaunchProvider() {
+ override fun launch(context: Context, componentName: ComponentName, launchIntent: Intent?) {
+ val tosIntentName = context.resources.getString(R.string.user_tos_activity_intent)
+ try {
+ launchApp(context, Intent.parseUri(tosIntentName, Intent.URI_ANDROID_APP_SCHEME))
+ } catch (se: URISyntaxException) {
+ Log.e(TAG, "Invalid intent URI in user_tos_activity_intent", se)
+ }
+ }
+
+ const val TAG = "TosDisabledAppLaunchProvider"
+ }
+
+ /**
+ * Handles app mirroring scenarios. Assumes that a launchIntent is provided with the
+ * necessary information to launch the mirrored app.
+ */
+ internal object MirroringAppLaunchProvider :
+ AppLaunchProvider() {
+ override fun launch(context: Context, componentName: ComponentName, launchIntent: Intent?) {
+ launchIntent?.let {
+ launchApp(context, it)
+ }
+ }
+ }
+}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/repositories/appactions/AppLaunchProviderFactory.kt b/libs/appgrid/lib/src/com/android/car/carlauncher/repositories/appactions/AppLaunchProviderFactory.kt
new file mode 100644
index 0000000..befd702
--- /dev/null
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/repositories/appactions/AppLaunchProviderFactory.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.repositories.appactions
+
+import android.car.media.CarMediaManager
+import android.content.pm.PackageManager
+import android.util.Log
+import com.android.car.carlauncher.repositories.appactions.AppLaunchProvider.DisabledAppLaunchProvider
+import com.android.car.carlauncher.repositories.appactions.AppLaunchProvider.LauncherActivityLaunchProvider
+import com.android.car.carlauncher.repositories.appactions.AppLaunchProvider.MediaServiceLaunchProvider
+import com.android.car.carlauncher.repositories.appactions.AppLaunchProvider.MirroringAppLaunchProvider
+import com.android.car.carlauncher.repositories.appactions.AppLaunchProvider.TosDisabledAppLaunchProvider
+
+/**
+ * Acts as a central factory for obtaining specialized AppLaunchProvider instances.
+ * It maintains an internal mapping between AppLauncherProviderType enum values and
+ * the corresponding provider objects. The factory is responsible for:
+ *
+ * * Registering available AppLaunchProvider implementations
+ * * Providing access to providers based on the requested AppLauncherProviderType
+ */
+class AppLaunchProviderFactory(
+ carMediaManager: CarMediaManager,
+ launchMediaCenter: Boolean,
+ onMediaSelected: () -> Unit,
+ packageManager: PackageManager
+) {
+
+ private val providerMap = mutableMapOf<AppLauncherProviderType, AppLaunchProvider>()
+
+ /**
+ * Enumerates the supported types of AppLaunchProvider implementations. Used
+ * internally by the factory to differentiate and retrieve providers.
+ */
+ enum class AppLauncherProviderType {
+ LAUNCHER, MEDIA, DISABLED, TOS_DISABLED, MIRRORING
+ }
+
+ init {
+ // Add all providers.
+ addProvider(LauncherActivityLaunchProvider)
+ addProvider(MediaServiceLaunchProvider(carMediaManager, launchMediaCenter, onMediaSelected))
+ addProvider(DisabledAppLaunchProvider(packageManager))
+ addProvider(TosDisabledAppLaunchProvider)
+ addProvider(MirroringAppLaunchProvider)
+ }
+
+ /**
+ * Retrieves the AppLaunchProvider associated with the specified AppLauncherProviderType.
+ *
+ * @param providerType The type of AppLaunchProvider to retrieve.
+ * @return The corresponding AppLaunchProvider if registered, otherwise null.
+ */
+ fun get(providerType: AppLauncherProviderType): AppLaunchProvider? {
+ return providerMap[providerType].also {
+ if (it == null) {
+ Log.i(TAG, "Launch provider for ${providerType.name} missing")
+ }
+ }
+ }
+
+ /**
+ * Registers an AppLaunchProvider within the factory. Providers are associated with
+ * their corresponding AppLauncherProviderType.
+ *
+ * @param provider The AppLaunchProvider instance to register.
+ */
+ private fun addProvider(provider: AppLaunchProvider) {
+ when (provider) {
+ is DisabledAppLaunchProvider -> {
+ providerMap[AppLauncherProviderType.DISABLED] = provider
+ }
+
+ LauncherActivityLaunchProvider -> {
+ providerMap[AppLauncherProviderType.LAUNCHER] = provider
+ }
+
+ is MediaServiceLaunchProvider -> {
+ providerMap[AppLauncherProviderType.MEDIA] = provider
+ }
+
+ is MirroringAppLaunchProvider -> {
+ providerMap[AppLauncherProviderType.MIRRORING] = provider
+ }
+
+ is TosDisabledAppLaunchProvider -> {
+ providerMap[AppLauncherProviderType.TOS_DISABLED] = provider
+ }
+ }
+ }
+
+ companion object {
+ const val TAG = "AppLaunchProviderFactory"
+ }
+}
diff --git a/libs/appgrid/lib/src/com/android/car/carlauncher/repositories/appactions/AppShortcutsFactory.kt b/libs/appgrid/lib/src/com/android/car/carlauncher/repositories/appactions/AppShortcutsFactory.kt
new file mode 100644
index 0000000..37f0c0e
--- /dev/null
+++ b/libs/appgrid/lib/src/com/android/car/carlauncher/repositories/appactions/AppShortcutsFactory.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2024 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.carlauncher.repositories.appactions
+
+import android.car.media.CarMediaManager
+import android.content.ComponentName
+import android.content.Context
+import android.os.Process
+import android.os.UserHandle
+import android.view.View
+import com.android.car.carlaunchercommon.shortcuts.AppInfoShortcutItem
+import com.android.car.carlaunchercommon.shortcuts.ForceStopShortcutItem
+import com.android.car.carlaunchercommon.shortcuts.PinShortcutItem
+import com.android.car.dockutil.Flags
+import com.android.car.dockutil.events.DockEventSenderHelper
+import com.android.car.ui.shortcutspopup.CarUiShortcutsPopup
+
+/**
+ * This class is responsible for creating and displaying app shortcuts popups within the
+ * car UI. It generates shortcuts for actions like "Stop App," "App Info," and potentially
+ * "Pin to Dock." The class interacts with CarMediaManager and relies on a ShortcutsListener
+ * to track interactions with the shortcuts popup.
+ *
+ * @param carMediaManager For controlling car media settings.
+ * @param mediaServiceComponents A set of ComponentNames identifying installed media services.
+ * @param shortcutsListener Listener for handling events triggered by the shortcuts popup.
+ */
+class AppShortcutsFactory(
+ private val carMediaManager: CarMediaManager,
+ private val mediaServiceComponents: Set<ComponentName>,
+ private val shortcutsListener: ShortcutsListener
+) {
+
+ /**
+ * Displays a car UI shortcuts popup anchored to the provided view. The popup includes
+ * shortcuts for "Force Stop," "App Info," and potentially "Pin to Dock" (if the feature
+ * is enabled).
+ *
+ * @param componentName The ComponentName of the app for which shortcuts are generated.
+ * @param displayName The display name of the app.
+ * @param context Application context.
+ * @param anchorView The UI view to anchor the shortcuts popup.
+ */
+ fun showShortcuts(
+ componentName: ComponentName,
+ displayName: CharSequence,
+ context: Context,
+ anchorView: View
+ ) {
+ val carUiShortcutsPopupBuilder =
+ CarUiShortcutsPopup.Builder()
+ .addShortcut(
+ ForceStopShortcutItem(
+ context,
+ componentName.packageName,
+ displayName,
+ carMediaManager,
+ mediaServiceComponents
+ )
+ )
+ .addShortcut(
+ AppInfoShortcutItem(
+ context,
+ componentName.packageName,
+ UserHandle.getUserHandleForUid(Process.myUid())
+ )
+ )
+ if (Flags.dockFeature()) {
+ carUiShortcutsPopupBuilder
+ .addShortcut(buildPinToDockShortcut(componentName, context))
+ }
+ val carUiShortcutsPopup =
+ carUiShortcutsPopupBuilder
+ .build(context, anchorView)
+ carUiShortcutsPopup.show()
+ shortcutsListener.onShortcutsShow(carUiShortcutsPopup)
+ }
+
+ /**
+ * Helper function to construct a shortcut item for the "Pin to Dock" action
+ * within the shortcuts popup.
+ *
+ * @param componentName ComponentName of the app to be pinned.
+ * @param context Application context.
+ * @return A CarUiShortcutsPopup.ShortcutItem for the "Pin to Dock" action, or null
+ * if the feature is not enabled.
+ */
+ private fun buildPinToDockShortcut(
+ componentName: ComponentName,
+ context: Context
+ ): CarUiShortcutsPopup.ShortcutItem? {
+ val helper = DockEventSenderHelper(context)
+ return PinShortcutItem(
+ context.resources,
+ false,
+ { helper.sendPinEvent(componentName) }
+ )
+ { helper.sendUnpinEvent(componentName) }
+ }
+
+ /**
+ * Simple callback interface for notifying clients when a car UI shortcuts
+ * popup is displayed.
+ */
+ interface ShortcutsListener {
+ /**
+ * Called when a CarUiShortcutsPopup view becomes visible.
+ *
+ * @param carUiShortcutsPopup The displayed popup view.
+ */
+ fun onShortcutsShow(carUiShortcutsPopup: CarUiShortcutsPopup)
+ }
+}
diff --git a/libs/appgrid/lib/tests/Android.bp b/libs/appgrid/lib/tests/Android.bp
index 1e8c043..4314dfd 100644
--- a/libs/appgrid/lib/tests/Android.bp
+++ b/libs/appgrid/lib/tests/Android.bp
@@ -15,64 +15,67 @@
package {
default_applicable_licenses: ["Android-Apache-2.0"],
+ default_team: "trendy_team_system_experience",
}
android_test {
- name: "CarAppGridTests",
+ name: "CarAppGridTests",
- srcs: ["src/**/*.java"],
+ srcs: ["src/**/*.java"],
- resource_dirs: ["res"],
+ resource_dirs: ["res"],
- libs: [
- "android.car",
- "android.test.base",
- "android.car-system-stubs",
- ],
+ libs: [
+ "android.car",
+ "android.test.base.stubs.system",
+ "android.car-system-stubs",
+ ],
- optimize: {
- enabled: false,
- },
+ optimize: {
+ enabled: false,
+ },
- static_libs: [
- "android.car.testapi",
- "android.car.test.utils",
- "androidx.test.core",
- "androidx.test.runner",
- "androidx.test.rules",
- "androidx.test.espresso.core",
- "androidx.test.espresso.contrib",
- "androidx.test.espresso.intents",
- "androidx.test.ext.junit",
- "hamcrest-library",
- "mockito-target-extended",
- "truth",
- "testables",
- "CarAppGrid-lib"
- ],
+ static_libs: [
+ "android.car.testapi",
+ "android.car.test.utils",
+ "androidx.test.core",
+ "androidx.test.runner",
+ "androidx.test.rules",
+ "androidx.test.espresso.core",
+ "androidx.test.espresso.contrib",
+ "androidx.test.espresso.intents",
+ "androidx.test.ext.junit",
+ "hamcrest-library",
+ "mockito-target-extended",
+ "truth",
+ "testables",
+ "CarAppGrid-lib",
+ ],
- platform_apis: true,
+ platform_apis: true,
- certificate: "platform",
+ certificate: "platform",
- privileged: true,
+ privileged: true,
- manifest: "AndroidManifest.xml",
+ manifest: "AndroidManifest.xml",
- instrumentation_for: "CarAppGrid-lib",
+ instrumentation_for: "CarAppGrid-lib",
- dex_preopt: {
- enabled: false,
- },
+ dex_preopt: {
+ enabled: false,
+ },
- jni_libs: [
- // For mockito extended
- "libdexmakerjvmtiagent",
- "libstaticjvmtiagent",
- ],
+ jni_libs: [
+ // For mockito extended
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ ],
- test_suites: [
- "automotive-tests",
- "device-tests",
- ],
+ test_suites: [
+ "automotive-tests",
+ "device-tests",
+ ],
+ // TODO(b/319708040): re-enable use_resource_processor
+ use_resource_processor: false,
}
diff --git a/docklib-util/res/values/strings.xml b/libs/appgrid/lib/tests/res/values-en-rCA/strings.xml
similarity index 70%
copy from docklib-util/res/values/strings.xml
copy to libs/appgrid/lib/tests/res/values-en-rCA/strings.xml
index 2c24df7..f7fcedc 100644
--- a/docklib-util/res/values/strings.xml
+++ b/libs/appgrid/lib/tests/res/values-en-rCA/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,10 +13,9 @@
~ 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.
- -->
+ -->
-<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_test_title" msgid="4167394338298199728">"AppGridTests"</string>
</resources>
diff --git a/libs/appgrid/lib/tests/src/com/android/car/carlauncher/AppGridActivityTest.java b/libs/appgrid/lib/tests/src/com/android/car/carlauncher/AppGridActivityTest.java
deleted file mode 100644
index d67191b..0000000
--- a/libs/appgrid/lib/tests/src/com/android/car/carlauncher/AppGridActivityTest.java
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * Copyright (C) 2023 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.carlauncher;
-
-import static android.car.settings.CarSettings.Secure.KEY_UNACCEPTED_TOS_DISABLED_APPS;
-import static android.car.settings.CarSettings.Secure.KEY_USER_TOS_ACCEPTED;
-
-import static androidx.test.espresso.Espresso.onView;
-import static androidx.test.espresso.assertion.ViewAssertions.matches;
-import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static androidx.test.espresso.matcher.ViewMatchers.withId;
-
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.atLeast;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.car.drivingstate.CarUxRestrictionsManager;
-import android.content.Intent;
-import android.provider.Settings;
-import android.testing.TestableContext;
-
-import androidx.lifecycle.Lifecycle;
-import androidx.test.InstrumentationRegistry;
-import androidx.test.core.app.ActivityScenario;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-
-/**
- * Programmatic tests for AppGridActivity and AppGridScrollBar
- */
-@RunWith(AndroidJUnit4.class)
-public class AppGridActivityTest {
- private ActivityScenario<AppGridActivity> mActivityScenario;
- private CarUxRestrictionsManager mCarUxRestrictionsManager;
- private PageIndicator mPageIndicator;
-
- @After
- public void tearDown() {
- if (mActivityScenario != null) {
- mActivityScenario.close();
- }
- }
-
- @Test
- public void onCreate_appGridRecyclerView_isVisible() {
- mActivityScenario = ActivityScenario.launch(AppGridActivity.class);
- onView(withId(R.id.apps_grid)).check(matches(isDisplayed()));
- onView(withId(R.id.page_indicator_container)).check(matches(isDisplayed()));
- }
-
- @Test
- public void onResume_ScrollStateIsUpdated() {
- mActivityScenario = ActivityScenario.launch(AppGridActivity.class);
- mActivityScenario.onActivity(activity -> {
- mPageIndicator = mock(PageIndicator.class);
- activity.setPageIndicator(mPageIndicator);
- });
- // The activity needs to be reset to go to the RESUMED state after the
- // mock object is set up
- mActivityScenario.moveToState(Lifecycle.State.CREATED);
- mActivityScenario.moveToState(Lifecycle.State.RESUMED);
- onView(withId(R.id.apps_grid)).check(matches(isDisplayed()));
- onView(withId(R.id.page_indicator_container)).check(matches(isDisplayed()));
- // onResumed will trigger LauncherViewModel.onChanged when the activity first started
- // which triggers this call again
- verify(mPageIndicator, atLeast(1)).updatePageCount(anyInt());
- }
-
- @Test
- public void onStop_CarUxRestrictionsManager_unregisterListener() {
- mActivityScenario = ActivityScenario.launch(AppGridActivity.class);
- mActivityScenario.onActivity(activity -> {
- mCarUxRestrictionsManager = mock(CarUxRestrictionsManager.class);
- activity.setCarUxRestrictionsManager(mCarUxRestrictionsManager);
- });
- mActivityScenario.moveToState(Lifecycle.State.DESTROYED);
- verify(mCarUxRestrictionsManager, times(1)).unregisterListener();
- }
-
- @Test
- public void onCreate_tosIsAccepted_tosContentObserversAreNull() {
- TestableContext mContext = new TestableContext(InstrumentationRegistry.getContext());
- Settings.Secure.putInt(mContext.getContentResolver(), KEY_USER_TOS_ACCEPTED, 2);
-
-
- mActivityScenario = ActivityScenario.launch(new Intent(mContext, AppGridActivity.class));
-
- mActivityScenario.onActivity(activity -> {
- assertNull(activity.mTosContentObserver); // Content observer not setup
- assertNull(activity.mTosDisabledAppsContentObserver); // Content observer not setup
- });
- }
-
- @Test
- public void afterTosIsAccepted_unregisterTosContentObservers() {
- TestableContext mContext = new TestableContext(InstrumentationRegistry.getContext());
- Settings.Secure.putInt(mContext.getContentResolver(), KEY_USER_TOS_ACCEPTED, 1);
-
- mActivityScenario = ActivityScenario.launch(new Intent(mContext, AppGridActivity.class));
-
- mActivityScenario.onActivity(activity -> {
- assertNotNull(activity.mTosContentObserver); // Content observer is setup
- assertNotNull(activity.mTosDisabledAppsContentObserver); // Content observer is setup
-
- // Accept TOS
- Settings.Secure.putInt(mContext.getContentResolver(), KEY_USER_TOS_ACCEPTED, 2);
- activity.mTosContentObserver.onChange(true);
- });
-
- // Content observer is null after tos is accepted
- mActivityScenario.onActivity(activity -> {
- assertNull(activity.mTosContentObserver);
- assertNull(activity.mTosDisabledAppsContentObserver);
- });
- }
-
- @Test
- public void tosUninitialized_changesToTosUnaccepted_doNotUnregisterTosContentObservers() {
- TestableContext mContext = new TestableContext(InstrumentationRegistry.getContext());
- Settings.Secure.putInt(mContext.getContentResolver(), KEY_USER_TOS_ACCEPTED, 0);
-
- mActivityScenario = ActivityScenario.launch(new Intent(mContext, AppGridActivity.class));
-
- mActivityScenario.onActivity(activity -> {
- assertNotNull(activity.mTosContentObserver); // Content observer is setup
- assertNotNull(activity.mTosDisabledAppsContentObserver); // Content observer is setup
-
- // TOS changed to unaccepted
- Settings.Secure.putInt(mContext.getContentResolver(), KEY_USER_TOS_ACCEPTED, 1);
- activity.mTosContentObserver.onChange(true);
- });
-
- // Content observer is not null after tos is unaccepted
- mActivityScenario.onActivity(activity -> {
- assertNotNull(activity.mTosContentObserver);
- assertNotNull(activity.mTosDisabledAppsContentObserver);
- });
- }
-
- @Test
- public void
- tosNotAccepted_tosDisabledAppsUpdate_doNotUnregisterTosDisabledAppsContentObserver() {
- TestableContext mContext = new TestableContext(InstrumentationRegistry.getContext());
- Settings.Secure.putInt(mContext.getContentResolver(), KEY_USER_TOS_ACCEPTED, 1);
- Settings.Secure.putString(
- mContext.getContentResolver(),
- KEY_UNACCEPTED_TOS_DISABLED_APPS,
- "tos_disabled_app_one,tos_disabled_app_2");
-
- mActivityScenario = ActivityScenario.launch(new Intent(mContext, AppGridActivity.class));
-
- mActivityScenario.onActivity(activity -> {
- assertNotNull(activity.mTosContentObserver); // Content observer is setup
- assertNotNull(activity.mTosDisabledAppsContentObserver); // Content observer is setup
-
- // TOS changed to unaccepted
- Settings.Secure.putString(mContext.getContentResolver(),
- KEY_UNACCEPTED_TOS_DISABLED_APPS,
- "tos_disabled_app_one");
- activity.mTosContentObserver.onChange(true);
- });
-
- // Content observer is not null after tos is unaccepted
- mActivityScenario.onActivity(activity -> {
- assertNotNull(activity.mTosContentObserver);
- assertNotNull(activity.mTosDisabledAppsContentObserver);
- });
- }
-}
diff --git a/libs/appgrid/lib/tests/src/com/android/car/carlauncher/AppGridAdapterTest.java b/libs/appgrid/lib/tests/src/com/android/car/carlauncher/AppGridAdapterTest.java
index ab357b0..b22e9e8 100644
--- a/libs/appgrid/lib/tests/src/com/android/car/carlauncher/AppGridAdapterTest.java
+++ b/libs/appgrid/lib/tests/src/com/android/car/carlauncher/AppGridAdapterTest.java
@@ -25,9 +25,10 @@
import android.content.Context;
import android.graphics.Rect;
-import android.view.LayoutInflater;
import android.view.View;
+import androidx.test.platform.app.InstrumentationRegistry;
+
import com.android.car.carlauncher.pagination.PageIndexingHelper;
import com.android.car.carlauncher.recyclerview.AppGridAdapter;
import com.android.car.carlauncher.recyclerview.AppItemViewHolder;
@@ -37,18 +38,18 @@
import org.mockito.Mock;
public class AppGridAdapterTest {
-
- @Mock public Context mMockContext;
- @Mock public LayoutInflater mMockLayoutInflater;
- @Mock public LauncherViewModel mMockLauncherModel;
- @Mock public AppItemViewHolder.AppItemDragCallback mMockDragCallback;
- @Mock public AppGridPageSnapper.AppGridPageSnapCallback mMockSnapCallback;
- @Mock public Rect mMockPageBound;
+ private final Context mContext =
+ InstrumentationRegistry.getInstrumentation().getTargetContext();
+ @Mock
+ public AppItemViewHolder.AppItemDragCallback mMockDragCallback;
+ @Mock
+ public AppGridPageSnapper.AppGridPageSnapCallback mMockSnapCallback;
+ @Mock
+ public Rect mMockPageBound;
public AppGridAdapter mTestAppGridAdapter;
@Before
public void setUp() throws Exception {
- mMockLauncherModel = mock(LauncherViewModel.class);
mMockDragCallback = mock(AppItemViewHolder.AppItemDragCallback.class);
mMockSnapCallback = mock(AppGridPageSnapper.AppGridPageSnapCallback.class);
}
@@ -57,9 +58,9 @@
public void testPageRounding_getItemCount_getPageCount() {
int numOfCols = 5;
int numOfRows = 3;
- mTestAppGridAdapter = new AppGridAdapter(mMockContext, numOfCols, numOfRows,
- PageOrientation.HORIZONTAL,
- mMockLayoutInflater, mMockLauncherModel, mMockDragCallback, mMockSnapCallback);
+ mTestAppGridAdapter = new AppGridAdapter(mContext, numOfCols, numOfRows,
+ mMockDragCallback, mMockSnapCallback,
+ mock(AppGridAdapter.AppGridAdapterListener.class), AppGridFragment.Mode.ALL_APPS);
mTestAppGridAdapter.updateViewHolderDimensions(mMockPageBound,
/* appItemWidth */ 260, /* appItemHeight */ 200);
mTestAppGridAdapter = spy(mTestAppGridAdapter);
@@ -85,9 +86,9 @@
numOfCols = 4;
numOfRows = 6;
- mTestAppGridAdapter = new AppGridAdapter(mMockContext, numOfCols, numOfRows,
- PageOrientation.HORIZONTAL,
- mMockLayoutInflater, mMockLauncherModel, mMockDragCallback, mMockSnapCallback);
+ mTestAppGridAdapter = new AppGridAdapter(mContext, numOfCols, numOfRows,
+ mMockDragCallback, mMockSnapCallback,
+ mock(AppGridAdapter.AppGridAdapterListener.class), AppGridFragment.Mode.ALL_APPS);
mTestAppGridAdapter.updateViewHolderDimensions(mMockPageBound,
/* appItemWidth */ 260, /* appItemHeight */ 200);
mTestAppGridAdapter = spy(mTestAppGridAdapter);
@@ -114,9 +115,9 @@
// an adapter with 45 items
int numOfCols = 5;
int numOfRows = 3;
- mTestAppGridAdapter = new AppGridAdapter(mMockContext, numOfCols, numOfRows,
- PageOrientation.HORIZONTAL,
- mMockLayoutInflater, mMockLauncherModel, mMockDragCallback, mMockSnapCallback);
+ mTestAppGridAdapter = new AppGridAdapter(mContext, numOfCols, numOfRows,
+ mMockDragCallback, mMockSnapCallback,
+ mock(AppGridAdapter.AppGridAdapterListener.class), AppGridFragment.Mode.ALL_APPS);
mTestAppGridAdapter.updateViewHolderDimensions(mMockPageBound,
/* appItemWidth */ 260, /* appItemHeight */ 200);
mTestAppGridAdapter = spy(mTestAppGridAdapter);
@@ -149,9 +150,9 @@
// an adapter with 45 items
int numOfRows = 5;
int numOfCols = 3;
- mTestAppGridAdapter = new AppGridAdapter(mMockContext, numOfCols, numOfRows,
- /* pageOrientation */ PageOrientation.HORIZONTAL,
- mMockLayoutInflater, mMockLauncherModel, mMockDragCallback, mMockSnapCallback);
+ mTestAppGridAdapter = new AppGridAdapter(mContext, numOfCols, numOfRows,
+ mMockDragCallback, mMockSnapCallback,
+ mock(AppGridAdapter.AppGridAdapterListener.class), AppGridFragment.Mode.ALL_APPS);
mTestAppGridAdapter.updateViewHolderDimensions(mMockPageBound,
/* appItemWidth */ 260, /* appItemHeight */ 200);
mTestAppGridAdapter = spy(mTestAppGridAdapter);
@@ -192,9 +193,9 @@
// an adapter with 40 items, 3 page, and 5 padded empty items
int numOfCols = 5;
int numOfRows = 3;
- mTestAppGridAdapter = new AppGridAdapter(mMockContext, numOfCols, numOfRows,
- PageOrientation.HORIZONTAL,
- mMockLayoutInflater, mMockLauncherModel, mMockDragCallback, mMockSnapCallback);
+ mTestAppGridAdapter = new AppGridAdapter(mContext, numOfCols, numOfRows,
+ mMockDragCallback, mMockSnapCallback,
+ mock(AppGridAdapter.AppGridAdapterListener.class), AppGridFragment.Mode.ALL_APPS);
mTestAppGridAdapter.updateViewHolderDimensions(mMockPageBound,
/* appItemWidth */ 260, /* appItemHeight */ 200);
mTestAppGridAdapter = spy(mTestAppGridAdapter);
@@ -245,9 +246,9 @@
// an adapter with 44 items, 3 page, and 16 padded empty items
int numOfCols = 4;
int numOfRows = 5;
- mTestAppGridAdapter = new AppGridAdapter(mMockContext, numOfCols, numOfRows,
- PageOrientation.HORIZONTAL,
- mMockLayoutInflater, mMockLauncherModel, mMockDragCallback, mMockSnapCallback);
+ mTestAppGridAdapter = new AppGridAdapter(mContext, numOfCols, numOfRows,
+ mMockDragCallback, mMockSnapCallback,
+ mock(AppGridAdapter.AppGridAdapterListener.class), AppGridFragment.Mode.ALL_APPS);
mTestAppGridAdapter.updateViewHolderDimensions(mMockPageBound,
/* appItemWidth */ 260, /* appItemHeight */ 200);
mTestAppGridAdapter = spy(mTestAppGridAdapter);
diff --git a/libs/appgrid/lib/tests/src/com/android/car/carlauncher/AppItemViewHolderTest.java b/libs/appgrid/lib/tests/src/com/android/car/carlauncher/AppItemViewHolderTest.java
deleted file mode 100644
index 9642c21..0000000
--- a/libs/appgrid/lib/tests/src/com/android/car/carlauncher/AppItemViewHolderTest.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (C) 2023 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.carlauncher;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.view.View;
-import android.view.ViewTreeObserver;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.carlauncher.recyclerview.AppItemViewHolder;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(AndroidJUnit4.class)
-public class AppItemViewHolderTest {
-
- private static final String TEST_APP_PACKAGE_NAME = "com.android.car.test";
- private static final String TEST_TOS_DISABLED_APP_CLASS_NAME = "TosDisabledApp";
-
- @Mock private View mView;
- @Mock private Context mContext;
- @Mock private AppItemViewHolder.AppItemDragCallback mDragCallback;
- @Mock private AppGridPageSnapper.AppGridPageSnapCallback mSnapCallback;
- @Mock private ImageView mAppIcon;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
-
- mContext = ApplicationProvider.getApplicationContext();
- }
-
- @Test
- public void testTosDisabledAppsOpacity() {
- setupMocksTosDisabledApps();
-
- AppItemViewHolder appItemViewHolder =
- new AppItemViewHolder(mView, mContext, mDragCallback, mSnapCallback);
-
- ComponentName componentName =
- new ComponentName(TEST_APP_PACKAGE_NAME, TEST_TOS_DISABLED_APP_CLASS_NAME);
-
- AppMetaData metaData =
- new AppMetaData(
- null,
- componentName,
- null,
- true,
- false,
- true,
- null,
- null);
-
- appItemViewHolder.bind(metaData, new AppItemViewHolder.BindInfo(false, null));
-
- verify(mAppIcon).setAlpha(0.46f);
- }
-
- private void setupMocksTosDisabledApps() {
- LinearLayout appItemView = mock(LinearLayout.class);
- ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
- TextView appName = mock(TextView.class);
-
- when(mView.findViewById(R.id.app_item)).thenReturn(appItemView);
- when(appItemView.findViewById(R.id.app_icon)).thenReturn(mAppIcon);
- when(appItemView.findViewById(R.id.app_name)).thenReturn(appName);
- when(mAppIcon.getViewTreeObserver()).thenReturn(viewTreeObserver);
- }
-}
diff --git a/libs/appgrid/lib/tests/src/com/android/car/carlauncher/AppLauncherUtilsTest.java b/libs/appgrid/lib/tests/src/com/android/car/carlauncher/AppLauncherUtilsTest.java
deleted file mode 100644
index c027a72..0000000
--- a/libs/appgrid/lib/tests/src/com/android/car/carlauncher/AppLauncherUtilsTest.java
+++ /dev/null
@@ -1,709 +0,0 @@
-/*
- * Copyright (C) 2023 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.carlauncher;
-
-import static android.car.settings.CarSettings.Secure.KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE;
-import static android.car.settings.CarSettings.Secure.KEY_UNACCEPTED_TOS_DISABLED_APPS;
-import static android.content.pm.ApplicationInfo.CATEGORY_AUDIO;
-import static android.content.pm.ApplicationInfo.CATEGORY_VIDEO;
-import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
-import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS;
-import static android.content.pm.PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
-
-import static com.android.car.carlauncher.AppLauncherUtils.APP_TYPE_LAUNCHABLES;
-import static com.android.car.carlauncher.AppLauncherUtils.APP_TYPE_MEDIA_SERVICES;
-import static com.android.car.carlauncher.AppLauncherUtils.PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR;
-import static com.android.car.carlauncher.AppLauncherUtils.TOS_DISABLED_APPS_SEPARATOR;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.app.ActivityManager;
-import android.car.Car;
-import android.car.content.pm.CarPackageManager;
-import android.car.media.CarMediaManager;
-import android.car.test.mocks.AbstractExtendedMockitoTestCase;
-import android.content.ComponentName;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.LauncherActivityInfo;
-import android.content.pm.LauncherApps;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ServiceInfo;
-import android.content.res.Resources;
-import android.os.Bundle;
-import android.os.UserHandle;
-import android.provider.Settings;
-import android.service.media.MediaBrowserService;
-import android.util.ArraySet;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.ArgumentMatchers;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public final class AppLauncherUtilsTest extends AbstractExtendedMockitoTestCase {
- private static final String TEST_DISABLED_APP_1 = "com.android.car.test.disabled1";
- private static final String TEST_DISABLED_APP_2 = "com.android.car.test.disabled2";
- private static final String TEST_ENABLED_APP = "com.android.car.test.enabled";
- private static final String TEST_TOS_DISABLED_APP_1 = "com.android.car.test.tosdisabled1";
- private static final String TEST_TOS_DISABLED_APP_2 = "com.android.car.test.tosdisabled2";
- private static final String TEST_VIDEO_APP = "com.android.car.test.video";
- // Default media app
- private static final String TEST_MEDIA_TEMPLATE_MBS = "com.android.car.test.mbs";
- // Video app that has a MBS defined but has its own launch activity
- private static final String TEST_VIDEO_MBS = "com.android.car.test.video.mbs";
- // NDO App that has opted in its MBS to launch in car
- private static final String TEST_NDO_MBS_LAUNCHABLE = "com.android.car.test.mbs.launchable";
- // NDO App that has opted out its MBS to launch in car
- private static final String TEST_NDO_MBS_NOT_LAUNCHABLE =
- "com.android.car.test.mbs.notlaunchable";
-
- private static final String CUSTOM_MEDIA_PACKAGE = "com.android.car.radio";
- private static final String CUSTOM_MEDIA_CLASS = "com.android.car.radio.service";
- private static final String CUSTOM_MEDIA_COMPONENT = CUSTOM_MEDIA_PACKAGE
- + "/" + CUSTOM_MEDIA_CLASS;
- private static final String TEST_MIRROR_APP_PKG = "com.android.car.test.mirroring";
- private static final String TOS_INTENT_NAME = "intent:#Intent;action="
- + "com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=true;"
- + "S.mini_flow_extra=GTOS_GATED_FLOW;end";
- private static final String TOS_INTENT_VERIFY = "#Intent;action="
- + "com.android.car.SHOW_USER_TOS_ACTIVITY;B.show_value_prop=true;"
- + "S.mini_flow_extra=GTOS_GATED_FLOW;end";
-
-
- @Mock private Context mMockContext;
- @Mock private LauncherApps mMockLauncherApps;
- @Mock private PackageManager mMockPackageManager;
- @Mock private AppLauncherUtils.ShortcutsListener mMockShortcutsListener;
-
- @Mock private Resources mResources;
-
- @Mock private LauncherActivityInfo mRadioLauncherActivityInfo;
-
- private CarMediaManager mCarMediaManager;
- private CarPackageManager mCarPackageManager;
- private Car mCar;
-
- @Before
- public void setUp() throws Exception {
- // Need for CarMediaManager to get the user from the context.
- when(mMockContext.getUser()).thenReturn(UserHandle.of(ActivityManager.getCurrentUser()));
-
- mCar = Car.createCar(mMockContext, /* handler = */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
- (car, ready) -> {
- if (!ready) {
- mCarPackageManager = null;
- mCarMediaManager = null;
- return;
- }
- mCarPackageManager = (CarPackageManager) car.getCarManager(Car.PACKAGE_SERVICE);
- mCarPackageManager = Mockito.spy(mCarPackageManager);
- mCarMediaManager = (CarMediaManager) car.getCarManager(Car.CAR_MEDIA_SERVICE);
- when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
- });
- }
-
- @After
- public void tearDown() throws Exception {
- if (mCar != null && mCar.isConnected()) {
- mCar.disconnect();
- mCar = null;
- }
- }
-
- @Override
- protected void onSessionBuilder(CustomMockitoSessionBuilder session) {
- session.spyStatic(Settings.Secure.class);
- }
-
- @Test
- public void testGetLauncherApps_MediaCenterAppSwitcher() {
- mockSettingsStringCalls();
- mockPackageManagerQueries();
-
- when(mMockContext.getResources()).thenReturn(mResources);
- when(mResources.getStringArray(eq(
- com.android.car.media.common.R.array.custom_media_packages)))
- .thenReturn(new String[]{CUSTOM_MEDIA_COMPONENT});
-
- // Setup custom media component
- when(mMockLauncherApps.getActivityList(any(), any()))
- .thenReturn(List.of(mRadioLauncherActivityInfo));
- when(mRadioLauncherActivityInfo.getComponentName())
- .thenReturn(new ComponentName(CUSTOM_MEDIA_PACKAGE, CUSTOM_MEDIA_CLASS));
- when(mRadioLauncherActivityInfo.getName())
- .thenReturn(CUSTOM_MEDIA_CLASS);
-
- AppLauncherUtils.LauncherAppsInfo launcherAppsInfo = AppLauncherUtils.getLauncherApps(
- mMockContext, /* appsToHide= */ new ArraySet<>(),
- /* appTypes= */ APP_TYPE_MEDIA_SERVICES,
- /* openMediaCenter= */ false, mMockLauncherApps, mCarPackageManager,
- mMockPackageManager, mCarMediaManager, mMockShortcutsListener,
- TEST_MIRROR_APP_PKG, /* mirroringAppRedirect= */ null);
-
- List<AppMetaData> appMetaData = launcherAppsInfo.getLaunchableComponentsList();
-
- // Only media apps should be present
- assertEquals(Set.of(
- TEST_MEDIA_TEMPLATE_MBS,
- TEST_NDO_MBS_LAUNCHABLE,
- CUSTOM_MEDIA_PACKAGE),
- appMetaData.stream()
- .map(am -> am.getComponentName().getPackageName())
- .collect(Collectors.toSet()));
-
- // This should include all MBS discovered
- assertEquals(5, launcherAppsInfo.getMediaServices().size());
-
- mockPmGetApplicationEnabledSetting(COMPONENT_ENABLED_STATE_ENABLED, TEST_DISABLED_APP_1,
- TEST_DISABLED_APP_2);
-
- launchAllApps(appMetaData);
-
- // Media apps should do only switching and not launch activity
- verify(mMockContext, never()).startActivity(any(), any());
- }
-
- @Test
- public void testGetLauncherApps_Launcher() {
- mockSettingsStringCalls();
- mockPackageManagerQueries();
-
- when(mMockContext.getResources()).thenReturn(mResources);
- when(mResources.getStringArray(eq(
- com.android.car.media.common.R.array.custom_media_packages)))
- .thenReturn(new String[]{CUSTOM_MEDIA_COMPONENT});
-
- // Setup custom media component
- when(mMockLauncherApps.getActivityList(any(), any()))
- .thenReturn(List.of(mRadioLauncherActivityInfo));
- when(mRadioLauncherActivityInfo.getComponentName())
- .thenReturn(new ComponentName(CUSTOM_MEDIA_PACKAGE, CUSTOM_MEDIA_CLASS));
- when(mRadioLauncherActivityInfo.getName())
- .thenReturn(CUSTOM_MEDIA_CLASS);
-
- AppLauncherUtils.LauncherAppsInfo launcherAppsInfo = AppLauncherUtils.getLauncherApps(
- mMockContext, /* appsToHide= */ new ArraySet<>(),
- /* appTypes= */ APP_TYPE_LAUNCHABLES + APP_TYPE_MEDIA_SERVICES,
- /* openMediaCenter= */ true, mMockLauncherApps, mCarPackageManager,
- mMockPackageManager, mCarMediaManager, mMockShortcutsListener,
- TEST_MIRROR_APP_PKG, /* mirroringAppRedirect= */ null);
-
- List<AppMetaData> appMetaData = launcherAppsInfo.getLaunchableComponentsList();
- // mMockLauncherApps is never stubbed, only services & disabled activities are expected.
-
- assertEquals(Set.of(
- TEST_MEDIA_TEMPLATE_MBS,
- TEST_NDO_MBS_LAUNCHABLE,
- CUSTOM_MEDIA_PACKAGE,
- TEST_DISABLED_APP_1,
- TEST_DISABLED_APP_2),
- appMetaData.stream()
- .map(am -> am.getComponentName().getPackageName())
- .collect(Collectors.toSet()));
-
-
- // This should include all MBS discovered
- assertEquals(5, launcherAppsInfo.getMediaServices().size());
-
- mockPmGetApplicationEnabledSetting(COMPONENT_ENABLED_STATE_ENABLED, TEST_DISABLED_APP_1,
- TEST_DISABLED_APP_2);
-
- launchAllApps(appMetaData);
-
- verify(mMockPackageManager).setApplicationEnabledSetting(
- eq(TEST_DISABLED_APP_1), eq(COMPONENT_ENABLED_STATE_ENABLED), eq(0));
-
- verify(mMockPackageManager).setApplicationEnabledSetting(
- eq(TEST_DISABLED_APP_2), eq(COMPONENT_ENABLED_STATE_ENABLED), eq(0));
-
- verify(mMockContext, times(5)).startActivity(any(), any());
-
- verify(mMockPackageManager, never()).setApplicationEnabledSetting(
- eq(TEST_ENABLED_APP), anyInt(), eq(0));
- }
-
-
- @Test
- public void testGetLauncherAppsWithEnableAndTosDisabledApps() {
- mockSettingsStringCalls();
- mockTosPackageManagerQueries();
-
- when(mMockContext.getResources()).thenReturn(mResources);
- when(mResources.getStringArray(eq(
- com.android.car.media.common.R.array.custom_media_packages)))
- .thenReturn(new String[]{CUSTOM_MEDIA_COMPONENT});
-
- AppLauncherUtils.LauncherAppsInfo launcherAppsInfo = AppLauncherUtils.getLauncherApps(
- mMockContext, /* appsToHide= */ new ArraySet<>(),
- /* appTypes= */ APP_TYPE_LAUNCHABLES + APP_TYPE_MEDIA_SERVICES,
- /* openMediaCenter= */ false, mMockLauncherApps, mCarPackageManager,
- mMockPackageManager, mCarMediaManager, mMockShortcutsListener,
- TEST_MIRROR_APP_PKG, /* mirroringAppRedirect= */ null);
-
- List<AppMetaData> appMetaData = launcherAppsInfo.getLaunchableComponentsList();
-
- // mMockLauncherApps is never stubbed, only services & disabled activities are expected.
- assertEquals(3, appMetaData.size());
-
- Resources resources = mock(Resources.class);
- when(mMockContext.getResources()).thenReturn(resources);
- when(resources.getString(anyInt())).thenReturn(TOS_INTENT_NAME);
-
- launchAllApps(appMetaData);
-
- ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
- verify(mMockContext, times(2)).startActivity(intentCaptor.capture(), any());
-
- String intentUri = intentCaptor.getAllValues().get(0).toUri(0);
- assertEquals(TOS_INTENT_VERIFY, intentUri);
- }
-
- @Test
- public void testGetLauncherAppsWithEnableAndTosDisabledDistractionOptimizedApps() {
- mockSettingsStringCalls();
- mockTosPackageManagerQueries();
-
- when(mMockContext.getResources()).thenReturn(mResources);
- when(mResources.getStringArray(eq(
- com.android.car.media.common.R.array.custom_media_packages)))
- .thenReturn(new String[]{CUSTOM_MEDIA_COMPONENT});
-
- doReturn(true)
- .when(mCarPackageManager)
- .isActivityDistractionOptimized(eq(TEST_TOS_DISABLED_APP_1), any());
- doReturn(true)
- .when(mCarPackageManager)
- .isActivityDistractionOptimized(eq(TEST_TOS_DISABLED_APP_2), any());
-
- AppLauncherUtils.LauncherAppsInfo launcherAppsInfo = AppLauncherUtils.getLauncherApps(
- mMockContext, /* appsToHide= */ new ArraySet<>(),
- /* appTypes= */ APP_TYPE_LAUNCHABLES + APP_TYPE_MEDIA_SERVICES,
- /* openMediaCenter= */ false, mMockLauncherApps, mCarPackageManager,
- mMockPackageManager, mCarMediaManager, mMockShortcutsListener,
- TEST_MIRROR_APP_PKG, /* mirroringAppRedirect= */ null);
-
- List<AppMetaData> appMetaData = launcherAppsInfo.getLaunchableComponentsList();
-
- // mMockLauncherApps is never stubbed, only services & disabled activities are expected.
- assertEquals(3, appMetaData.size());
-
- Resources resources = mock(Resources.class);
- when(mMockContext.getResources()).thenReturn(resources);
- when(resources.getString(anyInt())).thenReturn(TOS_INTENT_NAME);
-
- launchAllApps(appMetaData);
-
- ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
- verify(mMockContext, times(2)).startActivity(intentCaptor.capture(), any());
-
- String intentUri = intentCaptor.getAllValues().get(0).toUri(0);
- assertEquals(TOS_INTENT_VERIFY, intentUri);
- }
-
- private void forceStopInit(ActivityManager activityManager, CarMediaManager carMediaManager,
- ComponentName currentMediaComponentName,
- ComponentName previousMediaComponentName,
- Map<Integer, Boolean> currentModes, boolean isMedia) {
- when(mMockContext.getSystemService(
- ArgumentMatchers.<Class<ActivityManager>>any())).thenReturn(activityManager);
- when(mMockContext.getResources()).thenReturn(mock(Resources.class));
- if (isMedia) {
- currentModes.forEach((mode, current) -> {
- if (current) {
- when(carMediaManager.getMediaSource(mode)).thenReturn(
- currentMediaComponentName);
- } else {
- when(carMediaManager.getMediaSource(mode)).thenReturn(
- previousMediaComponentName);
- }
- });
- List<ComponentName> lastMediaSources = new ArrayList<>();
- lastMediaSources.add(currentMediaComponentName);
- if (previousMediaComponentName != null) {
- lastMediaSources.add(previousMediaComponentName);
- }
- when(carMediaManager.getLastMediaSources(anyInt())).thenReturn(lastMediaSources);
- } else {
- when(carMediaManager.getMediaSource(anyInt())).thenReturn(previousMediaComponentName);
- }
- }
-
- @Test
- public void forceStopNonMediaApp_shouldStopApp() {
- String packageName = "com.example.app";
- CharSequence displayName = "App";
- ActivityManager activityManager = mock(ActivityManager.class);
- CarMediaManager carMediaManager = mock(CarMediaManager.class);
- forceStopInit(activityManager, carMediaManager,
- /* currentMediaComponentName= */null, /* previousMediaComponentName= */null,
- /* currentModes= */Map.of(), /* isMedia= */false);
- Map<ComponentName, ResolveInfo> mediaServices = new HashMap<>();
-
- AppLauncherUtils.forceStop(packageName, mMockContext, displayName, carMediaManager,
- mediaServices, mMockShortcutsListener);
-
- verify(activityManager).forceStopPackage(packageName);
- verify(mMockShortcutsListener).onStopAppSuccess(nullable(String.class));
- verify(carMediaManager, never()).setMediaSource(nullable(ComponentName.class), anyInt());
- }
-
- @Test
- public void forceStopCurrentPlaybackOnlyMediaApp_shouldSetPlaybackOnlyToPreviousAndStopApp() {
- String packageName = "com.example.app";
- CharSequence displayName = "App";
- ActivityManager activityManager = mock(ActivityManager.class);
- CarMediaManager carMediaManager = mock(CarMediaManager.class);
- ComponentName currentMediaComponentName = new ComponentName(packageName,
- "com.example.service");
- ComponentName previousMediaComponentName = new ComponentName("test", "test");
- Map<Integer, Boolean> currentModes = new HashMap<>();
- currentModes.put(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK, true);
- currentModes.put(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE, false);
- forceStopInit(activityManager, carMediaManager, currentMediaComponentName,
- previousMediaComponentName, /* currentModes= */currentModes, /* isMedia= */true);
- Map<ComponentName, ResolveInfo> mediaServices = new HashMap<>();
-
- AppLauncherUtils.forceStop(packageName, mMockContext, displayName, carMediaManager,
- mediaServices, mMockShortcutsListener);
-
- verify(activityManager).forceStopPackage(packageName);
- verify(mMockShortcutsListener).onStopAppSuccess(nullable(String.class));
- verify(carMediaManager).setMediaSource(previousMediaComponentName,
- CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK);
- verify(carMediaManager, never()).setMediaSource(previousMediaComponentName,
- CarMediaManager.MEDIA_SOURCE_MODE_BROWSE);
-
- }
-
- @Test
- public void forceStopCurrentMediaApp_noHistory_shouldSetToOtherMediaServiceAndStopApp() {
- String packageName = "com.example.app";
- CharSequence displayName = "App";
- ActivityManager activityManager = mock(ActivityManager.class);
- CarMediaManager carMediaManager = mock(CarMediaManager.class);
- ComponentName currentMediaComponentName = new ComponentName(packageName,
- "com.example.service");
- ComponentName otherMediaComponentName = new ComponentName("other.package",
- "other.test");
- Map<Integer, Boolean> currentModes = new HashMap<>();
- currentModes.put(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK, true);
- currentModes.put(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE, true);
- forceStopInit(activityManager, carMediaManager, currentMediaComponentName,
- /* previousMediaComponentName= */null,
- /* currentModes= */currentModes, /* isMedia= */true);
- Map<ComponentName, ResolveInfo> mediaServices = new HashMap<>();
- mediaServices.put(otherMediaComponentName, mock(ResolveInfo.class));
-
- AppLauncherUtils.forceStop(packageName, mMockContext, displayName, carMediaManager,
- mediaServices, mMockShortcutsListener);
-
- verify(activityManager).forceStopPackage(packageName);
- verify(mMockShortcutsListener).onStopAppSuccess(nullable(String.class));
- verify(carMediaManager, times(2))
- .setMediaSource(eq(otherMediaComponentName), anyInt());
- }
-
- @Test
- public void forceStopNonCurrentMediaApp_shouldOnlyStopApp() {
- String packageName = "com.example.app";
- CharSequence displayName = "App";
- ActivityManager activityManager = mock(ActivityManager.class);
- CarMediaManager carMediaManager = mock(CarMediaManager.class);
- ComponentName currentMediaComponentName = new ComponentName(packageName,
- "com.example.service");
- ComponentName previousMediaComponentName = new ComponentName("test", "test");
- forceStopInit(activityManager, carMediaManager, currentMediaComponentName,
- previousMediaComponentName, /* currentModes= */Collections.emptyMap(),
- /* isMedia= */true);
- Map<ComponentName, ResolveInfo> mediaServices = new HashMap<>();
-
- AppLauncherUtils.forceStop(packageName, mMockContext, displayName, carMediaManager,
- mediaServices, mMockShortcutsListener);
-
- verify(activityManager).forceStopPackage(packageName);
- verify(mMockShortcutsListener).onStopAppSuccess(nullable(String.class));
- verify(carMediaManager, never()).setMediaSource(any(ComponentName.class), anyInt());
- }
-
- private void mockPackageManagerQueries() {
- // setup a media template app that uses media service
- ApplicationInfo mbsAppInfo = new ApplicationInfo();
- mbsAppInfo.category = CATEGORY_AUDIO;
- ResolveInfo mbs = constructServiceResolveInfo(TEST_MEDIA_TEMPLATE_MBS);
-
- try {
- Intent mbsIntent = new Intent();
- mbsIntent.setComponent(mbs.getComponentInfo().getComponentName());
- mbsIntent.setAction(MediaBrowserService.SERVICE_INTERFACE);
-
- when(mMockPackageManager.getApplicationInfo(mbs.getComponentInfo().packageName, 0))
- .thenReturn(mbsAppInfo);
-
- doReturn(Arrays.asList(mbs)).when(mMockPackageManager).queryIntentServices(
- argThat((Intent i) -> i != null
- && mbs.getComponentInfo().getComponentName().equals(i.getComponent())),
- eq(PackageManager.GET_META_DATA));
-
- when(mMockPackageManager.getLaunchIntentForPackage(mbs.getComponentInfo().packageName))
- .thenReturn(null);
- } catch (PackageManager.NameNotFoundException e) {
- throw new RuntimeException(e);
- }
-
- // setup a NDO Video app that has MBS but also its own activity, MBS won't be surfaced
- ApplicationInfo videoAppInfo = new ApplicationInfo();
- videoAppInfo.category = CATEGORY_VIDEO;
- ResolveInfo videoApp = constructServiceResolveInfo(TEST_VIDEO_MBS);
- try {
- Intent videoMbsIntent = new Intent();
- videoMbsIntent.setComponent(videoApp.getComponentInfo().getComponentName());
- videoMbsIntent.setAction(MediaBrowserService.SERVICE_INTERFACE);
-
- when(mMockPackageManager.getApplicationInfo(videoApp.getComponentInfo().packageName,
- 0))
- .thenReturn(videoAppInfo);
-
- doReturn(Arrays.asList(videoApp)).when(mMockPackageManager).queryIntentServices(
- argThat((Intent i) -> i != null
- && videoApp.getComponentInfo().getComponentName()
- .equals(i.getComponent())),
- eq(PackageManager.GET_META_DATA));
-
- when(mMockPackageManager.getLaunchIntentForPackage(
- videoApp.getComponentInfo().packageName))
- .thenReturn(new Intent());
- } catch (PackageManager.NameNotFoundException e) {
- throw new RuntimeException(e);
- }
-
- // setup a NDO app that has MBS opted out of launch in car
- ApplicationInfo notlaunchableMBSInfo = new ApplicationInfo();
- notlaunchableMBSInfo.category = CATEGORY_VIDEO;
- ResolveInfo notlaunchableMBSApp = constructServiceResolveInfo(TEST_NDO_MBS_NOT_LAUNCHABLE);
-
- try {
- Intent notlaunachableMbsIntent = new Intent();
- notlaunachableMbsIntent.setComponent(
- notlaunchableMBSApp.getComponentInfo().getComponentName());
- notlaunachableMbsIntent.setAction(MediaBrowserService.SERVICE_INTERFACE);
-
- when(mMockPackageManager.getApplicationInfo(
- notlaunchableMBSApp.getComponentInfo().packageName, 0))
- .thenReturn(notlaunchableMBSInfo);
-
-
- notlaunchableMBSApp.serviceInfo.metaData = new Bundle();
- notlaunchableMBSApp.serviceInfo.metaData
- .putBoolean("androidx.car.app.launchable", false);
-
- doReturn(Arrays.asList(notlaunchableMBSApp))
- .when(mMockPackageManager).queryIntentServices(
- argThat((Intent i) -> i != null
- && notlaunchableMBSApp.getComponentInfo().getComponentName()
- .equals(i.getComponent())),
- eq(PackageManager.GET_META_DATA));
-
- when(mMockPackageManager.getLaunchIntentForPackage(
- notlaunchableMBSApp.getComponentInfo().packageName))
- .thenReturn(new Intent());
- } catch (PackageManager.NameNotFoundException e) {
- throw new RuntimeException(e);
- }
-
-
- // setup a NDO app that has MBS opted in to launch in car
- ApplicationInfo launchableMBSInfo = new ApplicationInfo();
- launchableMBSInfo.category = CATEGORY_VIDEO;
- ResolveInfo launchableMBSApp = constructServiceResolveInfo(TEST_NDO_MBS_LAUNCHABLE);
- try {
- Intent mbsIntent = new Intent();
- mbsIntent.setComponent(launchableMBSApp.getComponentInfo().getComponentName());
- mbsIntent.setAction(MediaBrowserService.SERVICE_INTERFACE);
-
- when(mMockPackageManager.getApplicationInfo(
- launchableMBSApp.getComponentInfo().packageName,
- 0))
- .thenReturn(launchableMBSInfo);
-
-
- launchableMBSApp.serviceInfo.metaData = new Bundle();
- launchableMBSApp.serviceInfo.metaData.putBoolean("androidx.car.app.launchable", true);
-
- doReturn(Arrays.asList(launchableMBSApp)).when(mMockPackageManager).queryIntentServices(
- argThat((Intent i) -> i != null
- && launchableMBSApp.getComponentInfo().getComponentName()
- .equals(i.getComponent())),
- eq(PackageManager.GET_META_DATA));
-
- when(mMockPackageManager.getLaunchIntentForPackage(
- launchableMBSApp.getComponentInfo().packageName))
- .thenReturn(new Intent());
- } catch (PackageManager.NameNotFoundException e) {
- throw new RuntimeException(e);
- }
-
- when(mMockPackageManager.queryIntentServices(any(), eq(PackageManager.GET_RESOLVED_FILTER)))
- .thenAnswer(args -> {
- Intent intent = args.getArgument(0);
- if (intent.getAction().equals(MediaBrowserService.SERVICE_INTERFACE)) {
- return Arrays.asList(mbs, videoApp, notlaunchableMBSApp, launchableMBSApp,
- constructServiceResolveInfo(CUSTOM_MEDIA_PACKAGE));
- }
- return new ArrayList<>();
- });
-
- // setup activities
- when(mMockPackageManager.queryIntentActivities(any(), any())).thenAnswer(args -> {
- Intent intent = args.getArgument(0);
- PackageManager.ResolveInfoFlags flags = args.getArgument(1);
- List<ResolveInfo> resolveInfoList = new ArrayList<>();
- if (intent.getAction().equals(Intent.ACTION_MAIN)) {
- if ((flags.getValue() & MATCH_DISABLED_UNTIL_USED_COMPONENTS) != 0) {
- resolveInfoList.add(constructActivityResolveInfo(TEST_DISABLED_APP_1));
- resolveInfoList.add(constructActivityResolveInfo(TEST_DISABLED_APP_2));
- }
- // Keep custom media component in both MBS and Activity with Launch Intent
- resolveInfoList.add(constructActivityResolveInfo(CUSTOM_MEDIA_PACKAGE));
- // Add apps which will have their own Launcher Activity
- resolveInfoList.add(constructActivityResolveInfo(TEST_VIDEO_MBS));
- resolveInfoList.add(constructActivityResolveInfo(TEST_NDO_MBS_LAUNCHABLE));
- resolveInfoList.add(constructActivityResolveInfo(TEST_NDO_MBS_NOT_LAUNCHABLE));
- }
-
- return resolveInfoList;
- });
- }
-
- private void mockTosPackageManagerQueries() {
- ResolveInfo resolveInfo = constructServiceResolveInfo(TEST_ENABLED_APP);
- try {
- when(mMockPackageManager.getServiceInfo(
- resolveInfo
- .getComponentInfo().getComponentName(),
- PackageManager.GET_META_DATA))
- .thenReturn(new ServiceInfo());
- } catch (PackageManager.NameNotFoundException e) {
- throw new RuntimeException(e);
- }
- when(mMockPackageManager.queryIntentServices(any(), anyInt())).thenAnswer(args -> {
- Intent intent = args.getArgument(0);
- if (intent.getAction().equals(MediaBrowserService.SERVICE_INTERFACE)) {
- return Collections.singletonList(resolveInfo);
- }
- return new ArrayList<>();
- });
- when(mMockPackageManager.queryIntentActivities(any(), any())).thenAnswer(args -> {
- Intent intent = args.getArgument(0);
- PackageManager.ResolveInfoFlags flags = args.getArgument(1);
- List<ResolveInfo> resolveInfoList = new ArrayList<>();
- if (intent.getAction().equals(Intent.ACTION_MAIN)) {
- if ((flags.getValue() & MATCH_DISABLED_COMPONENTS) != 0) {
- resolveInfoList.add(constructActivityResolveInfo(TEST_TOS_DISABLED_APP_1));
- resolveInfoList.add(constructActivityResolveInfo(TEST_TOS_DISABLED_APP_2));
- }
- resolveInfoList.add(constructActivityResolveInfo(TEST_ENABLED_APP));
- }
- return resolveInfoList;
- });
- }
-
- private void mockPmGetApplicationEnabledSetting(int enabledState, String... packages) {
- for (String pkg : packages) {
- when(mMockPackageManager.getApplicationEnabledSetting(pkg)).thenReturn(enabledState);
- }
- }
-
- private void mockSettingsStringCalls() {
- when(mMockContext.createContextAsUser(any(UserHandle.class), anyInt()))
- .thenAnswer(args -> {
- Context context = mock(Context.class);
- ContentResolver contentResolver = mock(ContentResolver.class);
- when(context.getContentResolver()).thenReturn(contentResolver);
- return context;
- });
-
- doReturn(TEST_DISABLED_APP_1 + PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR
- + TEST_DISABLED_APP_2)
- .when(() -> Settings.Secure.getString(any(ContentResolver.class),
- eq(KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE)));
-
- doReturn(TEST_TOS_DISABLED_APP_1 + TOS_DISABLED_APPS_SEPARATOR
- + TEST_TOS_DISABLED_APP_2)
- .when(() -> Settings.Secure.getString(any(ContentResolver.class),
- eq(KEY_UNACCEPTED_TOS_DISABLED_APPS)));
- }
-
- private void launchAllApps(List<AppMetaData> appMetaData) {
- for (AppMetaData meta : appMetaData) {
- Consumer<Context> launchCallback = meta.getLaunchCallback();
- launchCallback.accept(mMockContext);
- }
- }
-
- private static ResolveInfo constructActivityResolveInfo(String packageName) {
- ResolveInfo info = new ResolveInfo();
- info.activityInfo = new ActivityInfo();
- info.activityInfo.packageName = packageName;
- info.activityInfo.name = packageName + ".activity";
- info.activityInfo.applicationInfo = new ApplicationInfo();
- return info;
- }
-
- private static ResolveInfo constructServiceResolveInfo(String packageName) {
- ResolveInfo info = new ResolveInfo();
- info.serviceInfo = new ServiceInfo();
- info.serviceInfo.packageName = packageName;
- info.serviceInfo.name = packageName + ".service";
- info.serviceInfo.applicationInfo = new ApplicationInfo();
- return info;
- }
-}
diff --git a/libs/appgrid/lib/tests/src/com/android/car/carlauncher/LauncherViewModelTest.java b/libs/appgrid/lib/tests/src/com/android/car/carlauncher/LauncherViewModelTest.java
deleted file mode 100644
index 90537c9..0000000
--- a/libs/appgrid/lib/tests/src/com/android/car/carlauncher/LauncherViewModelTest.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * Copyright (C) 2023 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.carlauncher;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import android.car.test.mocks.AbstractExtendedMockitoTestCase;
-import android.content.ComponentName;
-import android.graphics.drawable.Drawable;
-
-import androidx.lifecycle.Observer;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Consumer;
-
-@RunWith(AndroidJUnit4.class)
-public final class LauncherViewModelTest extends AbstractExtendedMockitoTestCase {
- @Rule
- public InstantTaskExecutorRule instantTaskExecutorRule =
- new InstantTaskExecutorRule();
- private LauncherViewModel mLauncherModel;
- private AppLauncherUtils.LauncherAppsInfo mLauncherAppsInfo;
- private Drawable mDrawable = mock(Drawable.class);
- private Consumer mConsumer = mock(Consumer.class);
- private List<LauncherItem> mCustomizedApps;
- private List<LauncherItem> mAlphabetizedApps;
- private List<AppMetaData> mApps;
-
- @Before
- public void setUp() throws Exception {
- mLauncherModel = new LauncherViewModel(
- new File("/data/user/10/com.android.car.carlauncher/files"));
- mCustomizedApps = new ArrayList<>();
- mAlphabetizedApps = new ArrayList<>();
- AppMetaData app1 = createTestAppMetaData("App1", "A");
- AppMetaData app2 = createTestAppMetaData("App2", "B");
- AppMetaData app3 = createTestAppMetaData("App3", "C");
- LauncherItem launcherItem1 = new AppItem(app1);
- LauncherItem launcherItem2 = new AppItem(app2);
- LauncherItem launcherItem3 = new AppItem(app3);
- mApps = new ArrayList<>();
- mApps.add(app1);
- mApps.add(app2);
- mApps.add(app3);
- mAlphabetizedApps = new ArrayList<>();
- mAlphabetizedApps.add(launcherItem1);
- mAlphabetizedApps.add(launcherItem2);
- mAlphabetizedApps.add(launcherItem3);
- mCustomizedApps = new ArrayList<>();
- mCustomizedApps.add(launcherItem2);
- mCustomizedApps.add(launcherItem3);
- mCustomizedApps.add(launcherItem1);
-
- mLauncherAppsInfo = mock(AppLauncherUtils.LauncherAppsInfo.class);
- when(mLauncherAppsInfo.getLaunchableComponentsList()).thenReturn(mApps);
- }
-
- private AppMetaData createTestAppMetaData(String displayName, String componentName) {
- return new AppMetaData(displayName, new ComponentName(componentName, componentName),
- mDrawable, true, false, true, mConsumer, mConsumer);
- }
-
- @Test
- @Ignore("b/304484141")
- public void test_concurrentExecution() throws InterruptedException {
- ExecutorService pool = Executors.newCachedThreadPool();
- for (int i = 0; i < 100; i++) {
- pool.execute(() -> {
- mLauncherModel.loadAppsOrderFromFile();
- });
- pool.execute(() -> {
- mLauncherModel.processAppsInfoFromPlatform(mLauncherAppsInfo);
- });
- }
- pool.shutdown(); // Disable new tasks from being submitted
- if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
- pool.shutdownNow(); // Cancel currently executing tasks
- }
- mLauncherModel.getCurrentLauncher().observeForever(new Observer<>() {
- @Override
- public void onChanged(List<LauncherItem> launcherItems) {
- assertEquals(3, launcherItems.size());
- assertEquals("A", launcherItems.get(0).getPackageName());
- assertEquals("B", launcherItems.get(1).getPackageName());
- assertEquals("C", launcherItems.get(2).getPackageName());
- //remove observer after assertion
- mLauncherModel.getCurrentLauncher().removeObserver(this);
- }
- });
- }
-
- @Test
- public void loadAppsOrderFromFile_first_noOrderFile() throws IOException {
- mLauncherModel.loadAppsOrderFromFile();
- mLauncherModel.processAppsInfoFromPlatform(mLauncherAppsInfo);
- mLauncherModel.getCurrentLauncher().observeForever(launcherItems -> {
- assertEquals(3, launcherItems.size());
- assertEquals("A", launcherItems.get(0).getPackageName());
- assertEquals("B", launcherItems.get(1).getPackageName());
- assertEquals("C", launcherItems.get(2).getPackageName());
- });
- }
-
- @Test
- public void loadAppsOrderFromFile_first_existsOrderFile() {
- mLauncherModel.processAppsInfoFromPlatform(mLauncherAppsInfo);
- mLauncherModel.loadAppsOrderFromFile();
-
- mLauncherModel.setAppPosition(0, mApps.get(2));
- // normally, the observer would make this call
- mLauncherModel.handleAppListChange();
-
- mLauncherModel.loadAppsOrderFromFile();
- mLauncherModel.getCurrentLauncher().observeForever(it -> {
- assertEquals("C", mApps.get(2).getPackageName());
- assertEquals(3, it.size());
- assertEquals("C", it.get(0).getPackageName());
- });
- }
-
- @Test
- public void processAppsInfoFromPlatform_first_noCustomOrderFile() {
- mLauncherModel.processAppsInfoFromPlatform(mLauncherAppsInfo);
- mLauncherModel.loadAppsOrderFromFile();
- mLauncherModel.getCurrentLauncher().observeForever(it -> {
- assertEquals(3, it.size());
- assertEquals("A", it.get(0).getPackageName());
- });
- }
-}
diff --git a/libs/appgrid/lib/tests/src/com/android/car/carlauncher/apporder/AppOrderControllerTest.java b/libs/appgrid/lib/tests/src/com/android/car/carlauncher/apporder/AppOrderControllerTest.java
deleted file mode 100644
index da153fd..0000000
--- a/libs/appgrid/lib/tests/src/com/android/car/carlauncher/apporder/AppOrderControllerTest.java
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- * Copyright (C) 2023 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.carlauncher.apporder;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.ComponentName;
-
-import androidx.lifecycle.MutableLiveData;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.carlauncher.AppItem;
-import com.android.car.carlauncher.AppMetaData;
-import com.android.car.carlauncher.LauncherItem;
-import com.android.car.carlauncher.LauncherItemMessageHelper;
-import com.android.car.carlauncher.LauncherItemProto.LauncherItemMessage;
-import com.android.car.carlauncher.datastore.launcheritem.LauncherItemListSource;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-@RunWith(AndroidJUnit4.class)
-public class AppOrderControllerTest {
- private AppOrderController mController;
- private Map<ComponentName, LauncherItem> mLauncherItemsMap;
- private List<LauncherItem> mDefaultOrder;
- private List<LauncherItem> mCustomizedOrder;
- @Mock
- private LauncherItemListSource mMockDataSource;
- private MutableLiveData<List<LauncherItem>> mCurrentAppList;
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
- when(mMockDataSource.exists()).thenReturn(true);
-
- mLauncherItemsMap = new HashMap<>();
- mDefaultOrder = spy(new ArrayList<>());
- mCustomizedOrder = spy(new ArrayList<>());
- mCurrentAppList = spy(new MutableLiveData<>());
- mCustomizedOrder.add(null);
-
- mController = spy(new AppOrderController(mMockDataSource, mCurrentAppList, mDefaultOrder,
- mCustomizedOrder));
- }
-
- @Test
- public void maybePublishAppList_loadAppListFromPlatform_noPublishing() {
- // tests that multiple platform connection does not publish app list
- mController.loadAppListFromPlatform(mLauncherItemsMap, mDefaultOrder);
- assertThat(mController.appsDataLoadingCompleted()).isFalse();
-
- mController.loadAppListFromPlatform(mLauncherItemsMap, mDefaultOrder);
- assertThat(mController.appsDataLoadingCompleted()).isFalse();
-
- verify(mController, times(2)).maybePublishAppList();
- verify(mCurrentAppList, never()).postValue(any());
- }
-
- @Test
- public void maybePublishAppList_loadAppListFromFile_noPublishing() {
- // tests that multiple file read does not publish app list
- mController.loadAppOrderFromFile();
- assertThat(mController.appsDataLoadingCompleted()).isFalse();
-
- mController.loadAppOrderFromFile();
- assertThat(mController.appsDataLoadingCompleted()).isFalse();
-
- verify(mController, times(2)).maybePublishAppList();
- verify(mCurrentAppList, never()).postValue(any());
- }
-
- @Test
- public void maybePublishAppList_publishing_defaultOrder() {
- when(mController.checkDataSourceExists()).thenReturn(false);
-
- mController.loadAppOrderFromFile();
- assertThat(mController.appsDataLoadingCompleted()).isFalse();
- assertThat(mController.shouldUseCustomOrder()).isFalse();
-
- mController.loadAppListFromPlatform(mLauncherItemsMap, mDefaultOrder);
- verify(mController, times(2)).maybePublishAppList();
- verify(mCurrentAppList, times(1)).postValue(any());
- }
-
- @Test
- public void maybePublishAppList_publishing_customOrder() {
- when(mController.checkDataSourceExists()).thenReturn(true);
- // if the data source exists and the list is non-empty, we expect to use custom oder
- List<LauncherItemMessage> nonEmptyMessageList = new ArrayList<>();
- LauncherItemMessage emptyAppItemMessage =
- (new AppItem("packageName", "className", "displayName", null))
- .convertToMessage(1, 1);
- nonEmptyMessageList.add(emptyAppItemMessage);
- LauncherItemMessageHelper helper = new LauncherItemMessageHelper();
- when(mMockDataSource.readFromFile()).thenReturn(
- helper.convertToMessage(nonEmptyMessageList));
-
- mController.loadAppOrderFromFile();
- assertThat(mController.appsDataLoadingCompleted()).isFalse();
- assertThat(mController.shouldUseCustomOrder()).isTrue();
-
- mController.loadAppListFromPlatform(mLauncherItemsMap, mDefaultOrder);
- verify(mController, times(2)).maybePublishAppList();
- verify(mCurrentAppList, times(1)).postValue(any());
- }
-
- @Test
- public void setAppPosition_postValue() {
- // simulate platform app loading
- LauncherItem testItem1 = new AppItem("packageName1", "className1", "displayName1", null);
- LauncherItem testItem2 = new AppItem("packageName2", "className2", "displayName2", null);
- String packageName3 = "packageName3";
- LauncherItem testItem3 = new AppItem(packageName3, "className3", "displayName3", null);
-
- mLauncherItemsMap.put(new ComponentName("componentName1", "componentName1"), testItem1);
- mLauncherItemsMap.put(new ComponentName("componentName2", "componentName2"), testItem2);
- ComponentName componentName3 = new ComponentName("componentName3", "componentName3");
- mLauncherItemsMap.put(componentName3, testItem3);
- List<LauncherItem> newAppList = new ArrayList<>();
- newAppList.add(testItem1);
- newAppList.add(testItem2);
- newAppList.add(testItem3);
- when(mCurrentAppList.getValue()).thenReturn(newAppList);
-
- // simulate launcher cold start - no app list from file
- mController.loadAppOrderFromFile();
- assertThat(mController.shouldUseCustomOrder()).isFalse();
- mController.loadAppListFromPlatform(mLauncherItemsMap, newAppList);
- verify(mCurrentAppList, times(1)).postValue(any());
-
- AppMetaData mockApp3MetaData = mock(AppMetaData.class);
- when(mockApp3MetaData.getComponentName()).thenReturn(componentName3);
-
- // tests that setAppPosition posts update to the user interface
- mController.setAppPosition(0, mockApp3MetaData);
- verify(mCurrentAppList, times(2)).postValue(any());
-
- // tests that the setAppPosition correctly modifies app position
- assertThat(mCurrentAppList.getValue()).isNotNull();
- assertThat(mCurrentAppList.getValue().isEmpty()).isFalse();
- assertThat(mCurrentAppList.getValue().get(0).getPackageName()).isEqualTo(packageName3);
- }
-}
diff --git a/libs/car-launcher-common/Android.bp b/libs/car-launcher-common/Android.bp
new file mode 100644
index 0000000..ebecfe1
--- /dev/null
+++ b/libs/car-launcher-common/Android.bp
@@ -0,0 +1,40 @@
+//
+// Copyright (C) 2024 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+ name: "CarLauncherCommon",
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ":hidden_api_enabled_srcs",
+ ],
+
+ resource_dirs: ["res"],
+
+ libs: ["android.car"],
+
+ static_libs: [
+ "androidx.lifecycle_lifecycle-extensions",
+ "com.google.android.material_material",
+ "car-ui-lib-no-overlayable",
+ "libprotobuf-java-lite",
+ ],
+
+ manifest: "AndroidManifest.xml",
+}
diff --git a/libs/car-launcher-common/AndroidManifest.xml b/libs/car-launcher-common/AndroidManifest.xml
new file mode 100644
index 0000000..8fee1c0
--- /dev/null
+++ b/libs/car-launcher-common/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.carlaunchercommon">
+ <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
+ <uses-permission android:name="android.permission.FORCE_STOP_PACKAGES"/>
+ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
+</manifest>
diff --git a/libs/car-launcher-common/OWNERS b/libs/car-launcher-common/OWNERS
new file mode 100644
index 0000000..f7ec255
--- /dev/null
+++ b/libs/car-launcher-common/OWNERS
@@ -0,0 +1,8 @@
+# Default code reviewers picked from top 3 or more developers.
+# Please update this list if you find better candidates.
+set noparent
+
[email protected]
[email protected]
[email protected]
[email protected]
diff --git a/libs/car-launcher-common/README b/libs/car-launcher-common/README
new file mode 100644
index 0000000..4dd17d2
--- /dev/null
+++ b/libs/car-launcher-common/README
@@ -0,0 +1,3 @@
+# AAOS Car Launcher Utility Library
+
+This library is to provides helper classes for common launcher components in AAOS.
diff --git a/libs/car-launcher-common/build.gradle b/libs/car-launcher-common/build.gradle
new file mode 100644
index 0000000..f3d1441
--- /dev/null
+++ b/libs/car-launcher-common/build.gradle
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+plugins {
+ id 'com.android.library'
+ id 'kotlin-android'
+ id "com.google.protobuf"
+}
+
+android {
+ namespace 'com.android.car.carlaunchercommon'
+ compileSdk gradle.ext.aaosTargetSDK
+
+ defaultConfig {
+ minSdk gradle.ext.aaosLatestSDK
+ targetSdk gradle.ext.aaosLatestSDK
+ versionCode gradle.ext.getVersionCode()
+ versionName gradle.ext.getVersionName()
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
+ sourceSets {
+ main {
+ manifest.srcFile 'AndroidManifest.xml'
+ res.srcDirs = ['res']
+ java.srcDirs = ['src']
+ }
+
+ androidTest {
+ java.srcDirs += 'tests/src'
+ }
+ }
+
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
+ returnDefaultValues = true
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ lintOptions {
+ disable 'PrivateResource'
+ }
+}
+
+dependencies {
+ compileOnly files(gradle.ext.lib_car_system_stubs)
+ compileOnly files(gradle.ext.lib_system_stubs)
+ // Verify that the code should fail on runtime with hidden api access
+ compileOnly project(":libs:hidden-apis-compat:hidden-apis-disabled")
+ implementation project(":libs:car-ui-lib")
+ implementation 'androidx.annotation:annotation:1.7.1'
+ implementation('com.google.protobuf:protobuf-javalite:3.25.1')
+
+ //Android Test dependencies
+ androidTestCompileOnly files(gradle.ext.lib_system_stubs)
+ androidTestImplementation files(gradle.ext.lib_car_system_stubs)
+ androidTestImplementation project(":libs:hidden-apis-compat:hidden-apis-disabled")
+ androidTestImplementation 'junit:junit:4.13.2'
+ androidTestImplementation "org.mockito:mockito-android:5.10.0"
+ androidTestImplementation "org.mockito:mockito-core:5.10.0"
+ androidTestImplementation "org.mockito.kotlin:mockito-kotlin:3.2.0"
+ androidTestImplementation "androidx.test:rules:1.5.0"
+ androidTestImplementation 'androidx.test:runner:1.5.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation "com.google.truth:truth:1.3.0"
+}
+
+protobuf {
+ protoc {
+ artifact = "com.google.protobuf:protoc:3.25.1"
+ }
+
+ // Generates the java Protobuf-lite code for the Protobufs in this project. See
+ // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
+ // for more information.
+ generateProtoTasks {
+ all().each { task ->
+ task.builtins {
+ java {
+ option 'lite'
+ }
+ }
+ }
+ }
+}
+
+java {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+}
diff --git a/libs/appgrid/lib/res/drawable/ic_app_info.xml b/libs/car-launcher-common/res/drawable/ic_app_info.xml
similarity index 96%
rename from libs/appgrid/lib/res/drawable/ic_app_info.xml
rename to libs/car-launcher-common/res/drawable/ic_app_info.xml
index 420ba81..038eab8 100644
--- a/libs/appgrid/lib/res/drawable/ic_app_info.xml
+++ b/libs/car-launcher-common/res/drawable/ic_app_info.xml
@@ -1,5 +1,5 @@
<!--
- ~ Copyright (C) 2023 The Android Open Source Project
+ ~ Copyright (C) 2024 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.
diff --git a/docklib-util/res/drawable/ic_dock_pin.xml b/libs/car-launcher-common/res/drawable/ic_dock_pin.xml
similarity index 95%
rename from docklib-util/res/drawable/ic_dock_pin.xml
rename to libs/car-launcher-common/res/drawable/ic_dock_pin.xml
index d52696e..44f4a83 100644
--- a/docklib-util/res/drawable/ic_dock_pin.xml
+++ b/libs/car-launcher-common/res/drawable/ic_dock_pin.xml
@@ -1,5 +1,5 @@
<!--
- ~ Copyright (C) 2023 The Android Open Source Project
+ ~ Copyright (C) 2024 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.
diff --git a/docklib-util/res/drawable/ic_dock_unpin.xml b/libs/car-launcher-common/res/drawable/ic_dock_unpin.xml
similarity index 60%
rename from docklib-util/res/drawable/ic_dock_unpin.xml
rename to libs/car-launcher-common/res/drawable/ic_dock_unpin.xml
index ea3a291..469d1b6 100644
--- a/docklib-util/res/drawable/ic_dock_unpin.xml
+++ b/libs/car-launcher-common/res/drawable/ic_dock_unpin.xml
@@ -1,3 +1,19 @@
+<!--
+ ~ Copyright (C) 2024 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"
diff --git a/libs/appgrid/lib/res/drawable/ic_force_stop_caution_icon.xml b/libs/car-launcher-common/res/drawable/ic_force_stop_caution_icon.xml
similarity index 94%
rename from libs/appgrid/lib/res/drawable/ic_force_stop_caution_icon.xml
rename to libs/car-launcher-common/res/drawable/ic_force_stop_caution_icon.xml
index 8adfb22..ea8454e 100644
--- a/libs/appgrid/lib/res/drawable/ic_force_stop_caution_icon.xml
+++ b/libs/car-launcher-common/res/drawable/ic_force_stop_caution_icon.xml
@@ -1,5 +1,5 @@
<!--
- ~ Copyright (C) 2022 The Android Open Source Project
+ ~ Copyright (C) 2024 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.
diff --git a/libs/car-launcher-common/res/values-af/strings.xml b/libs/car-launcher-common/res/values-af/strings.xml
new file mode 100644
index 0000000..f1aefae
--- /dev/null
+++ b/libs/car-launcher-common/res/values-af/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Speld app vas"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Ontspeld app"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Stop app"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Appinligting"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Stop app?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"As jy ’n app verplig om te stop, kan dit verkeerd optree."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> is gestop."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> kan nie gebruik word terwyl jy bestuur nie."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-am/strings.xml b/libs/car-launcher-common/res/values-am/strings.xml
new file mode 100644
index 0000000..adb8b95
--- /dev/null
+++ b/libs/car-launcher-common/res/values-am/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"መተግበሪያን ፒን ያድርጉ"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"መተግበሪያ ይንቀሉ"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"መተግበሪያን አቁም"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"የመተግበሪያ መረጃ"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"መተግበሪያ ይቁም?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"አንድ መተግበሪያን በኃይል እንዲቆም ካደረጉት በትክክል ላይሠራ ይችላል።"</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> እንዲቆም ተደርጓል።"</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> በመንዳት ላይ ሳለ ጥቅም ላይ መዋል አይችልም።"</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-ar/strings.xml b/libs/car-launcher-common/res/values-ar/strings.xml
new file mode 100644
index 0000000..ece78ea
--- /dev/null
+++ b/libs/car-launcher-common/res/values-ar/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"تثبيت التطبيق"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"إزالة تثبيت التطبيق"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"إيقاف التطبيق"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"معلومات التطبيقات"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"هل تريد إيقاف التطبيق؟"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"في حال فرض إيقاف أحد التطبيقات، قد لا يعمل بشكل صحيح."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"تم إيقاف \"<xliff:g id="APP_NAME">%1$s</xliff:g>\"."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"لا يمكن استخدام \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" أثناء القيادة."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-as/strings.xml b/libs/car-launcher-common/res/values-as/strings.xml
new file mode 100644
index 0000000..f068283
--- /dev/null
+++ b/libs/car-launcher-common/res/values-as/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"এপ্ পিন কৰক"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"এপ্ আনপিন কৰক"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"এপ্ বন্ধ কৰক"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"এপৰ তথ্য"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"এপ্ বন্ধ কৰিবনে?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"আপুনি কোনো এপ্ বলেৰে বন্ধ কৰিলে, ই অস্বাভাৱিক আচৰণ কৰিব পাৰে।"</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> বন্ধ কৰা হৈছে।"</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"গাড়ী চলাই থকাৰ সময়ত <xliff:g id="APP_NAME">%1$s</xliff:g> ব্যৱহাৰ কৰিব নোৱাৰি।"</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-az/strings.xml b/libs/car-launcher-common/res/values-az/strings.xml
new file mode 100644
index 0000000..15b9323
--- /dev/null
+++ b/libs/car-launcher-common/res/values-az/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Tətbiqi bərkidin"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Tətbiqi bərkitməyin"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Tətbiqi dayandırın"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Tətbiq haqqında"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Tətbiq dayandırılsın?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Tətbiqi məcburi dayandırsanız, səhv işləyə bilər."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> dayandırılıb."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Avtomobil sürərkən <xliff:g id="APP_NAME">%1$s</xliff:g> istifadə edilə bilməz."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-b+sr+Latn/strings.xml b/libs/car-launcher-common/res/values-b+sr+Latn/strings.xml
new file mode 100644
index 0000000..27efc08
--- /dev/null
+++ b/libs/car-launcher-common/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Zakači aplikaciju"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Otkači aplikaciju"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Zaustavi aplikaciju"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Informacije o aplikaciji"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Želite da zaustavite aplikaciju?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Ako prinudno zaustavite aplikaciju, možda će se ponašati neočekivano."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"Aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g> je zaustavljena."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Ne možete da koristite aplikaciju <xliff:g id="APP_NAME">%1$s</xliff:g> tokom vožnje."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-be/strings.xml b/libs/car-launcher-common/res/values-be/strings.xml
new file mode 100644
index 0000000..d693371
--- /dev/null
+++ b/libs/car-launcher-common/res/values-be/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Замацаваць праграму"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Адмацаваць праграму"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Спыніць праграму"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Звесткі пра праграму"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Спыніць праграму?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Прымусовае спыненне праграмы можа прывесці да збою."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"Праграма \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" спынена."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Праграмай \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" нельга карыстацца, калі вы за рулём."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-bg/strings.xml b/libs/car-launcher-common/res/values-bg/strings.xml
new file mode 100644
index 0000000..2899582
--- /dev/null
+++ b/libs/car-launcher-common/res/values-bg/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Фиксиране на приложението"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Освобождаване на приложението"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Спиране на приложението"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Информация от приложенията"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Да се спре ли приложението?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Ако принудително спрете приложение, то може да не функционира правилно."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"Приложението <xliff:g id="APP_NAME">%1$s</xliff:g> е спряно."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> не може да се използва при шофиране."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-bn/strings.xml b/libs/car-launcher-common/res/values-bn/strings.xml
new file mode 100644
index 0000000..ca41a7d
--- /dev/null
+++ b/libs/car-launcher-common/res/values-bn/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"অ্যাপ পিন করুন"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"অ্যাপ আনপিন করুন"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"অ্যাপ বন্ধ করুন"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"অ্যাপের তথ্য"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"অ্যাপ বন্ধ করবেন?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"আপনি কোনও অ্যাপকে জোর করে বন্ধ করলে, তা সঠিকভাবে কাজ নাও করতে পারে।"</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> বন্ধ করা হয়েছে।"</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"ড্রাইভ করার সময় <xliff:g id="APP_NAME">%1$s</xliff:g> ব্যবহার করা যাবে না।"</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-bs/strings.xml b/libs/car-launcher-common/res/values-bs/strings.xml
new file mode 100644
index 0000000..f08bcfc
--- /dev/null
+++ b/libs/car-launcher-common/res/values-bs/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Kačenje aplikacije"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Otkačivanje aplikacije"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Zaustavljanje aplikacije"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Informacije o aplikaciji"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Zaustaviti aplikaciju?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Ako prisilno zaustavite aplikaciju, moguće je da će se ponašati nepredviđeno."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"Aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g> je zaustavljena."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Nije moguće koristiti aplikaciju <xliff:g id="APP_NAME">%1$s</xliff:g> tokom vožnje."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-ca/strings.xml b/libs/car-launcher-common/res/values-ca/strings.xml
new file mode 100644
index 0000000..9d6ab6a
--- /dev/null
+++ b/libs/car-launcher-common/res/values-ca/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Fixa l\'aplicació"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Deixa de fixar l\'aplicació"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Atura l\'aplicació"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Informació de l\'aplicació"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Vols aturar l\'aplicació?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Si forces l\'aturada d\'una aplicació, és possible que no funcioni correctament."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> s\'ha aturat."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"No es pot utilitzar <xliff:g id="APP_NAME">%1$s</xliff:g> mentre es condueix."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-cs/strings.xml b/libs/car-launcher-common/res/values-cs/strings.xml
new file mode 100644
index 0000000..a9fac2a
--- /dev/null
+++ b/libs/car-launcher-common/res/values-cs/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Připnout aplikaci"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Odepnout aplikaci"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Zastavit aplikaci"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Informace o aplikaci"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Zastavit aplikaci?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Vynucené zastavení může způsobit nepředvídatelné chování aplikace."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"Aplikace <xliff:g id="APP_NAME">%1$s</xliff:g> byla zastavena."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Aplikaci <xliff:g id="APP_NAME">%1$s</xliff:g> nelze používat při řízení."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-da/strings.xml b/libs/car-launcher-common/res/values-da/strings.xml
new file mode 100644
index 0000000..cb3e2db
--- /dev/null
+++ b/libs/car-launcher-common/res/values-da/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Fastgør app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Frigør app"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Stands appen"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Appoplysninger"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Vil du standse appen?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Hvis du tvinger en app til at standse, kan det medføre, at den ikke fungerer korrekt."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> er blevet standset."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> kan ikke bruges under kørsel."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-de/strings.xml b/libs/car-launcher-common/res/values-de/strings.xml
new file mode 100644
index 0000000..0ab11e0
--- /dev/null
+++ b/libs/car-launcher-common/res/values-de/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"App anpinnen"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"App loslösen"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"App beenden"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"App-Info"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"App beenden?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Das Beenden der App zu erzwingen kann zu unerwünschtem Verhalten führen."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> wurde beendet."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> kann während der Fahrt nicht genutzt werden."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-el/strings.xml b/libs/car-launcher-common/res/values-el/strings.xml
new file mode 100644
index 0000000..b389471
--- /dev/null
+++ b/libs/car-launcher-common/res/values-el/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Καρφίτσωμα εφαρμογής"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Ξεκαρφίτσωμα εφαρμογής"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Διακοπή εφαρμογής"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Πληροφορίες εφαρμογής"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Διακοπή εφαρμογής;"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Αν κάνετε αναγκαστική διακοπή μιας εφαρμογής, ενδέχεται να μην λειτουργεί σωστά."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"Η εφαρμογή <xliff:g id="APP_NAME">%1$s</xliff:g> διακόπηκε."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Η χρήση της εφαρμογής <xliff:g id="APP_NAME">%1$s</xliff:g> είναι αδύνατη κατά την οδήγηση."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-en-rAU/strings.xml b/libs/car-launcher-common/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..eb650c6
--- /dev/null
+++ b/libs/car-launcher-common/res/values-en-rAU/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Pin app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Unpin app"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Stop app"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"App info"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Stop app?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"If you force stop an app, it may misbehave."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> has been stopped."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> can\'t be used while driving."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-en-rCA/strings.xml b/libs/car-launcher-common/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000..eb650c6
--- /dev/null
+++ b/libs/car-launcher-common/res/values-en-rCA/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Pin app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Unpin app"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Stop app"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"App info"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Stop app?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"If you force stop an app, it may misbehave."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> has been stopped."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> can\'t be used while driving."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-en-rGB/strings.xml b/libs/car-launcher-common/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..eb650c6
--- /dev/null
+++ b/libs/car-launcher-common/res/values-en-rGB/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Pin app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Unpin app"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Stop app"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"App info"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Stop app?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"If you force stop an app, it may misbehave."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> has been stopped."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> can\'t be used while driving."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-en-rIN/strings.xml b/libs/car-launcher-common/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..eb650c6
--- /dev/null
+++ b/libs/car-launcher-common/res/values-en-rIN/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Pin app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Unpin app"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Stop app"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"App info"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Stop app?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"If you force stop an app, it may misbehave."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> has been stopped."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> can\'t be used while driving."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-en-rXC/strings.xml b/libs/car-launcher-common/res/values-en-rXC/strings.xml
new file mode 100644
index 0000000..d3de402
--- /dev/null
+++ b/libs/car-launcher-common/res/values-en-rXC/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Pin app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Unpin app"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Stop app"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"App info"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Stop app?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"If you force stop an app, it may misbehave."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> has been stopped."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> can\'t be used while driving."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-es-rUS/strings.xml b/libs/car-launcher-common/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..0437b20
--- /dev/null
+++ b/libs/car-launcher-common/res/values-es-rUS/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Fijar app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Dejar de fijar app"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Detener app"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Información de la app"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"¿Quieres detener la app?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Si fuerzas la detención de una app, es posible que funcione incorrectamente."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"Se detuvo <xliff:g id="APP_NAME">%1$s</xliff:g>."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> no puede usarse mientras conduces."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-es/strings.xml b/libs/car-launcher-common/res/values-es/strings.xml
new file mode 100644
index 0000000..cf71ff8
--- /dev/null
+++ b/libs/car-launcher-common/res/values-es/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Fijar aplicación"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Desfijar aplicación"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Detener aplicación"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Información de la aplicación"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"¿Detener aplicación?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Si fuerzas la detención de una aplicación, puede que no funcione correctamente."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> se ha detenido."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> no se puede usar mientras se conduce."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-et/strings.xml b/libs/car-launcher-common/res/values-et/strings.xml
new file mode 100644
index 0000000..a225561
--- /dev/null
+++ b/libs/car-launcher-common/res/values-et/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Rakenduse kinnitamine"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Rakenduse vabastamine"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Peata rakendus"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Rakenduse teave"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Kas peatada rakendus?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Kui sundpeatate rakenduse, võib see valesti toimida."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> peatati."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Rakendust <xliff:g id="APP_NAME">%1$s</xliff:g> ei saa sõidu ajal kasutada."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-eu/strings.xml b/libs/car-launcher-common/res/values-eu/strings.xml
new file mode 100644
index 0000000..9b35f7e
--- /dev/null
+++ b/libs/car-launcher-common/res/values-eu/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Ainguratu aplikazioa"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Kendu aplikazioaren aingura"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Gelditu aplikazioa"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Aplikazioari buruzko informazioa"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Aplikazioa gelditu nahi duzu?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Aplikazioak gelditzera behartzen badituzu, baliteke behar bezala ez funtzionatzea."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"Gelditu da <xliff:g id="APP_NAME">%1$s</xliff:g>."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> ezin da erabili gidatu bitartean."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-fa/strings.xml b/libs/car-launcher-common/res/values-fa/strings.xml
new file mode 100644
index 0000000..774908d
--- /dev/null
+++ b/libs/car-launcher-common/res/values-fa/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"سنجاق کردن برنامه"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"برداشتن سنجاق برنامه"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"متوقف کردن برنامه"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"اطلاعات برنامه"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"برنامه متوقف شود؟"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"توقف اجباری برنامه ممکن است باعث عملکرد نادرست آن شود."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> متوقف شده است."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"هنگام رانندگی نمیتوان از <xliff:g id="APP_NAME">%1$s</xliff:g> استفاده کرد."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-fi/strings.xml b/libs/car-launcher-common/res/values-fi/strings.xml
new file mode 100644
index 0000000..a0d147c
--- /dev/null
+++ b/libs/car-launcher-common/res/values-fi/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Kiinnitä sovellus"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Irrota sovellus"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Sulje sovellus"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Sovelluksen tiedot"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Suljetaanko sovellus?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Jos pakotat sovelluksen sulkeutumaan, se ei välttämättä toimi oikein."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> on suljettu."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> ei voi olla käytössä ajon aikana."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-fr-rCA/strings.xml b/libs/car-launcher-common/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..6ef5f6a
--- /dev/null
+++ b/libs/car-launcher-common/res/values-fr-rCA/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Épingler l\'application"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Annuler l\'épinglage de l\'application"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Arrêter l\'application"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Détails de l\'appli"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Arrêter l\'application?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Si vous forcez l\'arrêt d\'une application, son fonctionnement peut en être affecté."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"L\'application <xliff:g id="APP_NAME">%1$s</xliff:g> a été arrêtée."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> ne peut pas être utilisée en conduisant."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-fr/strings.xml b/libs/car-launcher-common/res/values-fr/strings.xml
new file mode 100644
index 0000000..5777b56
--- /dev/null
+++ b/libs/car-launcher-common/res/values-fr/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Épingler l\'appli"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Retirer l\'appli"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Arrêter l\'appli"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Informations sur l\'appli"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Arrêter l\'appli ?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"L\'arrêt forcé d\'une appli peut provoquer un fonctionnement instable."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"L\'appli <xliff:g id="APP_NAME">%1$s</xliff:g> a été arrêtée."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Impossible d\'utiliser <xliff:g id="APP_NAME">%1$s</xliff:g> en conduisant."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-gl/strings.xml b/libs/car-launcher-common/res/values-gl/strings.xml
new file mode 100644
index 0000000..fe2d18f
--- /dev/null
+++ b/libs/car-launcher-common/res/values-gl/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Fixar aplicación"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Deixar de fixar aplicación"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Deter aplicación"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Información da aplicación"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Queres deter a aplicación?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Se forzas a parada dunha aplicación, é posible que non funcione correctamente."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"Detívose a aplicación <xliff:g id="APP_NAME">%1$s</xliff:g>."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Non se pode utilizar <xliff:g id="APP_NAME">%1$s</xliff:g> mentres se conduce."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-gu/strings.xml b/libs/car-launcher-common/res/values-gu/strings.xml
new file mode 100644
index 0000000..5155eed
--- /dev/null
+++ b/libs/car-launcher-common/res/values-gu/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"ઍપ પિન કરો"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"ઍપ અનપિન કરો"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"ઍપ બંધ કરો"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"ઍપની માહિતી"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"ઍપ બંધ કરીએ?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"જો તમે કોઈ ઍપને ફરજિયાત બંધ કરો, તો તે અયોગ્ય વર્તન કરી શકે છે."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> બંધ કરવામાં આવી છે."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"ડ્રાઇવ કરતી વખતે <xliff:g id="APP_NAME">%1$s</xliff:g>નો ઉપયોગ કરી શકાતો નથી."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-hi/strings.xml b/libs/car-launcher-common/res/values-hi/strings.xml
new file mode 100644
index 0000000..a4fe146
--- /dev/null
+++ b/libs/car-launcher-common/res/values-hi/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"ऐप्लिकेशन को पिन करें"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"ऐप्लिकेशन को अनपिन करें"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"ऐप्लिकेशन को बंद करें"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"ऐप्लिकेशन की जानकारी"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"क्या आपको ऐप्लिकेशन रोकना है?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"अगर किसी ऐप्लिकेशन को ज़बरदस्ती रोका जाता है, तो हो सकता है कि वह ठीक तरह से काम न करें."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> को रोक दिया गया है."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"गाड़ी चलाते समय <xliff:g id="APP_NAME">%1$s</xliff:g> ऐप्लिकेशन इस्तेमाल नहीं किया जा सकता."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-hr/strings.xml b/libs/car-launcher-common/res/values-hr/strings.xml
new file mode 100644
index 0000000..6ec46fd
--- /dev/null
+++ b/libs/car-launcher-common/res/values-hr/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Prikvačite aplikaciju"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Otkvačite aplikaciju"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Zaustavi aplikaciju"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Informacije o aplikaciji"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Želite li zaustaviti aplikaciju?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Ako prisilno zaustavite aplikaciju, možda će se ponašati nepredviđeno."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"Aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g> je zaustavljena."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g> ne može se koristiti tijekom vožnje."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-hu/strings.xml b/libs/car-launcher-common/res/values-hu/strings.xml
new file mode 100644
index 0000000..c8c269e
--- /dev/null
+++ b/libs/car-launcher-common/res/values-hu/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Alkalmazás kitűzése"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Alkalmazás kitűzésének megszüntetése"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Alkalmazás leállítása"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Alkalmazásadatok"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Leállítja az alkalmazást?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Ha egy alkalmazást leállásra kényszerít, lehetséges, hogy hibásan fog működni."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> leállítva."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"A(z) <xliff:g id="APP_NAME">%1$s</xliff:g> nem használható vezetés közben."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-hy/strings.xml b/libs/car-launcher-common/res/values-hy/strings.xml
new file mode 100644
index 0000000..5d118e2
--- /dev/null
+++ b/libs/car-launcher-common/res/values-hy/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Ամրացնել հավելվածը"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Ապամրացնել հավելվածը"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Կանգնեցնել հավելվածը"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Հավելվածի մասին"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Կանգնեցնե՞լ հավելվածը"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Հավելվածի ստիպողական կանգնեցումը կարող է ազդել դրա աշխատանքի վրա։"</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> հավելվածը կանգնեցվեց։"</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> հավելվածը հնարավոր չէ օգտագործել վարելու ժամանակ։"</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-in/strings.xml b/libs/car-launcher-common/res/values-in/strings.xml
new file mode 100644
index 0000000..82e5e2e
--- /dev/null
+++ b/libs/car-launcher-common/res/values-in/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Sematkan aplikasi"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Lepaskan aplikasi"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Hentikan aplikasi"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Info aplikasi"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Hentikan aplikasi?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Jika aplikasi dihentikan paksa, fungsinya mungkin akan terganggu."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> telah dihentikan."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> tidak dapat digunakan saat mengemudi."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-is/strings.xml b/libs/car-launcher-common/res/values-is/strings.xml
new file mode 100644
index 0000000..9969554
--- /dev/null
+++ b/libs/car-launcher-common/res/values-is/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Festa forrit"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Losa forrit"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Loka forriti"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Upplýsingar um forrit"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Loka forriti?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Ef þú þvingar lokun forrits gæti það látið illa."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> var lokað."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Ekki er hægt að nota <xliff:g id="APP_NAME">%1$s</xliff:g> við akstur."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-it/strings.xml b/libs/car-launcher-common/res/values-it/strings.xml
new file mode 100644
index 0000000..d8588f8
--- /dev/null
+++ b/libs/car-launcher-common/res/values-it/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Fissa app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Sblocca app"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Interrompi l\'app"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Informazioni app"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Interrompere l\'app?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Se forzi l\'interruzione di un\'app, questa potrebbe funzionare in modo anomalo."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"L\'app <xliff:g id="APP_NAME">%1$s</xliff:g> è stata interrotta."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Non è possibile usare l\'app <xliff:g id="APP_NAME">%1$s</xliff:g> durante la guida."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-iw/strings.xml b/libs/car-launcher-common/res/values-iw/strings.xml
new file mode 100644
index 0000000..3459473
--- /dev/null
+++ b/libs/car-launcher-common/res/values-iw/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"הצמדת האפליקציה"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"ביטול הצמדת האפליקציה"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"עצירת האפליקציה"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"פרטי האפליקציה"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"לעצור את האפליקציה?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"אם סוגרים אפליקציה באופן ידני, יכול להיות שהיא לא תפעל כמו שצריך."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"עצרת את האפליקציה <xliff:g id="APP_NAME">%1$s</xliff:g>."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"לא ניתן להשתמש באפליקציה <xliff:g id="APP_NAME">%1$s</xliff:g> במהלך הנהיגה."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-ja/strings.xml b/libs/car-launcher-common/res/values-ja/strings.xml
new file mode 100644
index 0000000..d93e110
--- /dev/null
+++ b/libs/car-launcher-common/res/values-ja/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"アプリを固定"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"アプリの固定を解除"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"アプリを停止"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"アプリ情報"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"アプリを停止しますか?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"アプリを強制停止すると、アプリが正常に機能しないことがあります。"</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> を停止しました。"</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"運転中は<xliff:g id="APP_NAME">%1$s</xliff:g>を使用できません。"</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-ka/strings.xml b/libs/car-launcher-common/res/values-ka/strings.xml
new file mode 100644
index 0000000..78988f1
--- /dev/null
+++ b/libs/car-launcher-common/res/values-ka/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"აპის ჩამაგრება"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"აპის ჩამაგრების მოხსნა"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"აპის შეჩერება"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"აპის ინფორმაცია"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"გსურთ აპის შეჩერება?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"თუ აპის მუშაობას ძალით შეაჩერებთ, მან შესაძლოა გაუმართავად განაგრძოს მუშაობა."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> შეჩერებულია."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"მანქანის მართვისას ვერ გამოიყენებთ <xliff:g id="APP_NAME">%1$s</xliff:g> აპს."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-kk/strings.xml b/libs/car-launcher-common/res/values-kk/strings.xml
new file mode 100644
index 0000000..7fb4673
--- /dev/null
+++ b/libs/car-launcher-common/res/values-kk/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Қолданбаны бекіту"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Қолданбаны босату"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Қолданба жұмысын тоқтату"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Қолданба туралы ақпарат"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Қолданба жұмысы тоқтатылсын ба?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Қолданбаны қолмен тоқтату оның жұмысына кері әсерін тигізуі мүмкін."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> жұмысы тоқтатылды."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Көлік жүргізу кезінде <xliff:g id="APP_NAME">%1$s</xliff:g> қолданбасын пайдалануға болмайды."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-km/strings.xml b/libs/car-launcher-common/res/values-km/strings.xml
new file mode 100644
index 0000000..418ca83
--- /dev/null
+++ b/libs/car-launcher-common/res/values-km/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"ខ្ទាស់កម្មវិធី"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"ដកខ្ទាស់កម្មវិធី"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"បញ្ឈប់កម្មវិធី"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"ព័ត៌មានកម្មវិធី"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"បញ្ឈប់កម្មវិធីឬ?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"ប្រសិនបើអ្នកបង្ខំឱ្យបញ្ឈប់កម្មវិធី កម្មវិធីអាចនឹងដំណើរការមិនត្រឹមត្រូវ។"</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> ត្រូវបានបញ្ឈប់។"</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"មិនអាចប្រើ <xliff:g id="APP_NAME">%1$s</xliff:g> នៅពេលបើកបរបានទេ។"</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-kn/strings.xml b/libs/car-launcher-common/res/values-kn/strings.xml
new file mode 100644
index 0000000..c0c43ae
--- /dev/null
+++ b/libs/car-launcher-common/res/values-kn/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"ಆ್ಯಪ್ ಪಿನ್ ಮಾಡಿ"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"ಆ್ಯಪ್ ಅನ್ಪಿನ್ ಮಾಡಿ"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"ಆ್ಯಪ್ ಅನ್ನು ನಿಲ್ಲಿಸಿ"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"ಆ್ಯಪ್ ಮಾಹಿತಿ"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"ಆ್ಯಪ್ ಅನ್ನು ನಿಲ್ಲಿಸಬೇಕೆ?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"ನೀವು ಆ್ಯಪ್ ಅನ್ನು ಬಲವಂತವಾಗಿ ನಿಲ್ಲಿಸಿದರೆ, ಅದು ಸರಿಯಾಗಿ ಕಾರ್ಯನಿರ್ವಹಿಸದಿರಬಹುದು."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> ಅನ್ನು ನಿಲ್ಲಿಸಲಾಗಿದೆ."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"ಡ್ರೈವ್ ಮಾಡುವಾಗ <xliff:g id="APP_NAME">%1$s</xliff:g> ಬಳಸಲು ಸಾಧ್ಯವಾಗುವುದಿಲ್ಲ."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-ko/strings.xml b/libs/car-launcher-common/res/values-ko/strings.xml
new file mode 100644
index 0000000..16ec54d
--- /dev/null
+++ b/libs/car-launcher-common/res/values-ko/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"앱 고정"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"앱 고정 해제"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"앱 종료"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"앱 정보"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"앱을 종료하시겠습니까?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"강제로 앱을 종료하면 예기치 않은 오류가 발생할 수 있습니다."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> 앱이 종료되었습니다."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"운전 중에는 <xliff:g id="APP_NAME">%1$s</xliff:g> 앱을 사용할 수 없습니다."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-ky/strings.xml b/libs/car-launcher-common/res/values-ky/strings.xml
new file mode 100644
index 0000000..3de6f0b
--- /dev/null
+++ b/libs/car-launcher-common/res/values-ky/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Колдонмону кадап коюу"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Колдонмону бошотуу"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Колдонмону токтотуу"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Колдонмо тууралуу"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Колдонмону токтотосузбу?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Колдонмону мажбурлап токтотсоңуз, ал туура эмес иштеп калышы мүмкүн."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> токтотулду."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Айдап баратканда <xliff:g id="APP_NAME">%1$s</xliff:g> колдонулбайт."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-lo/strings.xml b/libs/car-launcher-common/res/values-lo/strings.xml
new file mode 100644
index 0000000..53e137f
--- /dev/null
+++ b/libs/car-launcher-common/res/values-lo/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"ປັກໝຸດແອັບ"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"ຖອດປັກໝຸດແອັບ"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"ຢຸດແອັບ"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"ຂໍ້ມູນແອັບ"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"ຢຸດແອັບໄວ້ບໍ?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"ຫາກທ່ານບັງຄັບປິດແອັບໃດໜຶ່ງ, ມັນອາດເຮັດວຽກຜິດປົກກະຕິໄດ້."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"ຢຸດ <xliff:g id="APP_NAME">%1$s</xliff:g> ແລ້ວ."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"ບໍ່ສາມາດໃຊ້ <xliff:g id="APP_NAME">%1$s</xliff:g> ໃນຂະນະທີ່ຂັບລົດໄດ້."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-lt/strings.xml b/libs/car-launcher-common/res/values-lt/strings.xml
new file mode 100644
index 0000000..4db7df6
--- /dev/null
+++ b/libs/car-launcher-common/res/values-lt/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Prisegti programą"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Atsegti programą"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Sustabdyti programą"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Programos informacija"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Sustabdyti programą?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Jei priverstinai sustabdysite programą, ji gali tinkamai neveikti."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"Programa „<xliff:g id="APP_NAME">%1$s</xliff:g>“ sustabdyta."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Vairuojant negalima naudoti „<xliff:g id="APP_NAME">%1$s</xliff:g>“."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-lv/strings.xml b/libs/car-launcher-common/res/values-lv/strings.xml
new file mode 100644
index 0000000..84541fc
--- /dev/null
+++ b/libs/car-launcher-common/res/values-lv/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Piespraust lietotni"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Atspraust lietotni"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Apturēt lietotnes darbību"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Informācija par lietotni"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Vai apturēt lietotnes darbību?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Piespiedu kārtā apturot lietotnes darbību, var rasties šīs lietotnes darbības traucējumi."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"Lietotnes <xliff:g id="APP_NAME">%1$s</xliff:g> darbība ir apturēta."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Lietotni <xliff:g id="APP_NAME">%1$s</xliff:g> nevar izmantot braukšanas laikā."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-mk/strings.xml b/libs/car-launcher-common/res/values-mk/strings.xml
new file mode 100644
index 0000000..cec5e63
--- /dev/null
+++ b/libs/car-launcher-common/res/values-mk/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Закачување на апликацијата"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Откачување на апликацијата"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Исклучи ја апликацијата"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Информации за апликацијата"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Да се исклучи апликацијата?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Ако исклучите апликација присилно, таа може да не се однесува правилно."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> е исклучена."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> не може да се користи при возење."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-ml/strings.xml b/libs/car-launcher-common/res/values-ml/strings.xml
new file mode 100644
index 0000000..0c207ff
--- /dev/null
+++ b/libs/car-launcher-common/res/values-ml/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"ആപ്പ് പിൻ ചെയ്യുക"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"ആപ്പ് അൺപിൻ ചെയ്യുക"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"ആപ്പിന്റെ പ്രവർത്തനം നിർത്തുക"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"ആപ്പ് വിവരങ്ങൾ"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"ആപ്പിന്റെ പ്രവർത്തനം നിർത്തണോ?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"ഒരു ആപ്പിന്റെ പ്രവർത്തനം നിർബന്ധിതമായി നിർത്തിയാൽ, അത് ശരിയായി പ്രവർത്തിക്കാനിടയില്ല."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> എന്നതിന്റെ പ്രവർത്തനം നിർത്തി."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"ഡ്രൈവിംഗിനിടെ <xliff:g id="APP_NAME">%1$s</xliff:g> ഉപയോഗിക്കാനാകില്ല."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-mn/strings.xml b/libs/car-launcher-common/res/values-mn/strings.xml
new file mode 100644
index 0000000..4c310de
--- /dev/null
+++ b/libs/car-launcher-common/res/values-mn/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Аппыг бэхлэх"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Аппыг бэхэлснийг болиулах"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Аппыг зогсоох"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Аппын мэдээлэл"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Аппыг зогсоох уу?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Хэрэв та аппыг хүчээр зогсоовол энэ нь буруу ажиллаж магадгүй."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g>-г зогсоосон."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Жолоо барьж байх үед <xliff:g id="APP_NAME">%1$s</xliff:g>-г ашиглах боломжгүй."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-mr/strings.xml b/libs/car-launcher-common/res/values-mr/strings.xml
new file mode 100644
index 0000000..0230643
--- /dev/null
+++ b/libs/car-launcher-common/res/values-mr/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"अॅप पिन करा"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"अॅप अनपिन करा"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"अॅप थांबवा"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"ॲप माहिती"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"अॅप थांबवायचे आहे का?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"तुम्ही अॅप सक्तीने थांबवल्यास, ते अयोग्य वर्तन करू शकते."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> थांबवण्यात आले आहे."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"ड्राइव्ह करताना <xliff:g id="APP_NAME">%1$s</xliff:g> वापरता येऊ शकत नाही."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-ms/strings.xml b/libs/car-launcher-common/res/values-ms/strings.xml
new file mode 100644
index 0000000..7f095f8
--- /dev/null
+++ b/libs/car-launcher-common/res/values-ms/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Semat apl"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Nyahsemat apl"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Hentikan apl"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Maklumat apl"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Hentikan apl?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Jika anda menghentikan apl secara paksa, fungsi apl mungkin terganggu."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> telah dihentikan."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> tidak boleh digunakan semasa memandu."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-my/strings.xml b/libs/car-launcher-common/res/values-my/strings.xml
new file mode 100644
index 0000000..cad5ccb
--- /dev/null
+++ b/libs/car-launcher-common/res/values-my/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"အက်ပ်ကို ပင်ထိုးရန်"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"အက်ပ်ကို ပင်ဖြုတ်ရန်"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"အက်ပ် ရပ်ရန်"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"အက်ပ်အချက်အလက်များ"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"အက်ပ်ကို ရပ်မလား။"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"အက်ပ်ကို မဖြစ်မနေ ရပ်ခိုင်းလျှင် အမှားဖြစ်နိုင်သည်။"</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> ကို ရပ်လိုက်ပါပြီ။"</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"ကားမောင်းနေစဉ် <xliff:g id="APP_NAME">%1$s</xliff:g> ကို သုံး၍မရပါ။"</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-nb/strings.xml b/libs/car-launcher-common/res/values-nb/strings.xml
new file mode 100644
index 0000000..e6074ff
--- /dev/null
+++ b/libs/car-launcher-common/res/values-nb/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Fest appen"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Løsne appen"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Avslutt appen"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Appinformasjon"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Vil du avslutte appen?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Hvis du tvinger en app til å avslutte, kan det oppstå problemer."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> er avsluttet."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Du kan ikke bruke <xliff:g id="APP_NAME">%1$s</xliff:g> mens du kjører."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-ne/strings.xml b/libs/car-launcher-common/res/values-ne/strings.xml
new file mode 100644
index 0000000..15fc8b6
--- /dev/null
+++ b/libs/car-launcher-common/res/values-ne/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"एप पिन गर्नुहोस्"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"एप अनपिन गर्नुहोस्"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"एप बन्द गर्नुहोस्"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"एपसम्बन्धी जानकारी"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"एप बन्द गर्ने हो?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"तपाईंले कुनै एप जबरजस्ती रोक्नुभयो भने त्यसले सही तरिकाले काम नगर्न सक्छ।"</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> बन्द गरिएको छ।"</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"गाडी चलाइरहेका बेला <xliff:g id="APP_NAME">%1$s</xliff:g> प्रयोग गर्न मिल्दैन।"</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-nl/strings.xml b/libs/car-launcher-common/res/values-nl/strings.xml
new file mode 100644
index 0000000..caf836d
--- /dev/null
+++ b/libs/car-launcher-common/res/values-nl/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"App vastzetten"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"App losmaken"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"App stoppen"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"App-informatie"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"App stoppen?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Als je een app gedwongen stopt, kan deze onverwacht gedrag vertonen."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> is gestopt."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Je kunt <xliff:g id="APP_NAME">%1$s</xliff:g> niet gebruiken tijdens het rijden."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-or/strings.xml b/libs/car-launcher-common/res/values-or/strings.xml
new file mode 100644
index 0000000..127b09a
--- /dev/null
+++ b/libs/car-launcher-common/res/values-or/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"ଆପ ପିନ କରନ୍ତୁ"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"ଆପ ଅନପିନ କରନ୍ତୁ"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"ଆପକୁ ବନ୍ଦ କରନ୍ତୁ"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"ଆପ୍ ସୂଚନା"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"ଆପକୁ ବନ୍ଦ କରିବେ?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"ଯଦି ଆପଣ ଏକ ଆପକୁ ବାଧ୍ୟତାର ସହ ବନ୍ଦ କରନ୍ତି, ତେବେ ଏହା ଠିକ୍ ଭାବେ କାମ ନକରିପାରେ।"</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g>କୁ ବନ୍ଦ କରାଯାଇଛି।"</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"ଡ୍ରାଇଭ୍ କରିବା ସମୟରେ <xliff:g id="APP_NAME">%1$s</xliff:g> ବ୍ୟବହାର କରାଯାଇପାରିବ ନାହିଁ।"</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-pa/strings.xml b/libs/car-launcher-common/res/values-pa/strings.xml
new file mode 100644
index 0000000..394620d
--- /dev/null
+++ b/libs/car-launcher-common/res/values-pa/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"ਐਪ ਨੂੰ ਪਿੰਨ ਕਰੋ"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"ਐਪ ਨੂੰ ਅਣਪਿੰਨ ਕਰੋ"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"ਐਪ ਬੰਦ ਕਰੋ"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"ਐਪ ਜਾਣਕਾਰੀ"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"ਕੀ ਐਪ ਬੰਦ ਕਰਨੀ ਹੈ?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"ਜੇ ਤੁਸੀਂ ਕਿਸੇ ਐਪ ਨੂੰ ਜ਼ਬਰਦਸਤੀ ਬੰਦ ਕਰਦੇ ਹੋ, ਤਾਂ ਹੋ ਸਕਦਾ ਹੈ ਇਹ ਠੀਕ ਤਰ੍ਹਾਂ ਕੰਮ ਨਾ ਕਰੇ।"</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> ਬੰਦ ਕਰ ਦਿੱਤੀ ਗਈ ਹੈ।"</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"ਗੱਡੀ ਚਲਾਉਣ ਵੇਲੇ <xliff:g id="APP_NAME">%1$s</xliff:g> ਨੂੰ ਵਰਤਿਆ ਨਹੀਂ ਜਾ ਸਕਦਾ।"</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-pl/strings.xml b/libs/car-launcher-common/res/values-pl/strings.xml
new file mode 100644
index 0000000..3decf2a
--- /dev/null
+++ b/libs/car-launcher-common/res/values-pl/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Przypnij aplikację"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Odepnij aplikację"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Zatrzymaj aplikację"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Informacje o aplikacji"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Zatrzymać aplikację?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Jeśli wymusisz zatrzymanie aplikacji, może ona zadziałać nieprawidłowo."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"Aplikacja <xliff:g id="APP_NAME">%1$s</xliff:g> została zatrzymana."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Podczas jazdy nie można korzystać z aplikacji <xliff:g id="APP_NAME">%1$s</xliff:g>."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-pt-rPT/strings.xml b/libs/car-launcher-common/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..48ce4f8
--- /dev/null
+++ b/libs/car-launcher-common/res/values-pt-rPT/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Afixar app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Desafixar app"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Parar app"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Informações da app"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Parar a app?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Se forçar a paragem de uma app, esta pode apresentar um comportamento anormal."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"A app <xliff:g id="APP_NAME">%1$s</xliff:g> foi parada."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Não é possível usar a app <xliff:g id="APP_NAME">%1$s</xliff:g> durante a condução."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-pt/strings.xml b/libs/car-launcher-common/res/values-pt/strings.xml
new file mode 100644
index 0000000..6648f9a
--- /dev/null
+++ b/libs/car-launcher-common/res/values-pt/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Fixar app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Liberar app"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Parar o app"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Informações do app"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Parar o app?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Se você forçar o fechamento de um app, ele pode apresentar mau funcionamento."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"O app <xliff:g id="APP_NAME">%1$s</xliff:g> foi parado."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"O app <xliff:g id="APP_NAME">%1$s</xliff:g> não pode ser usado ao dirigir."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-ro/strings.xml b/libs/car-launcher-common/res/values-ro/strings.xml
new file mode 100644
index 0000000..3ab7385
--- /dev/null
+++ b/libs/car-launcher-common/res/values-ro/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Fixează aplicația"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Anulează fixarea aplicației"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Oprește aplicația"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Informații despre aplicații"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Oprești aplicația?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Dacă oprești forțat o aplicație, aceasta se poate comporta necorespunzător."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> a fost oprită."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Nu poți folosi <xliff:g id="APP_NAME">%1$s</xliff:g> în timp ce conduci."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-ru/strings.xml b/libs/car-launcher-common/res/values-ru/strings.xml
new file mode 100644
index 0000000..47e4924
--- /dev/null
+++ b/libs/car-launcher-common/res/values-ru/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Закрепить приложение"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Открепить приложение"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Остановить"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Сведения о приложении"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Остановить приложение?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Если принудительно остановить приложение, оно может работать некорректно."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"Приложение \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" остановлено."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Приложение \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" нельзя использовать за рулем."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-si/strings.xml b/libs/car-launcher-common/res/values-si/strings.xml
new file mode 100644
index 0000000..a017561
--- /dev/null
+++ b/libs/car-launcher-common/res/values-si/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"යෙදුම අමුණන්න"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"යෙදුම නොඅමුණන්න"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"යෙදුම නවත්වන්න"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"යෙදුම් තතු"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"යෙදුම නවත්වන්න ද?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"ඔබ යෙදුමක් බලෙන් නැවත වුවහොත්, එය වැරදි ලෙස ක්රියා කරනු ඇත."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> නතර කර ඇත."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> හට රිය පදවන අතරේ යතුරු පුවරුව භාවිතා කළ නොහැක."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-sk/strings.xml b/libs/car-launcher-common/res/values-sk/strings.xml
new file mode 100644
index 0000000..911ab53
--- /dev/null
+++ b/libs/car-launcher-common/res/values-sk/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Pripnúť aplikáciu"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Odopnúť aplikáciu"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Zastaviť aplikáciu"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Informácie o aplikácii"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Chcete zastaviť aplikáciu?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Ak vynútite zastavenie aplikácie, môže sa správať zvláštne."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"Aplikácia <xliff:g id="APP_NAME">%1$s</xliff:g> bola zastavená."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Pri šoférovaní nie je možné používať aplikáciu <xliff:g id="APP_NAME">%1$s</xliff:g>."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-sl/strings.xml b/libs/car-launcher-common/res/values-sl/strings.xml
new file mode 100644
index 0000000..3e9ddfc
--- /dev/null
+++ b/libs/car-launcher-common/res/values-sl/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Pripenjanje aplikacije"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Odpenjanje aplikacije"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Ustavi aplikacijo"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Podatki o aplikacijah"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Želite ustaviti aplikacijo?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Če boste vsilili zaustavitev aplikacije, morda ne bo pravilno delovala."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"Aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g> je ustavljena."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Med vožnjo ni mogoče uporabljati aplikacije <xliff:g id="APP_NAME">%1$s</xliff:g>."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-sq/strings.xml b/libs/car-launcher-common/res/values-sq/strings.xml
new file mode 100644
index 0000000..d34e189
--- /dev/null
+++ b/libs/car-launcher-common/res/values-sq/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Gozhdo aplikacionin"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Zhgozhdo aplikacionin"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Ndalo aplikacionin"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Informacione mbi aplikacionin"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Të ndalohet aplikacioni?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Nëse e ndalon me forcë një aplikacion, ai mund të ketë çrregullime në funksionim."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> është ndaluar."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Aplikacioni \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" nuk mund të përdoret gjatë drejtimit të makinës."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-sr/strings.xml b/libs/car-launcher-common/res/values-sr/strings.xml
new file mode 100644
index 0000000..e6f2b3b
--- /dev/null
+++ b/libs/car-launcher-common/res/values-sr/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Закачи апликацију"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Откачи апликацију"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Заустави апликацију"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Информације о апликацији"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Желите да зауставите апликацију?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Ако принудно зауставите апликацију, можда ће се понашати неочекивано."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"Апликација <xliff:g id="APP_NAME">%1$s</xliff:g> је заустављена."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Не можете да користите апликацију <xliff:g id="APP_NAME">%1$s</xliff:g> током вожње."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-sv/strings.xml b/libs/car-launcher-common/res/values-sv/strings.xml
new file mode 100644
index 0000000..8ce82bb
--- /dev/null
+++ b/libs/car-launcher-common/res/values-sv/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Fäst appen"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Lossa appen"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Avsluta appen"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Appinformation"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Vill du avsluta appen?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Om du tvingar appen att avsluta kanske den inte fungerar som den ska."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> har avslutats."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g> går inte att använda under körning."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-sw/strings.xml b/libs/car-launcher-common/res/values-sw/strings.xml
new file mode 100644
index 0000000..cda2ca0
--- /dev/null
+++ b/libs/car-launcher-common/res/values-sw/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Bandika programu"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Bandua programu"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Zima programu"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Maelezo ya programu"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Ungependa kuzima programu?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Huenda programu isifanye kazi ipasavyo, iwapo utailazimisha kuzima."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"Umezima <xliff:g id="APP_NAME">%1$s</xliff:g>."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Huwezi kutumia <xliff:g id="APP_NAME">%1$s</xliff:g> wakati unaendesha gari."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-ta/strings.xml b/libs/car-launcher-common/res/values-ta/strings.xml
new file mode 100644
index 0000000..af3ebdb
--- /dev/null
+++ b/libs/car-launcher-common/res/values-ta/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"ஆப்ஸைப் பின் செய்தல்"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"ஆப்ஸை அகற்றுதல்"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"ஆப்ஸை நிறுத்து"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"ஆப்ஸ் தகவல்கள்"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"ஆப்ஸை நிறுத்தவா?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"ஆப்ஸை உடனே நிறுத்தினால் அது சரியாகச் செயல்படாமல் போகக்கூடும்."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> ஆப்ஸ் நிறுத்தப்பட்டது."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"வாகனம் ஓட்டும்போது <xliff:g id="APP_NAME">%1$s</xliff:g> ஆப்ஸைப் பயன்படுத்த முடியாது."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-te/strings.xml b/libs/car-launcher-common/res/values-te/strings.xml
new file mode 100644
index 0000000..c6e9151
--- /dev/null
+++ b/libs/car-launcher-common/res/values-te/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"యాప్ను పిన్ చేయండి"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"యాప్ను అన్పిన్ చేయండి"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"యాప్ను ఆపివేయండి"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"యాప్ సమాచారం"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"యాప్ను ఆపివేయాలా?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"మీరు యాప్ను ఫోర్స్ స్టాప్ చేస్తే, అది సరిగ్గా పని చేయకపోవచ్చు."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> ఆపివేయబడింది."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"డ్రైవింగ్ చేస్తున్నపుడు <xliff:g id="APP_NAME">%1$s</xliff:g>ను ఉపయోగించలేరు."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-th/strings.xml b/libs/car-launcher-common/res/values-th/strings.xml
new file mode 100644
index 0000000..78dc957
--- /dev/null
+++ b/libs/car-launcher-common/res/values-th/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"ปักหมุดแอป"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"เลิกปักหมุดแอป"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"หยุดแอป"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"ข้อมูลแอป"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"หยุดแอปไหม"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"แอปอาจทำงานผิดพลาดหากคุณบังคับให้หยุด"</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"หยุด <xliff:g id="APP_NAME">%1$s</xliff:g> แล้ว"</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"ใช้ <xliff:g id="APP_NAME">%1$s</xliff:g> ขณะขับรถไม่ได้"</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-tl/strings.xml b/libs/car-launcher-common/res/values-tl/strings.xml
new file mode 100644
index 0000000..668396d
--- /dev/null
+++ b/libs/car-launcher-common/res/values-tl/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"I-pin ang app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"I-unpin ang app"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Ihinto ang app"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Impormasyon ng app"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Ihinto ang app?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Kung sapilitan mong ihihinto ang isang app, posible itong hindi gumana nang maayos."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"Inihinto ang <xliff:g id="APP_NAME">%1$s</xliff:g>."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Hindi magagamit ang <xliff:g id="APP_NAME">%1$s</xliff:g> habang nagmamaneho."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-tr/strings.xml b/libs/car-launcher-common/res/values-tr/strings.xml
new file mode 100644
index 0000000..738ac6c
--- /dev/null
+++ b/libs/car-launcher-common/res/values-tr/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Uygulamayı sabitle"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Uygulamanın sabitlemesini kaldır"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Uygulamayı durdur"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Uygulama bilgisi"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Uygulama durdurulsun mu?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Uygulamayı zorla durdurursanız düzgün çalışmayabilir."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> durduruldu."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"<xliff:g id="APP_NAME">%1$s</xliff:g>, sürüş sırasında kullanılamaz."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-uk/strings.xml b/libs/car-launcher-common/res/values-uk/strings.xml
new file mode 100644
index 0000000..e8df735
--- /dev/null
+++ b/libs/car-launcher-common/res/values-uk/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Закріпити додаток"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Відкріпити додаток"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Припинити роботу додатка"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Інформація про додаток"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Припинити роботу додатка?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Примусове припинення роботи додатка може призвести до збою в ньому."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"Роботу додатка <xliff:g id="APP_NAME">%1$s</xliff:g> припинено."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Додатком <xliff:g id="APP_NAME">%1$s</xliff:g> не можна користуватися під час руху."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-ur/strings.xml b/libs/car-launcher-common/res/values-ur/strings.xml
new file mode 100644
index 0000000..f9ce48c
--- /dev/null
+++ b/libs/car-launcher-common/res/values-ur/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"ایپ پن کریں"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"ایپ کی پن ہٹائیں"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"ایپ کو روکیں"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"ایپ کی معلومات"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"ایپ کو روکیں؟"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"اگر آپ کسی ایپ کو زبردستی روک دیتے ہیں تو یہ غلط برتاؤ کر سکتی ہے۔"</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> کو روک دیا گیا ہے۔"</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"ڈرائیو کرتے وقت <xliff:g id="APP_NAME">%1$s</xliff:g> کا استعمال نہیں کیا جا سکتا۔"</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-uz/strings.xml b/libs/car-launcher-common/res/values-uz/strings.xml
new file mode 100644
index 0000000..d352e59
--- /dev/null
+++ b/libs/car-launcher-common/res/values-uz/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Ilovani mahkamlash"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Ilovani yechish"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Ilovani toʻxtatish"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Ilova haqida"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Ilova toʻxtatilsinmi?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Ilovani majburan toʻxtatish uning ishlashiga taʼsir koʻrsatishi mumkin."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> ilovasi toʻxtatildi."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Avtomobil rejimida <xliff:g id="APP_NAME">%1$s</xliff:g> ishlamaydi."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-vi/strings.xml b/libs/car-launcher-common/res/values-vi/strings.xml
new file mode 100644
index 0000000..4043959
--- /dev/null
+++ b/libs/car-launcher-common/res/values-vi/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Ghim ứng dụng"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Bỏ ghim ứng dụng"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Dừng ứng dụng"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Thông tin ứng dụng"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Dừng ứng dụng?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Nếu bạn buộc một ứng dụng dừng lại, ứng dụng đó có thể hoạt động không đúng cách."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"<xliff:g id="APP_NAME">%1$s</xliff:g> đã bị dừng."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"Không dùng <xliff:g id="APP_NAME">%1$s</xliff:g> được trong khi lái xe."</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-zh-rCN/strings.xml b/libs/car-launcher-common/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..b854ed2
--- /dev/null
+++ b/libs/car-launcher-common/res/values-zh-rCN/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"固定应用"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"取消固定应用"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"停止应用"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"应用信息"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"要停止应用吗?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"强行停止某个应用可能会导致其出现异常。"</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"已停止“<xliff:g id="APP_NAME">%1$s</xliff:g>”。"</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"驾车时无法使用“<xliff:g id="APP_NAME">%1$s</xliff:g>”。"</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-zh-rHK/strings.xml b/libs/car-launcher-common/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..7d76ba2
--- /dev/null
+++ b/libs/car-launcher-common/res/values-zh-rHK/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"固定應用程式"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"取消固定應用程式"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"停止應用程式"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"應用程式資料"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"要停止應用程式嗎?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"強制停止應用程式,可能會導致操作不正常。"</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"已停止「<xliff:g id="APP_NAME">%1$s</xliff:g>」。"</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"駕駛時無法使用「<xliff:g id="APP_NAME">%1$s</xliff:g>」。"</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-zh-rTW/strings.xml b/libs/car-launcher-common/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..f38b89b
--- /dev/null
+++ b/libs/car-launcher-common/res/values-zh-rTW/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"固定應用程式"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"取消固定應用程式"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"停止應用程式"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"應用程式資訊"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"要停止應用程式嗎?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"請注意,強制停止可能會使應用程式出現異常行為。"</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"已停止「<xliff:g id="APP_NAME">%1$s</xliff:g>」。"</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"開車時無法使用「<xliff:g id="APP_NAME">%1$s</xliff:g>」。"</string>
+</resources>
diff --git a/libs/car-launcher-common/res/values-zu/strings.xml b/libs/car-launcher-common/res/values-zu/strings.xml
new file mode 100644
index 0000000..6d52830
--- /dev/null
+++ b/libs/car-launcher-common/res/values-zu/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label" msgid="7319916279393348946">"Phina i-app"</string>
+ <string name="dock_unpin_shortcut_label" msgid="1657441916649851101">"Susa ukuphina i-app"</string>
+ <string name="stop_app_shortcut_label" msgid="5893149773635675147">"Misa i-app"</string>
+ <string name="app_info_shortcut_label" msgid="6724408677044314793">"Ulwazi lwe-app"</string>
+ <string name="stop_app_dialog_title" msgid="7027389231920405129">"Misa i-app?"</string>
+ <string name="stop_app_dialog_text" msgid="4997162043741899166">"Uma uphoqelela ukumisa i-app, kungenzeka ukuthi ingasebenzi kahle."</string>
+ <string name="stop_app_success_toast_text" msgid="3740858295088091414">"I-<xliff:g id="APP_NAME">%1$s</xliff:g> imisiwe."</string>
+ <string name="ndo_launch_fail_toast_text" msgid="633927891331266600">"I-<xliff:g id="APP_NAME">%1$s</xliff:g> ayikwazi ukusetshenziswa ngenkathi ushayela."</string>
+</resources>
diff --git a/docklib-util/res/values/strings.xml b/libs/car-launcher-common/res/values/colors.xml
similarity index 66%
copy from docklib-util/res/values/strings.xml
copy to libs/car-launcher-common/res/values/colors.xml
index 2c24df7..b92b974 100644
--- a/docklib-util/res/values/strings.xml
+++ b/libs/car-launcher-common/res/values/colors.xml
@@ -1,6 +1,6 @@
-<?xml version="1.0" encoding="UTF-8" ?>
+<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2023 The Android Open Source Project
+ ~ Copyright (C) 2024 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.
@@ -14,9 +14,6 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
<resources>
- <!-- todo(b/314817575): update the string to final value -->
- <string name="dock_pin_shortcut_label">Pin to the dock</string>
- <string name="dock_unpin_shortcut_label">Unpin from the dock</string>
+ <color name="shortcuts_icon_color">#FFFFFFFF</color>
</resources>
diff --git a/libs/car-launcher-common/res/values/strings.xml b/libs/car-launcher-common/res/values/strings.xml
new file mode 100644
index 0000000..66933d0
--- /dev/null
+++ b/libs/car-launcher-common/res/values/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="dock_pin_shortcut_label">Pin app</string>
+ <string name="dock_unpin_shortcut_label">Unpin app</string>
+ <string name="stop_app_shortcut_label">Stop app</string>
+ <string name="app_info_shortcut_label">App info</string>
+
+ <!-- Manage applications, title for dialog when killing persistent apps. [CHAR LIMIT=40] -->
+ <string name="stop_app_dialog_title">Stop app?</string>
+ <!-- Manage applications, text for dialog when killing persistent apps. [CHAR LIMIT=200] -->
+ <string name="stop_app_dialog_text">If you force stop an app, it may
+ misbehave.
+ </string>
+ <string name="stop_app_success_toast_text">
+ <xliff:g id="app_name" example="Radio">%1$s</xliff:g>
+ has been stopped.
+ </string>
+
+ <!-- String for toast when user tries to launch NDO app when driving -->
+ <string name="ndo_launch_fail_toast_text">
+ <xliff:g id="app_name" example="Settings">%1$s</xliff:g>
+ can\'t be used while driving.
+ </string>
+</resources>
diff --git a/libs/car-launcher-common/src/com/android/car/carlaunchercommon/proto/ProtoDataSource.kt b/libs/car-launcher-common/src/com/android/car/carlaunchercommon/proto/ProtoDataSource.kt
new file mode 100644
index 0000000..dc52cbf
--- /dev/null
+++ b/libs/car-launcher-common/src/com/android/car/carlaunchercommon/proto/ProtoDataSource.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2024 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.carlaunchercommon.proto
+
+import android.util.Log
+import com.google.protobuf.MessageLite
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+
+/**
+ * Class level abstraction representing a proto file holding app data.
+ *
+ * Only a single controller should hold reference to this class. All methods that perform read or
+ * write operations must be thread safe and idempotent.
+ *
+ * @param <T> the proto object type that this data file is holding
+</T> */
+// TODO: b/301482942 This class is copied from AppGrid. We should reuse it in AppGrid
+abstract class ProtoDataSource<T : MessageLite>(private val dataFile: File) {
+ private var mInputStream: FileInputStream? = null
+ private var mOutputStream: FileOutputStream? = null
+
+ /**
+ * @return true if the file exists on disk, and false otherwise.
+ */
+ fun exists(): Boolean {
+ return try {
+ dataFile.exists() && dataFile.canRead()
+ } catch (e: Exception) {
+ false
+ }
+ }
+
+ /**
+ * Writes the [MessageLite] subclass T to the file represented by this object.
+ * This method will write data as bytes to the declared file using protobuf library.
+ */
+ fun writeToFile(data: T) {
+ try {
+ if (mOutputStream == null) {
+ mOutputStream = FileOutputStream(dataFile, false)
+ }
+ writeDelimitedTo(data, mOutputStream)
+ } catch (e: IOException) {
+ Log.e(TAG, "Dock item list not written to file successfully.", e)
+ } finally {
+ try {
+ mOutputStream?.apply {
+ flush()
+ fd.sync()
+ close()
+ }
+ mOutputStream = null
+ } catch (e: IOException) {
+ Log.e(TAG, "Unable to close output stream. ")
+ }
+ }
+ }
+
+ /**
+ * Reads the [MessageLite] subclass T from the file represented by this object.
+ * This method will parse the bytes using protobuf library.
+ */
+ fun readFromFile(): T? {
+ if (!exists()) {
+ Log.e(TAG, "File does not exist. Cannot read from file.")
+ return null
+ }
+ var result: T? = null
+ try {
+ if (mInputStream == null) {
+ mInputStream = FileInputStream(dataFile)
+ }
+ result = parseDelimitedFrom(mInputStream)
+ } catch (e: IOException) {
+ Log.e(TAG, "Read from input stream not successfully")
+ } finally {
+ try {
+ mInputStream?.close()
+ mInputStream = null
+ } catch (e: IOException) {
+ Log.e(TAG, "Unable to close input stream")
+ }
+ }
+ return result
+ }
+
+ /**
+ * This method will be called by [ProtoDataSource.readFromFile].
+ *
+ * Implementation is left to subclass since [MessageLite.parseDelimitedFrom]
+ * requires a defined class at compile time. Subclasses should implement this method by directly
+ * calling YourMessageType.parseDelimitedFrom(inputStream) here.
+ *
+ * @param inputStream the input stream to be which the data source should read from.
+ * @return the object T written to this file.
+ * @throws IOException an IOException for when reading from proto fails.
+ */
+ @Throws(IOException::class)
+ protected abstract fun parseDelimitedFrom(inputStream: InputStream?): T?
+
+ /**
+ * This method will be called by [ProtoDataSource.writeToFile].
+ *
+ * Implementation is left to subclass since [MessageLite.writeDelimitedTo]
+ * requires a defined class at compile time. Subclasses should implement this method by directly
+ * calling T.writeDelimitedTo(outputStream) here.
+ *
+ * @param outputData the output data T to be written to the file.
+ * @param outputStream the output stream which the data should be written to.
+ * @throws IOException an IO Exception for when writing to proto fails.
+ */
+ @Throws(IOException::class)
+ protected abstract fun writeDelimitedTo(outputData: T, outputStream: OutputStream?)
+
+ companion object {
+ private const val TAG = "ProtoDataSource"
+ }
+
+ override fun toString(): String {
+ return dataFile.absolutePath
+ }
+}
diff --git a/libs/car-launcher-common/src/com/android/car/carlaunchercommon/shortcuts/AppInfoShortcutItem.kt b/libs/car-launcher-common/src/com/android/car/carlaunchercommon/shortcuts/AppInfoShortcutItem.kt
new file mode 100644
index 0000000..689f82a
--- /dev/null
+++ b/libs/car-launcher-common/src/com/android/car/carlaunchercommon/shortcuts/AppInfoShortcutItem.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 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.carlaunchercommon.shortcuts
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.UserHandle
+import android.provider.Settings
+import com.android.car.carlaunchercommon.R
+import com.android.car.hidden.apis.HiddenApiAccess.startActivityAsUser
+import com.android.car.ui.shortcutspopup.CarUiShortcutsPopup
+
+class AppInfoShortcutItem constructor(
+ private val context: Context,
+ private val packageName: String,
+ private val userHandle: UserHandle
+) : CarUiShortcutsPopup.ShortcutItem {
+ private companion object {
+ private const val PACKAGE_URI_PREFIX = "package:"
+ }
+
+ override fun data(): CarUiShortcutsPopup.ItemData {
+ return CarUiShortcutsPopup.ItemData(
+ R.drawable.ic_app_info,
+ context.resources.getString(R.string.app_info_shortcut_label)
+ )
+ }
+
+ override fun onClick(): Boolean {
+ val packageURI = Uri.parse(PACKAGE_URI_PREFIX + packageName)
+ val intent = Intent(
+ Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+ packageURI
+ )
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivityAsUser(context, intent, userHandle)
+ return true
+ }
+
+ override fun isEnabled(): Boolean {
+ return true
+ }
+}
diff --git a/libs/car-launcher-common/src/com/android/car/carlaunchercommon/shortcuts/ForceStopShortcutItem.kt b/libs/car-launcher-common/src/com/android/car/carlaunchercommon/shortcuts/ForceStopShortcutItem.kt
new file mode 100644
index 0000000..f0cc2a2
--- /dev/null
+++ b/libs/car-launcher-common/src/com/android/car/carlaunchercommon/shortcuts/ForceStopShortcutItem.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2024 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.carlaunchercommon.shortcuts
+
+import android.app.Activity
+import android.app.ActivityManager
+import android.app.admin.DevicePolicyManager
+import android.car.media.CarMediaManager
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.os.UserHandle
+import android.os.UserManager
+import android.util.Log
+import android.view.WindowManager
+import android.widget.Toast
+import androidx.annotation.VisibleForTesting
+import com.android.car.carlaunchercommon.R
+import com.android.car.hidden.apis.HiddenApiAccess.hasBaseUserRestriction
+import com.android.car.hidden.apis.HiddenApiAccess.isDebuggable
+import com.android.car.ui.AlertDialogBuilder
+import com.android.car.ui.shortcutspopup.CarUiShortcutsPopup
+
+/**
+ * @property context the [Context] for the user that the app is running in.
+ * @property mediaServiceComponents list of [ComponentName] of the services the adhere to the media
+ * service interface
+ */
+open class ForceStopShortcutItem(
+ private val context: Context,
+ private val packageName: String,
+ private val displayName: CharSequence,
+ private val carMediaManager: CarMediaManager?,
+ private val mediaServiceComponents: Set<ComponentName>
+) : CarUiShortcutsPopup.ShortcutItem {
+ // todo(b/312718542): hidden class(CarMediaManager) usage
+
+ companion object {
+ private const val TAG = "ForceStopShortcutItem"
+ private val DEBUG = isDebuggable()
+ }
+
+ override fun data(): CarUiShortcutsPopup.ItemData {
+ return CarUiShortcutsPopup.ItemData(
+ R.drawable.ic_force_stop_caution_icon,
+ context.resources.getString(
+ R.string.stop_app_shortcut_label
+ )
+ )
+ }
+
+ override fun onClick(): Boolean {
+ val builder = getAlertDialogBuilder(context)
+ .setTitle(R.string.stop_app_dialog_title)
+ .setMessage(R.string.stop_app_dialog_text)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ forceStop(packageName, displayName)
+ }
+ .setNegativeButton(
+ android.R.string.cancel,
+ null // listener
+ )
+ builder.create().let {
+ if (context !is Activity || context.window.decorView.windowToken == null) {
+ // If the context is not an Activity or lacks a valid window token,
+ // it's likely we're in a non-Activity context (e.g., Service, SystemUI).
+ // To ensure the AlertDialog is displayed properly, we explicitly set its window
+ // type to SYSTEM_ALERT, allowing it to overlay other windows, even from SystemUI.
+ it.window?.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT)
+ }
+ it.show()
+ }
+ return true
+ }
+
+ override fun isEnabled(): Boolean {
+ return shouldAllowStopApp(packageName)
+ }
+
+ /**
+ * @param packageName name of the package to stop the app
+ * @return true if an app should show the Stop app action
+ */
+ private fun shouldAllowStopApp(packageName: String): Boolean {
+ val dm = context.getSystemService(DevicePolicyManager::class.java)
+ if (dm == null || dm.packageHasActiveAdmins(packageName)) {
+ return false
+ }
+ try {
+ val appInfo = context.packageManager.getApplicationInfo(
+ packageName,
+ PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong())
+ )
+ // Show only if the User has no restrictions to force stop this app
+ if (hasUserRestriction(appInfo)) {
+ return false
+ }
+ // Show only if the app is running
+ if (appInfo.flags and ApplicationInfo.FLAG_STOPPED == 0) {
+ return true
+ }
+ } catch (e: PackageManager.NameNotFoundException) {
+ if (DEBUG) Log.d(TAG, "shouldAllowStopApp() package $packageName was not found")
+ }
+ return false
+ }
+
+ /**
+ * @return true if the user has restrictions to force stop an app with `appInfo`
+ */
+ private fun hasUserRestriction(appInfo: ApplicationInfo): Boolean {
+ val restriction = UserManager.DISALLOW_APPS_CONTROL
+ val userManager = context.getSystemService(UserManager::class.java)
+ if (userManager == null) {
+ if (DEBUG) Log.e(TAG, " Disabled because UserManager is null")
+ return true
+ }
+ if (!userManager.hasUserRestriction(restriction)) {
+ return false
+ }
+ val user = UserHandle.getUserHandleForUid(appInfo.uid)
+ if (hasBaseUserRestriction(userManager, restriction, user)) {
+ if (DEBUG) Log.d(TAG, " Disabled because $user has $restriction restriction")
+ return true
+ }
+ // Not disabled for this User
+ return false
+ }
+
+ /**
+ * Force stops an app
+ */
+ @VisibleForTesting
+ fun forceStop(packageName: String, displayName: CharSequence) {
+ // Both MEDIA_SOURCE_MODE_BROWSE and MEDIA_SOURCE_MODE_PLAYBACK should be replaced to their
+ // previous available values
+ maybeReplaceMediaSource(packageName, CarMediaManager.MEDIA_SOURCE_MODE_BROWSE)
+ maybeReplaceMediaSource(packageName, CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK)
+
+ val activityManager = context.getSystemService(ActivityManager::class.java) ?: return
+ // todo(b/312718542): hidden api(ActivityManager.forceStopPackage) usage
+ activityManager.forceStopPackage(packageName)
+ val message = context.resources.getString(R.string.stop_app_success_toast_text, displayName)
+ createToast(context, message, Toast.LENGTH_LONG).show()
+ }
+
+ /**
+ * Updates the MediaSource to second most recent if [packageName] is current media source.
+ * @param mode media source mode (ex. [CarMediaManager.MEDIA_SOURCE_MODE_BROWSE])
+ */
+ private fun maybeReplaceMediaSource(packageName: String, mode: Int) {
+ if (!isCurrentMediaSource(packageName, mode)) {
+ if (DEBUG) Log.e(TAG, "Not current media source")
+ return
+ }
+ // find the most recent source from history not equal to force-stopping package
+ val mediaSources = carMediaManager?.getLastMediaSources(mode)
+ var componentName = mediaSources?.firstOrNull { it?.packageName != packageName }
+ if (componentName == null) {
+ // no recent package found, find from all available media services.
+ componentName = mediaServiceComponents.firstOrNull { it.packageName != packageName }
+ }
+ if (componentName == null) {
+ if (DEBUG) Log.e(TAG, "Stop-app, no alternative media service found")
+ return
+ }
+ carMediaManager?.setMediaSource(componentName, mode)
+ }
+
+ private fun isCurrentMediaSource(packageName: String, mode: Int): Boolean {
+ val componentName = carMediaManager?.getMediaSource(mode)
+ ?: return false // There is no current media source.
+ if (DEBUG) Log.e(TAG, "isCurrentMediaSource: $packageName, $componentName")
+ return componentName.packageName == packageName
+ }
+
+ /**
+ * Should be overridden in the test to provide a mock [AlertDialogBuilder]
+ */
+ @VisibleForTesting
+ open fun getAlertDialogBuilder(context: Context) = AlertDialogBuilder(context)
+
+ /**
+ * Should be overridden in the test to provide a mock [Toast]
+ */
+ @VisibleForTesting
+ open fun createToast(context: Context, text: CharSequence, duration: Int): Toast =
+ Toast.makeText(context, text, duration)
+}
diff --git a/docklib-util/src/com/android/car/dockutil/shortcuts/PinShortcutItem.kt b/libs/car-launcher-common/src/com/android/car/carlaunchercommon/shortcuts/PinShortcutItem.kt
similarity index 67%
rename from docklib-util/src/com/android/car/dockutil/shortcuts/PinShortcutItem.kt
rename to libs/car-launcher-common/src/com/android/car/carlaunchercommon/shortcuts/PinShortcutItem.kt
index 6aba157..88f95b1 100644
--- a/docklib-util/src/com/android/car/dockutil/shortcuts/PinShortcutItem.kt
+++ b/libs/car-launcher-common/src/com/android/car/carlaunchercommon/shortcuts/PinShortcutItem.kt
@@ -1,7 +1,23 @@
-package com.android.car.dockutil.shortcuts
+/*
+ * Copyright (C) 2024 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.carlaunchercommon.shortcuts
import android.content.res.Resources
-import com.android.car.dockutil.R
+import com.android.car.carlaunchercommon.R
import com.android.car.ui.shortcutspopup.CarUiShortcutsPopup
/**
diff --git a/libs/car-launcher-common/src/com/android/car/carlaunchercommon/toasts/NonDrivingOptimizedLaunchFailedToast.kt b/libs/car-launcher-common/src/com/android/car/carlaunchercommon/toasts/NonDrivingOptimizedLaunchFailedToast.kt
new file mode 100644
index 0000000..17108a5
--- /dev/null
+++ b/libs/car-launcher-common/src/com/android/car/carlaunchercommon/toasts/NonDrivingOptimizedLaunchFailedToast.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 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.carlaunchercommon.toasts
+
+import android.content.Context
+import android.widget.Toast
+import com.android.car.carlaunchercommon.R
+
+/**
+ * Helper to create and show a [Toast] when user taps on a non driving optimized app when
+ * UX restrictions are in effect.
+ */
+class NonDrivingOptimizedLaunchFailedToast {
+ companion object {
+ /**
+ * Show the NDO launch fail toast
+ *
+ * @param displayName app's display name/label
+ */
+ fun showToast(context: Context, displayName: String) {
+ getToast(context, displayName).show()
+ }
+
+ private fun getToast(context: Context, displayName: String): Toast {
+ val warningText: String = context.getResources()
+ .getString(R.string.ndo_launch_fail_toast_text, displayName)
+
+ return Toast.makeText(context, warningText, Toast.LENGTH_LONG)
+ }
+ }
+}
diff --git a/libs/car-launcher-common/tests/Android.bp b/libs/car-launcher-common/tests/Android.bp
new file mode 100644
index 0000000..e72cea6
--- /dev/null
+++ b/libs/car-launcher-common/tests/Android.bp
@@ -0,0 +1,66 @@
+//
+// Copyright (C) 2024 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+ default_team: "trendy_team_system_experience",
+}
+
+android_test {
+ name: "CarLauncherCommonTests",
+
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+
+ libs: [
+ "android.test.base.stubs.system",
+ "android.car",
+ ],
+
+ optimize: {
+ enabled: false,
+ },
+
+ static_libs: [
+ "androidx.test.runner",
+ "androidx.test.ext.junit",
+ "mockito-target-extended",
+ "mockito-kotlin2",
+ "truth",
+ "CarLauncherCommon",
+ "flag-junit",
+ ],
+
+ manifest: "AndroidManifest.xml",
+
+ instrumentation_for: "CarLauncherCommon",
+
+ dex_preopt: {
+ enabled: false,
+ },
+
+ jni_libs: [
+ // For mockito extended
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ ],
+
+ test_suites: [
+ "automotive-tests",
+ ],
+}
diff --git a/libs/car-launcher-common/tests/AndroidManifest.xml b/libs/car-launcher-common/tests/AndroidManifest.xml
new file mode 100644
index 0000000..a1149df
--- /dev/null
+++ b/libs/car-launcher-common/tests/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.carlaunchercommon.test">
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner"/>
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:label="Tests for Car Launcher common utility lib"
+ android:targetPackage="com.android.car.carlaunchercommon.test" />
+</manifest>
diff --git a/libs/car-launcher-common/tests/src/com/android/car/carlaunchercommon/shortcuts/ForceStopShortcutItemTest.kt b/libs/car-launcher-common/tests/src/com/android/car/carlaunchercommon/shortcuts/ForceStopShortcutItemTest.kt
new file mode 100644
index 0000000..7365314
--- /dev/null
+++ b/libs/car-launcher-common/tests/src/com/android/car/carlaunchercommon/shortcuts/ForceStopShortcutItemTest.kt
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2023 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.carlaunchercommon.shortcuts
+
+import android.app.ActivityManager
+import android.app.AlertDialog
+import android.car.media.CarMediaManager
+import android.content.ComponentName
+import android.content.Context
+import android.content.DialogInterface
+import android.content.res.Resources
+import android.widget.Toast
+import com.android.car.ui.AlertDialogBuilder
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.ArgumentMatchers.nullable
+import org.mockito.Mockito
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+class ForceStopShortcutItemTest {
+ private val resourcesMock = mock<Resources> {
+ on { getString(anyInt(), anyString()) } doReturn "testResourceString"
+ }
+ private val activityManagerMock = mock<ActivityManager> {}
+ private val contextMock = mock<Context> {
+ on { resources } doReturn resourcesMock
+ on { getSystemService(eq(ActivityManager::class.java)) } doReturn activityManagerMock
+ }
+ private val toastMock = mock<Toast> {}
+ private val carMediaManagerMock = mock<CarMediaManager> {}
+ private val alertDialogMock = mock<AlertDialog> {}
+ private val alertDialogBuilderMock = mock<AlertDialogBuilder> {
+ on { setTitle(anyInt()) } doReturn it
+ on { setMessage(anyInt()) } doReturn it
+ on { setPositiveButton(anyInt(), any<DialogInterface.OnClickListener>()) } doReturn it
+ on {
+ setNegativeButton(anyInt(), nullable(DialogInterface.OnClickListener::class.java))
+ } doReturn it
+ on { create() } doReturn alertDialogMock
+ }
+
+ @Test
+ fun forceStop_nonMediaApp_forceStopPackageCalled() {
+ val packageName = "com.example.app"
+
+ createForceStopShortcutItem(
+ contextMock,
+ packageName,
+ carMediaManagerMock,
+ mediaServiceComponents = setOf()
+ ).forceStop(packageName, displayName = "testDisplayName")
+
+ verify(activityManagerMock).forceStopPackage(eq(packageName))
+ }
+
+ @Test
+ fun forceStop_nonMediaApp_shouldNotChangeMediaSource() {
+ createForceStopShortcutItem(
+ contextMock,
+ packageName = "com.example.app",
+ carMediaManagerMock,
+ mediaServiceComponents = setOf()
+ ).forceStop(packageName = "com.example.app", displayName = "testDisplayName")
+
+ Mockito.verify(carMediaManagerMock, Mockito.never())
+ .setMediaSource(any<ComponentName>(), anyInt())
+ }
+
+ @Test
+ fun forceStop_activeBrowseMediaSrc_foundLastBrowseSrc_forceStopPackageCalled() {
+ val forceStoppedComponent =
+ ComponentName("forceStoppedPkg", "testClassName")
+ val lastBrowsedMediaComponent =
+ ComponentName("lastBrowsedMediaPkg", "testClassName")
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE)))
+ .doReturn(forceStoppedComponent)
+ whenever(
+ carMediaManagerMock.getLastMediaSources(eq(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE))
+ ).doReturn(listOf(forceStoppedComponent, lastBrowsedMediaComponent))
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK)))
+ .doReturn(ComponentName("otherPkg", "testClassName"))
+
+ createForceStopShortcutItem(
+ contextMock,
+ forceStoppedComponent.packageName,
+ carMediaManagerMock,
+ mediaServiceComponents = setOf()
+ ).forceStop(forceStoppedComponent.packageName, displayName = "testDisplayName")
+
+ Mockito.verify(activityManagerMock).forceStopPackage(forceStoppedComponent.packageName)
+ }
+
+ @Test
+ fun forceStop_activeBrowseMediaSrc_foundLastBrowseSrc_browseMediaSrcSet() {
+ val forceStoppedComponent =
+ ComponentName("forceStoppedPkg", "testClassName")
+ val lastBrowsedMediaComponent =
+ ComponentName("lastBrowsedMediaPkg", "testClassName")
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE)))
+ .doReturn(forceStoppedComponent)
+ whenever(
+ carMediaManagerMock.getLastMediaSources(eq(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE))
+ ).doReturn(listOf(forceStoppedComponent, lastBrowsedMediaComponent))
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK)))
+ .doReturn(ComponentName("otherPkg", "testClassName"))
+
+ createForceStopShortcutItem(
+ contextMock,
+ forceStoppedComponent.packageName,
+ carMediaManagerMock,
+ mediaServiceComponents = setOf()
+ ).forceStop(forceStoppedComponent.packageName, displayName = "testDisplayName")
+
+ Mockito.verify(carMediaManagerMock).setMediaSource(
+ lastBrowsedMediaComponent,
+ CarMediaManager.MEDIA_SOURCE_MODE_BROWSE
+ )
+ }
+
+ @Test
+ fun forceStop_activeBrowseMediaSrc_foundLastBrowseSrc_playbackMediaSrcNotSet() {
+ val forceStoppedComponent =
+ ComponentName("forceStoppedPkg", "testClassName")
+ val lastBrowsedMediaComponent =
+ ComponentName("lastBrowsedMediaPkg", "testClassName")
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE)))
+ .doReturn(forceStoppedComponent)
+ whenever(
+ carMediaManagerMock.getLastMediaSources(eq(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE))
+ ).doReturn(listOf(forceStoppedComponent, lastBrowsedMediaComponent))
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK)))
+ .doReturn(ComponentName("otherPkg", "testClassName"))
+
+ createForceStopShortcutItem(
+ contextMock,
+ forceStoppedComponent.packageName,
+ carMediaManagerMock,
+ mediaServiceComponents = setOf()
+ ).forceStop(forceStoppedComponent.packageName, displayName = "testDisplayName")
+
+ Mockito.verify(carMediaManagerMock, never()).setMediaSource(
+ any<ComponentName>(),
+ eq(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK)
+ )
+ }
+
+ @Test
+ fun forceStop_activeBrowseMediaSrc_noLastBrowseSrc_browseMediaSrcSet() {
+ val forceStoppedComponent =
+ ComponentName("forceStoppedPkg", "testClassName")
+ val mediaComponent = ComponentName("mediaComponent", "testClassName")
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE)))
+ .doReturn(forceStoppedComponent)
+ whenever(
+ carMediaManagerMock.getLastMediaSources(eq(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE))
+ ).doReturn(listOf(forceStoppedComponent))
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK)))
+ .doReturn(ComponentName("otherPkg", "testClassName"))
+
+ createForceStopShortcutItem(
+ contextMock,
+ forceStoppedComponent.packageName,
+ carMediaManagerMock,
+ mediaServiceComponents = setOf(forceStoppedComponent, mediaComponent)
+ ).forceStop(forceStoppedComponent.packageName, displayName = "testDisplayName")
+
+ Mockito.verify(carMediaManagerMock).setMediaSource(
+ mediaComponent,
+ CarMediaManager.MEDIA_SOURCE_MODE_BROWSE
+ )
+ }
+
+ @Test
+ fun forceStop_activeBrowseMediaSrc_noLastBrowseSrc_noMediaService_browseMediaSrcNotSet() {
+ val forceStoppedComponent =
+ ComponentName("forceStoppedPkg", "testClassName")
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE)))
+ .doReturn(forceStoppedComponent)
+ whenever(
+ carMediaManagerMock.getLastMediaSources(eq(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE))
+ ).doReturn(listOf(forceStoppedComponent))
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK)))
+ .doReturn(ComponentName("otherPkg", "testClassName"))
+
+ createForceStopShortcutItem(
+ contextMock,
+ forceStoppedComponent.packageName,
+ carMediaManagerMock,
+ mediaServiceComponents = setOf()
+ ).forceStop(forceStoppedComponent.packageName, displayName = "testDisplayName")
+
+ Mockito.verify(carMediaManagerMock, Mockito.never()).setMediaSource(
+ any<ComponentName>(),
+ anyInt()
+ )
+ }
+
+ @Test
+ fun forceStop_activePlaybackMediaSrc_lastPlaybackSrcFound_forceStopPackageCalled() {
+ val forceStoppedComponent =
+ ComponentName("forceStoppedPkg", "testClassName")
+ val lastPlaybackMediaComponent =
+ ComponentName("lastPlaybackMediaPkg", "testClassName")
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK)))
+ .doReturn(forceStoppedComponent)
+ whenever(
+ carMediaManagerMock.getLastMediaSources(eq(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK))
+ ).doReturn(listOf(forceStoppedComponent, lastPlaybackMediaComponent))
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE)))
+ .doReturn(ComponentName("otherPkg", "testClassName"))
+
+ createForceStopShortcutItem(
+ contextMock,
+ forceStoppedComponent.packageName,
+ carMediaManagerMock,
+ mediaServiceComponents = setOf()
+ ).forceStop(forceStoppedComponent.packageName, displayName = "testDisplayName")
+
+ Mockito.verify(activityManagerMock).forceStopPackage(forceStoppedComponent.packageName)
+ }
+
+ @Test
+ fun forceStop_activePlaybackMediaSrc_lastPlaybackSrcFound_playbackMediaSrcSet() {
+ val forceStoppedComponent =
+ ComponentName("forceStoppedPkg", "testClassName")
+ val lastPlaybackMediaComponent =
+ ComponentName("lastPlaybackMediaPkg", "testClassName")
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK)))
+ .doReturn(forceStoppedComponent)
+ whenever(
+ carMediaManagerMock.getLastMediaSources(eq(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK))
+ ).doReturn(listOf(forceStoppedComponent, lastPlaybackMediaComponent))
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE)))
+ .doReturn(ComponentName("otherPkg", "testClassName"))
+
+ createForceStopShortcutItem(
+ contextMock,
+ forceStoppedComponent.packageName,
+ carMediaManagerMock,
+ mediaServiceComponents = setOf()
+ ).forceStop(forceStoppedComponent.packageName, displayName = "testDisplayName")
+
+ Mockito.verify(carMediaManagerMock).setMediaSource(
+ lastPlaybackMediaComponent,
+ CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK
+ )
+ }
+
+ @Test
+ fun forceStop_activePlaybackMediaSrc_lastPlaybackSrcFound_browseMediaSrcNotSet() {
+ val forceStoppedComponent =
+ ComponentName("forceStoppedPkg", "testClassName")
+ val lastPlaybackMediaComponent =
+ ComponentName("lastPlaybackMediaPkg", "testClassName")
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK)))
+ .doReturn(forceStoppedComponent)
+ whenever(
+ carMediaManagerMock.getLastMediaSources(eq(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK))
+ ).doReturn(listOf(forceStoppedComponent, lastPlaybackMediaComponent))
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE)))
+ .doReturn(ComponentName("otherPkg", "testClassName"))
+
+ createForceStopShortcutItem(
+ contextMock,
+ forceStoppedComponent.packageName,
+ carMediaManagerMock,
+ mediaServiceComponents = setOf()
+ ).forceStop(forceStoppedComponent.packageName, displayName = "testDisplayName")
+
+ Mockito.verify(carMediaManagerMock, never()).setMediaSource(
+ any<ComponentName>(),
+ eq(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE)
+ )
+ }
+
+ @Test
+ fun forceStop_activePlaybackMediaSrc_noLastPlaybackSrc_playbackMediaSrcSet() {
+ val forceStoppedComponent =
+ ComponentName("forceStoppedPkg", "testClassName")
+ val mediaComponent = ComponentName("mediaComponent", "testClassName")
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK)))
+ .doReturn(forceStoppedComponent)
+ whenever(
+ carMediaManagerMock.getLastMediaSources(eq(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK))
+ ).doReturn(listOf(forceStoppedComponent))
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE)))
+ .doReturn(ComponentName("otherPkg", "testClassName"))
+
+ createForceStopShortcutItem(
+ contextMock,
+ forceStoppedComponent.packageName,
+ carMediaManagerMock,
+ mediaServiceComponents = setOf(forceStoppedComponent, mediaComponent)
+ ).forceStop(forceStoppedComponent.packageName, displayName = "testDisplayName")
+
+ Mockito.verify(carMediaManagerMock).setMediaSource(
+ mediaComponent,
+ CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK
+ )
+ }
+
+ @Test
+ fun forceStop_activePlaybackMediaSrc_noLastPlaybackSrc_noMediaService_playbackMediaSrcNotSet() {
+ val forceStoppedComponent =
+ ComponentName("forceStoppedPkg", "testClassName")
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK)))
+ .doReturn(forceStoppedComponent)
+ whenever(
+ carMediaManagerMock.getLastMediaSources(eq(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK))
+ ).doReturn(listOf(forceStoppedComponent))
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE)))
+ .doReturn(ComponentName("otherPkg", "testClassName"))
+
+ createForceStopShortcutItem(
+ contextMock,
+ forceStoppedComponent.packageName,
+ carMediaManagerMock,
+ mediaServiceComponents = setOf()
+ ).forceStop(forceStoppedComponent.packageName, displayName = "testDisplayName")
+
+ Mockito.verify(carMediaManagerMock, Mockito.never()).setMediaSource(
+ any<ComponentName>(),
+ anyInt()
+ )
+ }
+
+ @Test
+ fun forceStop_activeBrowseAndPlaybackMediaSrc_foundLastSrcs_browseAndPlaybackMediaSrcSet() {
+ val forceStoppedComponent =
+ ComponentName("forceStoppedPkg", "testClassName")
+ val lastPlaybackMediaComponent =
+ ComponentName("lastPlaybackMediaPkg", "testClassName")
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE)))
+ .doReturn(forceStoppedComponent)
+ whenever(
+ carMediaManagerMock.getLastMediaSources(eq(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE))
+ ).doReturn(listOf(forceStoppedComponent, lastPlaybackMediaComponent))
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK)))
+ .doReturn(forceStoppedComponent)
+ whenever(
+ carMediaManagerMock.getLastMediaSources(eq(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK))
+ ).doReturn(listOf(forceStoppedComponent, lastPlaybackMediaComponent))
+
+ createForceStopShortcutItem(
+ contextMock,
+ forceStoppedComponent.packageName,
+ carMediaManagerMock,
+ mediaServiceComponents = setOf()
+ ).forceStop(forceStoppedComponent.packageName, displayName = "testDisplayName")
+
+ Mockito.verify(carMediaManagerMock).setMediaSource(
+ lastPlaybackMediaComponent,
+ CarMediaManager.MEDIA_SOURCE_MODE_BROWSE
+ )
+ Mockito.verify(carMediaManagerMock).setMediaSource(
+ lastPlaybackMediaComponent,
+ CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK
+ )
+ }
+
+ @Test
+ fun forceStop_notActiveMediaBrowseNorPlaybackSource_forceStopPackageCalled() {
+ val forceStoppedPackageName = "forceStoppedPackageName"
+ val activeMediaPackageName = "activeMediaPackageName"
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE)))
+ .doReturn(ComponentName(activeMediaPackageName, "testClassName"))
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK)))
+ .doReturn(ComponentName(activeMediaPackageName, "testClassName"))
+
+ createForceStopShortcutItem(
+ contextMock,
+ forceStoppedPackageName,
+ carMediaManagerMock,
+ mediaServiceComponents = setOf()
+ ).forceStop(forceStoppedPackageName, displayName = "testDisplayName")
+
+ Mockito.verify(activityManagerMock).forceStopPackage(forceStoppedPackageName)
+ }
+
+ @Test
+ fun forceStop_notActiveMediaBrowseNorPlaybackSource_doesNotChangeMediaSource() {
+ val forceStoppedPackageName = "forceStoppedPackageName"
+ val activeMediaPackageName = "activeMediaPackageName"
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_BROWSE)))
+ .doReturn(ComponentName(activeMediaPackageName, "testClassName"))
+ whenever(carMediaManagerMock.getMediaSource(eq(CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK)))
+ .doReturn(ComponentName(activeMediaPackageName, "testClassName"))
+
+ createForceStopShortcutItem(
+ contextMock,
+ forceStoppedPackageName,
+ carMediaManagerMock,
+ mediaServiceComponents = setOf()
+ ).forceStop(forceStoppedPackageName, displayName = "testDisplayName")
+
+ Mockito.verify(carMediaManagerMock, Mockito.never()).setMediaSource(
+ any<ComponentName>(),
+ anyInt()
+ )
+ }
+
+ private fun createForceStopShortcutItem(
+ context: Context,
+ packageName: String,
+ carMediaManager: CarMediaManager?,
+ mediaServiceComponents: Set<ComponentName>
+
+ ): ForceStopShortcutItem {
+ return object : ForceStopShortcutItem(
+ context,
+ packageName,
+ displayName = "testDisplayName",
+ carMediaManager,
+ mediaServiceComponents
+ ) {
+ override fun getAlertDialogBuilder(context: Context) = alertDialogBuilderMock
+ override fun createToast(context: Context, text: CharSequence, duration: Int) =
+ toastMock
+ }
+ }
+}
diff --git a/docklib-util/tests/src/com/android/car/dockutil/shortcuts/PinShortcutItemTest.kt b/libs/car-launcher-common/tests/src/com/android/car/carlaunchercommon/shortcuts/PinShortcutItemTest.kt
similarity index 62%
rename from docklib-util/tests/src/com/android/car/dockutil/shortcuts/PinShortcutItemTest.kt
rename to libs/car-launcher-common/tests/src/com/android/car/carlaunchercommon/shortcuts/PinShortcutItemTest.kt
index d3498ae..167e3df 100644
--- a/docklib-util/tests/src/com/android/car/dockutil/shortcuts/PinShortcutItemTest.kt
+++ b/libs/car-launcher-common/tests/src/com/android/car/carlaunchercommon/shortcuts/PinShortcutItemTest.kt
@@ -1,4 +1,20 @@
-package com.android.car.dockutil.shortcuts
+/*
+ * Copyright (C) 2024 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.carlaunchercommon.shortcuts
import android.content.res.Resources
import androidx.test.ext.junit.runners.AndroidJUnit4
diff --git a/libs/hidden-apis-compat/hidden-apis-disabled/build.gradle b/libs/hidden-apis-compat/hidden-apis-disabled/build.gradle
new file mode 100644
index 0000000..9b08470
--- /dev/null
+++ b/libs/hidden-apis-compat/hidden-apis-disabled/build.gradle
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+plugins {
+ id "com.android.library"
+}
+
+android {
+ namespace 'com.android.car.hidden.apis'
+ compileSdk gradle.ext.aaosTargetSDK
+
+ defaultConfig {
+ minSdk gradle.ext.aaosLatestSDK
+ targetSdk gradle.ext.aaosLatestSDK
+ versionCode gradle.ext.getVersionCode()
+ versionName gradle.ext.getVersionName()
+ }
+
+ sourceSets {
+ main {
+ java.srcDirs = ['src']
+ }
+ }
+
+ dependencies {
+ implementation 'junit:junit:4.13.2'
+ }
+}
diff --git a/libs/hidden-apis-compat/hidden-apis-disabled/src/android/platform/test/flag/junit/SetFlagsRule.java b/libs/hidden-apis-compat/hidden-apis-disabled/src/android/platform/test/flag/junit/SetFlagsRule.java
new file mode 100644
index 0000000..58c8b68
--- /dev/null
+++ b/libs/hidden-apis-compat/hidden-apis-disabled/src/android/platform/test/flag/junit/SetFlagsRule.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 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 android.platform.test.flag.junit;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * Fake implementation of SetFlagRule for gradle builds
+ */
+public class SetFlagsRule implements TestRule {
+
+ /**
+ * Do nothing function. To enable the feature use cmd before running the tests.
+ * <p>
+ * Called for gradle builds only
+ */
+ public void enableFlags(String featureFlag) {
+ System.out.println(
+ "Gradle builds: Ensure that flagDockFeature is enabled before running the tests, "
+ + "otherwise the tests might fail prematurely");
+ }
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return base;
+ }
+
+}
diff --git a/libs/appgrid/lib/hidden_apis_disabled/src/com/android/car/carlauncher/hidden/HiddenApiAccess.java b/libs/hidden-apis-compat/hidden-apis-disabled/src/com/android/car/hidden/apis/HiddenApiAccess.java
similarity index 71%
rename from libs/appgrid/lib/hidden_apis_disabled/src/com/android/car/carlauncher/hidden/HiddenApiAccess.java
rename to libs/hidden-apis-compat/hidden-apis-disabled/src/com/android/car/hidden/apis/HiddenApiAccess.java
index b3fbb7d..1ea7abb 100644
--- a/libs/appgrid/lib/hidden_apis_disabled/src/com/android/car/carlauncher/hidden/HiddenApiAccess.java
+++ b/libs/hidden-apis-compat/hidden-apis-disabled/src/com/android/car/hidden/apis/HiddenApiAccess.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -14,14 +14,18 @@
* limitations under the License.
*/
-package com.android.car.carlauncher.hidden;
+package com.android.car.hidden.apis;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
import android.os.UserHandle;
import android.os.UserManager;
+import android.view.Display;
import android.view.DragEvent;
import android.view.SurfaceControl;
-/***
+/**
* This classes is a place to surface all hidden/blocked apis which are not available to be accessed
* by unbundled application.
* All of the apis here are only visible to platform apps.
@@ -29,10 +33,18 @@
* Note: This folder location is compiled only with Gradle builds. There exists alternative Soong
* build version under (hidden_apis_enabled) folder.
* Gradle version fails softly when we need this api, so some functionality might appear broken.
- *
+ * <p>
* Gradle builds are testing/development only. Please build with Soong once before merging changes
*/
public class HiddenApiAccess {
+
+ /**
+ * @return true: For gradle builds can always consider as debuggable
+ */
+ public static boolean isDebuggable() {
+ return true;
+ }
+
/**
* Empty/Null SurfaceControl
*/
@@ -54,4 +66,18 @@
* android.view.View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION
*/
public static int DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION = 1 << 11;
+
+ /**
+ * Always returns {@link Display.DEFAULT_DISPLAY}
+ */
+ public static int getDisplayId(ActivityManager.RunningTaskInfo taskInfo) {
+ return Display.DEFAULT_DISPLAY;
+ }
+
+ /**
+ * Start Activity with the current user
+ */
+ public static void startActivityAsUser(Context context, Intent intent, UserHandle userHandle) {
+ context.startActivity(intent);
+ }
}
diff --git a/libs/hidden-apis-compat/hidden-apis-enabled/Android.bp b/libs/hidden-apis-compat/hidden-apis-enabled/Android.bp
new file mode 100644
index 0000000..866f773
--- /dev/null
+++ b/libs/hidden-apis-compat/hidden-apis-enabled/Android.bp
@@ -0,0 +1,22 @@
+//
+// Copyright (C) 2024 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.
+//
+
+filegroup {
+ name: "hidden_api_enabled_srcs",
+ srcs: [
+ "src/**/*.java",
+ ],
+}
diff --git a/libs/appgrid/lib/hidden_apis_enabled/src/com/android/car/carlauncher/hidden/HiddenApiAccess.java b/libs/hidden-apis-compat/hidden-apis-enabled/src/com/android/car/hidden/apis/HiddenApiAccess.java
similarity index 68%
rename from libs/appgrid/lib/hidden_apis_enabled/src/com/android/car/carlauncher/hidden/HiddenApiAccess.java
rename to libs/hidden-apis-compat/hidden-apis-enabled/src/com/android/car/hidden/apis/HiddenApiAccess.java
index b36f057..21d985b 100644
--- a/libs/appgrid/lib/hidden_apis_enabled/src/com/android/car/carlauncher/hidden/HiddenApiAccess.java
+++ b/libs/hidden-apis-compat/hidden-apis-enabled/src/com/android/car/hidden/apis/HiddenApiAccess.java
@@ -14,8 +14,13 @@
* limitations under the License.
*/
-package com.android.car.carlauncher.hidden;
+package com.android.car.hidden.apis;
+import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
import android.os.UserHandle;
import android.os.UserManager;
import android.view.DragEvent;
@@ -33,6 +38,13 @@
public class HiddenApiAccess {
/**
+ * @return true: For gradle builds can always consider as debuggable
+ */
+ public static boolean isDebuggable() {
+ return Build.isDebuggable();
+ }
+
+ /**
* Calls hidden api {@link android.view.DragEvent#getDragSurface}
*/
public static SurfaceControl getDragSurface(DragEvent event) {
@@ -42,6 +54,7 @@
/**
* Calls hidden api {@link android.os.UserManager#hasBaseUserRestriction}
*/
+ @SuppressLint("MissingPermission")
public static boolean hasBaseUserRestriction(UserManager userManager, String restriction,
UserHandle user) {
return userManager.hasBaseUserRestriction(restriction, user);
@@ -52,4 +65,20 @@
*/
public static int DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION =
View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION;
+
+
+ /**
+ * Calls the hidden api
+ */
+ public static int getDisplayId(ActivityManager.RunningTaskInfo taskInfo) {
+ return taskInfo.getDisplayId();
+ }
+
+ /**
+ * Start Activity with the current user
+ */
+ @SuppressLint("MissingPermission")
+ public static void startActivityAsUser(Context context, Intent intent, UserHandle userHandle) {
+ context.startActivityAsUser(intent, userHandle);
+ }
}
diff --git a/settings.gradle b/settings.gradle
index 195d14d..11fa3ff 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -34,3 +34,7 @@
include ':libs:car-apps-common'
include ':libs:car-media-common'
include ':libs:car-ui-lib'
+include ':libs:aconfig-platform-compat'
+include ':libs:hidden-apis-compat:hidden-apis-disabled'
+include ':libs:car-launcher-common'
+include ':docklib-util'