Merge "Update existing ECM settings to use new infrastructure" into main
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 2a45624..19927a2 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -138,6 +138,7 @@
<uses-permission android:name="android.permission.CUSTOMIZE_SYSTEM_UI" />
<uses-permission android:name="android.permission.REMAP_MODIFIER_KEYS" />
<uses-permission android:name="android.permission.ACCESS_GPU_SERVICE" />
+ <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:name=".SettingsApplication"
@@ -491,6 +492,17 @@
android:value="@string/menu_key_display"/>
</activity>
+ <activity android:name=".Settings$ScreenTimeoutActivity"
+ android:label="@string/screen_timeout"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.settings.SCREEN_TIMEOUT_SETTINGS" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
+ android:value="com.android.settings.display.ScreenTimeoutSettings"/>
+ </activity>
+
<activity
android:name="Settings$ConfigureWifiSettingsActivity"
android:label="@string/wifi_configure_settings_preference_title"
@@ -4200,8 +4212,23 @@
android:value="TogglePermissionAppList/MediaRoutingControl"/>
<meta-data android:name="com.android.settings.HIGHLIGHT_MENU_KEY"
android:value="@string/menu_key_apps"/>
- <meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"
- android:value="true" />
+ </activity-alias>
+
+ <activity-alias
+ android:name="AppMediaRoutingControlActivity"
+ android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
+ android:exported="true"
+ android:targetActivity=".spa.SpaAppBridgeActivity"
+ android:label="@string/media_routing_control_title">
+ <intent-filter android:priority="1">
+ <action android:name="android.settings.REQUEST_MEDIA_ROUTING_CONTROL" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="package" />
+ </intent-filter>
+ <meta-data android:name="com.android.settings.spa.DESTINATION"
+ android:value="TogglePermissionAppInfoPage/MediaRoutingControl"/>
+ <meta-data android:name="com.android.settings.HIGHLIGHT_MENU_KEY"
+ android:value="@string/menu_key_apps"/>
</activity-alias>
<!-- Keep compatibility with old WebView-picker implementation -->
diff --git a/res/drawable/audio_sharing_guidance.png b/res/drawable/audio_sharing_guidance.png
new file mode 100644
index 0000000..c0ab637
--- /dev/null
+++ b/res/drawable/audio_sharing_guidance.png
Binary files differ
diff --git a/res/drawable/ic_calls_sms.xml b/res/drawable/ic_calls_sms.xml
new file mode 100644
index 0000000..2033e8f
--- /dev/null
+++ b/res/drawable/ic_calls_sms.xml
@@ -0,0 +1,29 @@
+<!--
+ Copyright (C) 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?android:attr/colorControlNormal"
+ >
+
+ <path
+ android:pathData="M 0 0 H 24 V 24 H 0 V 0 Z" />
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M20.17,14.85l-3.26-0.65c-0.33-0.07-0.67,0.04-0.9,0.27l-2.62,2.62c-2.75-1.49-5.01-3.75-6.5-6.5l2.62-2.62 c0.24-0.24,0.34-0.58,0.27-0.9L9.13,3.82c-0.09-0.47-0.5-0.8-0.98-0.8H4c-0.56,0-1.03,0.47-1,1.03c0.17,2.91,1.04,5.63,2.43,8.01 c1.57,2.69,3.81,4.93,6.5,6.5c2.38,1.39,5.1,2.26,8.01,2.43c0.56,0.03,1.03-0.44,1.03-1v-4.15C20.97,15.36,20.64,14.95,20.17,14.85 L20.17,14.85z M12,3v10l3-3h6V3H12z M19,8h-5V5h5V8z" />
+</vector>
diff --git a/res/layout/audio_sharing_device_item.xml b/res/layout/audio_sharing_device_item.xml
index f8e7454..04ecdd7 100644
--- a/res/layout/audio_sharing_device_item.xml
+++ b/res/layout/audio_sharing_device_item.xml
@@ -17,16 +17,17 @@
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
android:layout_width="match_parent"
- android:layout_height="wrap_content">
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
<Button
android:id="@+id/device_button"
- android:overScrollMode="never"
+ style="@style/SettingsLibActionButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:textAlignment="center"
- android:text=""/>
+ android:layout_marginTop="4dp"
+ android:background="@drawable/audio_sharing_rounded_bg_ripple"
+ android:textAlignment="center" />
</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/dialog_audio_sharing.xml b/res/layout/dialog_audio_sharing.xml
index 3b11020..aace5ab 100644
--- a/res/layout/dialog_audio_sharing.xml
+++ b/res/layout/dialog_audio_sharing.xml
@@ -19,38 +19,64 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:padding="24dp"
- android:orientation="vertical">
+ android:orientation="vertical"
+ android:paddingHorizontal="?android:dialogPreferredPadding"
+ android:paddingBottom="?android:dialogPreferredPadding">
<TextView
- style="@style/DeviceAudioSharingText"
android:id="@+id/share_audio_subtitle1"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textAlignment="center"
- android:layout_gravity="center"/>
-
- <TextView
style="@style/DeviceAudioSharingText"
- android:id="@+id/share_audio_subtitle2"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textAlignment="center"
- android:layout_gravity="center"/>
-
- <com.android.internal.widget.RecyclerView
- android:visibility="visible"
- android:id="@+id/btn_list"
- android:nestedScrollingEnabled="false"
- android:overScrollMode="never"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="center"/>
-
- <Button
- android:id="@+id/cancel_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
- android:text="@string/cancel"/>
+ android:paddingBottom="14dp"
+ android:textFontWeight="500"
+ android:visibility="gone" />
+
+ <TextView
+ android:id="@+id/share_audio_subtitle2"
+ style="@style/DeviceAudioSharingText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:paddingBottom="24dp"
+ android:textFontWeight="400" />
+
+ <ImageView
+ android:id="@+id/share_audio_guidance"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:src="@drawable/audio_sharing_guidance"
+ android:visibility="gone" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/btn_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:nestedScrollingEnabled="false"
+ android:overScrollMode="never"
+ android:visibility="gone" />
+
+ <Button
+ android:id="@+id/share_btn"
+ style="@style/SettingsLibActionButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginTop="4dp"
+ android:background="@drawable/audio_sharing_rounded_bg_ripple"
+ android:visibility="gone" />
+
+ <Button
+ android:id="@+id/cancel_btn"
+ style="@style/SettingsLibActionButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginTop="4dp"
+ android:background="@drawable/audio_sharing_rounded_bg_ripple"
+ android:text="Not now"
+ android:visibility="gone" />
</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/dialog_audio_sharing_disconnect.xml b/res/layout/dialog_audio_sharing_disconnect.xml
index 09bac40..592b41b 100644
--- a/res/layout/dialog_audio_sharing_disconnect.xml
+++ b/res/layout/dialog_audio_sharing_disconnect.xml
@@ -19,29 +19,33 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:padding="24dp"
- android:orientation="vertical">
+ android:orientation="vertical"
+ android:paddingHorizontal="?android:dialogPreferredPadding"
+ android:paddingBottom="?android:dialogPreferredPadding">
<TextView
android:id="@+id/share_audio_disconnect_description"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textAlignment="center"
- android:layout_gravity="center"/>
-
- <com.android.internal.widget.RecyclerView
- android:visibility="visible"
- android:id="@+id/device_btn_list"
- android:nestedScrollingEnabled="false"
- android:overScrollMode="never"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="center"/>
-
- <Button
- android:id="@+id/cancel_btn"
+ style="@style/DeviceAudioSharingText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
- android:text="@string/cancel"/>
+ android:paddingBottom="24dp" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/device_btn_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:nestedScrollingEnabled="false"
+ android:overScrollMode="never" />
+
+ <Button
+ android:id="@+id/cancel_btn"
+ style="@style/SettingsLibActionButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginTop="4dp"
+ android:background="@drawable/audio_sharing_rounded_bg_ripple"
+ android:text="@string/cancel" />
</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/dialog_audio_sharing_join.xml b/res/layout/dialog_audio_sharing_join.xml
index 42d964a..4bdab7f 100644
--- a/res/layout/dialog_audio_sharing_join.xml
+++ b/res/layout/dialog_audio_sharing_join.xml
@@ -19,35 +19,44 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:padding="24dp"
- android:orientation="vertical">
+ android:orientation="vertical"
+ android:paddingHorizontal="?android:dialogPreferredPadding"
+ android:paddingBottom="?android:dialogPreferredPadding">
<TextView
android:id="@+id/share_audio_subtitle1"
+ style="@style/DeviceAudioSharingText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:textAlignment="center"
- android:layout_gravity="center"/>
+ android:layout_gravity="center"
+ android:paddingBottom="14dp"
+ android:textFontWeight="500" />
<TextView
android:id="@+id/share_audio_subtitle2"
+ style="@style/DeviceAudioSharingText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:textAlignment="center"
- android:layout_gravity="center"/>
+ android:layout_gravity="center"
+ android:paddingBottom="24dp"
+ android:textFontWeight="400" />
<Button
android:id="@+id/share_btn"
+ style="@style/SettingsLibActionButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
- android:text=""/>
+ android:layout_marginTop="4dp"
+ android:background="@drawable/audio_sharing_rounded_bg_ripple" />
<Button
android:id="@+id/cancel_btn"
+ style="@style/SettingsLibActionButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
- android:text="@string/cancel"/>
-
+ android:layout_marginTop="4dp"
+ android:background="@drawable/audio_sharing_rounded_bg_ripple"
+ android:text="Not now" />
</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/privatespace_account_login_error.xml b/res/layout/privatespace_account_login_error.xml
index a38dd50..beae22f 100644
--- a/res/layout/privatespace_account_login_error.xml
+++ b/res/layout/privatespace_account_login_error.xml
@@ -22,7 +22,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:filterTouchesWhenObscured="true"
- app:sucHeaderText="@string/privatespace_retry_signin_title"
- app:sudDescriptionText="@string/privatespace_retry_summary"
+ app:sucHeaderText="@string/private_space_retry_signin_title"
+ app:sudDescriptionText="@string/private_space_retry_summary"
android:icon="@drawable/ic_error_red">
</com.google.android.setupdesign.GlifLayout>
diff --git a/res/layout/privatespace_advancing_screen.xml b/res/layout/privatespace_advancing_screen.xml
index 5b69593..3a85b16 100644
--- a/res/layout/privatespace_advancing_screen.xml
+++ b/res/layout/privatespace_advancing_screen.xml
@@ -17,7 +17,7 @@
<com.google.android.setupdesign.GlifLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/privatesapce_autoadvance_screen"
+ android:id="@+id/private_space_autoadvance_screen"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:icon="@drawable/ic_privatespace_icon">
@@ -39,7 +39,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
- android:text="@string/privatespace_setting_up_text"
+ android:text="@string/private_space_setting_up_text"
android:layout_marginBottom="24dp"/>
</LinearLayout>
diff --git a/res/layout/privatespace_creation_error.xml b/res/layout/privatespace_creation_error.xml
index af11f3a..0ec0cb6 100644
--- a/res/layout/privatespace_creation_error.xml
+++ b/res/layout/privatespace_creation_error.xml
@@ -17,11 +17,10 @@
<com.google.android.setupdesign.GlifLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/privatespace_setup_error"
+ android:id="@+id/private_space_setup_error"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:filterTouchesWhenObscured="true"
- app:sucHeaderText="@string/privatespace_error_screen_title"
- app:sudDescriptionText="@string/privatespace_error_screen_summary"
+ app:sucHeaderText="@string/private_space_error_screen_title"
android:icon="@drawable/ic_warning_circle_red">
</com.google.android.setupdesign.GlifLayout>
diff --git a/res/layout/privatespace_education_screen.xml b/res/layout/privatespace_education_screen.xml
index adb65c9..350e780 100644
--- a/res/layout/privatespace_education_screen.xml
+++ b/res/layout/privatespace_education_screen.xml
@@ -21,8 +21,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:filterTouchesWhenObscured="true"
- app:sucHeaderText="@string/privatespace_setup_title"
- app:sudDescriptionText="@string/privatespace_hide_apps_summary"
+ app:sucHeaderText="@string/private_space_setup_title"
+ app:sudDescriptionText="@string/private_space_hide_apps_summary"
android:icon="@drawable/ic_privatespace_icon">
<ScrollView
android:id="@+id/main_clear_scrollview"
@@ -43,7 +43,29 @@
android:src="@drawable/privatespace_placeholder_image"/>
<TextView
style="@style/PrivateSpaceSetupSubHeaderStyle"
- android:text="@string/privatespace_how_title"/>
+ android:text="@string/private_space_how_title"/>
+ <RelativeLayout
+ style="@style/PrivateSpaceSetupBulletPointLayoutStyle">
+ <ImageView
+ android:id="@+id/lockIcon"
+ style="@style/PrivateSpaceBulletPointIconStyle"
+ android:src="@drawable/ic_lock_closed" />
+ <TextView
+ style="@style/PrivateSpaceBulletPointTextFontStyle"
+ android:layout_toRightOf="@+id/lockIcon"
+ android:text="@string/private_space_protected_lock_text"/>
+ </RelativeLayout>
+ <RelativeLayout
+ style="@style/PrivateSpaceSetupBulletPointLayoutStyle">
+ <ImageView
+ android:id="@+id/bellIcon"
+ style="@style/PrivateSpaceBulletPointIconStyle"
+ android:src="@drawable/ic_notifications" />
+ <TextView
+ style="@style/PrivateSpaceBulletPointTextFontStyle"
+ android:layout_toRightOf="@+id/bellIcon"
+ android:text="@string/private_space_hidden_notifications_text"/>
+ </RelativeLayout>
<RelativeLayout
style="@style/PrivateSpaceSetupBulletPointLayoutStyle"
android:layout_width="fill_parent"
@@ -55,29 +77,7 @@
<TextView
style="@style/PrivateSpaceBulletPointTextFontStyle"
android:layout_toRightOf="@+id/appsIcon"
- android:text="@string/privatespace_access_bottom_text"/>
- </RelativeLayout>
- <RelativeLayout
- style="@style/PrivateSpaceSetupBulletPointLayoutStyle">
- <ImageView
- android:id="@+id/lockIcon"
- style="@style/PrivateSpaceBulletPointIconStyle"
- android:src="@drawable/ic_lock_closed" />
- <TextView
- style="@style/PrivateSpaceBulletPointTextFontStyle"
- android:layout_toRightOf="@+id/lockIcon"
- android:text="@string/privatespace_protected_lock_text"/>
- </RelativeLayout>
- <RelativeLayout
- style="@style/PrivateSpaceSetupBulletPointLayoutStyle">
- <ImageView
- android:id="@+id/bellIcon"
- style="@style/PrivateSpaceBulletPointIconStyle"
- android:src="@drawable/ic_notifications" />
- <TextView
- style="@style/PrivateSpaceBulletPointTextFontStyle"
- android:layout_toRightOf="@+id/bellIcon"
- android:text="@string/privatespace_hidden_notifications_text"/>
+ android:text="@string/private_space_access_bottom_text"/>
</RelativeLayout>
<Space
android:layout_width="wrap_content"
@@ -94,7 +94,7 @@
style="@style/PrivateSpaceBulletPointTextFontStyle"
android:textSize = "14sp"
android:layout_toRightOf="@+id/infoIcon"
- android:text="@string/privatespace_apps_permission_text"/>
+ android:text="@string/private_space_apps_permission_text"/>
</RelativeLayout>
</LinearLayout>
</ScrollView>
diff --git a/res/layout/privatespace_setlock_screen.xml b/res/layout/privatespace_setlock_screen.xml
index 5caf4ae..7211948 100644
--- a/res/layout/privatespace_setlock_screen.xml
+++ b/res/layout/privatespace_setlock_screen.xml
@@ -21,8 +21,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:filterTouchesWhenObscured="true"
- app:sucHeaderText="@string/privatespace_lockscreen_title"
- app:sudDescriptionText="@string/privatespace_lockscreen_summary"
+ app:sucHeaderText="@string/private_space_lockscreen_title"
+ app:sudDescriptionText="@string/private_space_lockscreen_summary"
android:icon="@drawable/ic_lock">
<com.google.android.setupdesign.view.FillContentLayout
style="@style/SudContentFrame"
diff --git a/res/layout/privatespace_setup_success.xml b/res/layout/privatespace_setup_success.xml
index 00b6fec..e3e6b68 100644
--- a/res/layout/privatespace_setup_success.xml
+++ b/res/layout/privatespace_setup_success.xml
@@ -17,11 +17,11 @@
<com.google.android.setupdesign.GlifLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/privatespace_setup_success"
+ android:id="@+id/private_space_setup_success"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:filterTouchesWhenObscured="true"
- app:sucHeaderText="@string/privatespace_success_title"
- app:sudDescriptionText="@string/privatespace_access_text"
+ app:sucHeaderText="@string/private_space_success_title"
+ app:sudDescriptionText="@string/private_space_access_text"
android:icon="@drawable/ic_privatespace_done">
</com.google.android.setupdesign.GlifLayout>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 5795aea..01f2525 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1253,63 +1253,61 @@
<!-- Title of the Alert Dialog when no screen lock is set [CHAR LIMIT=30] -->
<string name="no_device_lock_title">Set a screen lock</string>
<!-- Summary of the alert when no screen lock is set [CHAR LIMIT=90] -->
- <string name="no_device_lock_summary">To use Private Space, set a screen lock on this device.</string>
+ <string name="no_device_lock_summary">To use your private space, set a screen lock on this device</string>
<!-- Action label for dialog when no screen lock is set [CHAR LIMIT=30] -->
<string name="no_device_lock_action_label">Set screen lock</string>
<!-- Action label to cancel Alert dialog when no screen lock is set [CHAR LIMIT=30] -->
<string name="no_device_lock_cancel">Cancel</string>
- <!-- Action label to cancel Private Space Setup flow [CHAR LIMIT=30] -->
- <string name="privatespace_cancel_label">Cancel</string>
- <!-- Label for Private Space setup button to create Private Space [CHAR LIMIT=30] -->
- <string name="privatespace_setup_button_label">Set up</string>
+ <!-- Action label to cancel private space Setup flow [CHAR LIMIT=30] -->
+ <string name="private_space_cancel_label">Cancel</string>
+ <!-- Label for private space setup button to create private space [CHAR LIMIT=30] -->
+ <string name="private_space_setup_button_label">Set up</string>
<!-- Title for Private Space setup education screen. [CHAR LIMIT=50] -->
- <string name="privatespace_setup_title">Set up Private Space</string>
- <!-- Summary for the Private Space setup education screen. [CHAR LIMIT=NONE] -->
- <string name="privatespace_hide_apps_summary">Hide private apps in a secure space that only you can access</string>
- <!-- Text shown in Private Space setup screen which explains how the Private Space works [CHAR LIMIT=50] -->
- <string name="privatespace_how_title">How it works</string>
- <!-- Text shown in Private Space setup screen which explains Private Space can be accessed from bottom of all apps list. [CHAR LIMIT=NONE] -->
- <string name="privatespace_access_bottom_text">You can access Private Space from the bottom of your apps list</string>
- <!-- Text shown in Private Space setup screen which explains Private Space apps are protected by a lock. [CHAR LIMIT=60] -->
- <string name="privatespace_protected_lock_text">Apps in Private Space are protected by a lock</string>
- <!-- Text shown in Private Space setup screen which explains notifications from Private Space apps will not be shown when Private Space is locked. [CHAR LIMIT=NONE] -->
- <string name="privatespace_hidden_notifications_text">Notifications from apps in Private Space are hidden when it\u2019s locked</string>
- <!-- Text shown in Private Space setup screen which explains that the permissions granted to Private Space apps will not be shown in settings when Private Space is locked. [CHAR LIMIT=NONE] -->
- <string name="privatespace_apps_permission_text">Private Space apps won\u2019t appear in permission manager, privacy dashboard, and other settings when Private Space is locked</string>
- <!-- Text shown at the bottom in Private Space auto advancing screens. [CHAR LIMIT=60] -->
- <string name="privatespace_setting_up_text">Setting up Private Space\u2026</string>
- <!-- Title for Private Space setup in auto advancing screen informing private space is hidden when locked. [CHAR LIMIT=NONE] -->
- <string name="privatespace_apps_hidden_title">Usage info for Private Space apps is hidden when it\u2019s locked</string>
- <!-- Title for Private Space setup in auto advancing screen informing private space can be accessed from apps list. [CHAR LIMIT=60] -->
- <string name="privatespace_access_from_apps_title">Access Private Space from your apps list</string>
- <!-- Title for Private Space setup in auto advancing screen informing some system apps are already installed in Private Space. [CHAR LIMIT=NONE] -->
- <string name="privatespace_system_apps_installed_title">Some system apps are already installed in Private Space</string>
- <!-- Title for Private Space creation error screen. [CHAR LIMIT=60] -->
- <string name="privatespace_error_screen_title">Couldn\u2019t set up Private Space</string>
- <!-- Summary for the Private Space creation error screen. [CHAR LIMIT=60] -->
- <string name="privatespace_error_screen_summary">Try again now, or come back later</string>
+ <string name="private_space_setup_title">Set up a private space</string>
+ <!-- Summary for the private space setup education screen. [CHAR LIMIT=NONE] -->
+ <string name="private_space_hide_apps_summary">Keep private apps in a separate space that you can hide or lock</string>
+ <!-- Text shown in private space setup screen which explains how the private space works [CHAR LIMIT=50] -->
+ <string name="private_space_how_title">How it works</string>
+ <!-- Text shown in private space setup screen which explains private space can be accessed from bottom of all apps list. [CHAR LIMIT=NONE] -->
+ <string name="private_space_access_bottom_text">You can access your private space from the bottom of your apps list</string>
+ <!-- Text shown in private space setup screen which explains private space apps are protected by a lock. [CHAR LIMIT=60] -->
+ <string name="private_space_protected_lock_text">Apps in your private space are protected by a lock</string>
+ <!-- Text shown in private space setup screen which explains notifications from private space apps will not be shown when private space is locked. [CHAR LIMIT=NONE] -->
+ <string name="private_space_hidden_notifications_text">Notifications from apps in your private space are hidden when it\u2019s locked</string>
+ <!-- This is info text to help explain in private space setup screen that the permissions granted to private space apps will not be shown in settings when private space is locked. [CHAR LIMIT=NONE] -->
+ <string name="private_space_apps_permission_text">Apps in your private space won\'t appear in permission manager, privacy dashboard, and other settings when your private space is locked.\n\nYour private space can\'t be moved to a new device. You\'ll need to set up another private space if you want to use it on another device.\n\nAnyone that connects your device to a computer or installs harmful apps on your device may be able to access your private space.</string>
+ <!-- Text shown at the bottom in private space auto advancing screens. [CHAR LIMIT=60] -->
+ <string name="private_space_setting_up_text">Setting up private space\u2026</string>
+ <!-- Title for private space setup in auto advancing screen informing private space notifications are hidden when locked. [CHAR LIMIT=NONE] -->
+ <string name="private_space_notifications_hidden_title">Notifications from apps in private space are hidden when it\u2019s locked</string>
+ <!-- Title for private space setup in auto advancing screen informing photos/files from private space can be shared when unlocked. [CHAR LIMIT=NONE] -->
+ <string name="private_space_share_photos_title">Unlock your space to share photos or files from private space apps</string>
+ <!-- Title for private space setup in auto advancing screen informing some system apps are already installed in private space. [CHAR LIMIT=NONE] -->
+ <string name="private_space_apps_installed_title">Some apps are already installed in your private space</string>
+ <!-- Title for private space creation error screen. [CHAR LIMIT=60] -->
+ <string name="private_space_error_screen_title">Couldn\u2019t set up private space</string>
<!-- Label for button to retry creating private space again on creation error. [CHAR LIMIT=30] -->
- <string name="privatespace_tryagain_label">Try Again</string>
- <!-- Title for Private Space lock setup screen. [CHAR LIMIT=50] -->
- <string name="privatespace_lockscreen_title">Use screen lock to unlock?</string>
- <!-- Summary for the Private Space lock setup screen. [CHAR LIMIT=NONE] -->
- <string name="privatespace_lockscreen_summary">You can unlock Private Space the same way you unlock your device, or choose a different lock</string>
- <!-- Action label to use existing device lock for Private Space. [CHAR LIMIT=50] -->
- <string name="privatespace_use_screenlock_label">Use screen lock</string>
- <!-- Label for Private Space lock setup button to choose a new lock. [CHAR LIMIT=50] -->
- <string name="privatespace_set_lock_label">Choose new lock</string>
- <!-- Title for Private Space setup success screen. [CHAR LIMIT=30] -->
- <string name="privatespace_success_title">All set!</string>
- <!-- Summary for the Private Space setup success screen. [CHAR LIMIT=NONE] -->
- <string name="privatespace_access_text">You can access Private Space from your apps list</string>
- <!-- Label for Private Space done button to show a toast, finish setup and launch All apps [CHAR LIMIT=30] -->
- <string name="privatespace_done_label">Done</string>
+ <string name="private_space_tryagain_label">Try Again</string>
+ <!-- Title for private space lock setup screen. [CHAR LIMIT=50] -->
+ <string name="private_space_lockscreen_title">Use screen lock to unlock private space?</string>
+ <!-- Summary for the private space lock setup screen. [CHAR LIMIT=NONE] -->
+ <string name="private_space_lockscreen_summary">You can unlock your private space the same way you unlock your device, or choose a different lock</string>
+ <!-- Action label to use existing device lock for private space. [CHAR LIMIT=50] -->
+ <string name="private_space_use_screenlock_label">Use screen lock</string>
+ <!-- Label for private space lock setup button to choose a new lock. [CHAR LIMIT=50] -->
+ <string name="private_space_set_lock_label">Choose new lock</string>
+ <!-- Title for private space setup success screen. [CHAR LIMIT=30] -->
+ <string name="private_space_success_title">All set!</string>
+ <!-- Summary for the private space setup success screen. [CHAR LIMIT=NONE] -->
+ <string name="private_space_access_text">To access your private space, go to your apps list then scroll down</string>
+ <!-- Label for private space done button to show a toast, finish setup and launch All apps [CHAR LIMIT=30] -->
+ <string name="private_space_done_label">Done</string>
<!-- Toast to show on private space setup completion informing user to scroll down All apps to access private space. [CHAR LIMIT=60] -->
- <string name="scrolldown_to_access">Scroll down to access Private Space</string>
- <!-- Title for Private Space account login error screen. [CHAR LIMIT=60] -->
- <string name="privatespace_retry_signin_title">Sign in to set up Private Space</string>
- <!-- Summary for the Private Space account login error screen. [CHAR LIMIT=NONE] -->
- <string name="privatespace_retry_summary">You need to sign in to an account to set up Private Space</string>
+ <string name="private_space_scrolldown_to_access">Scroll down to find private space</string>
+ <!-- Title for private space account login error screen. [CHAR LIMIT=60] -->
+ <string name="private_space_retry_signin_title">Sign in to set up a private space</string>
+ <!-- Summary for the private space account login error screen. [CHAR LIMIT=NONE] -->
+ <string name="private_space_retry_summary">You need to sign in to an account to set up a private space</string>
<!-- private space lock setup screen title. This title is asking the user to choose a type of screen lock (such as a pattern, PIN, or password) that they need to enter to unlock private space. [CHAR LIMIT=60] -->
<string name="private_space_lock_setup_title">Choose a lock for your private space</string>
<!-- private space lock setup screen description [CHAR LIMIT=NONE] -->
@@ -3270,6 +3268,8 @@
<string name="error_mnc_not23">MNC field must be 2 or 3 digits.</string>
<!-- APN error dialog messages: -->
<string name="error_adding_apn_type">Carrier does not allow adding APNs of type %s.</string>
+ <!-- APN error messages: -->
+ <string name="error_mmsc_valid">MMSC field must be valid.</string>
<!-- The message of dialog indicated restoring default APN settings in progress -->
<string name="restore_default_apn">Restoring default APN settings.</string>
<!-- APNs screen menu option to reset default APN settings -->
@@ -3896,6 +3896,10 @@
<string name="controls_label">Controls</string>
<!-- Manage applications, text label for button to kill / force stop an application -->
<string name="force_stop">Force stop</string>
+ <!-- Manage applications, text label for button to archive an application. Archiving means uninstalling the app without deleting user's personal data and replacing the app with a stub app with minimum size. So, the user can unarchive the app later and not lose any personal data. -->
+ <string name="archive">Archive</string>
+ <!-- Manage applications, text label for button to restore an application. Restoring means installing the archived app. -->
+ <string name="restore">Restore</string>
<!-- Manage applications, individual application info screen,label under Storage heading. The total storage space taken up by this app. -->
<string name="total_size_label">Total</string>
<!-- Manage applications, individual application info screen, label under Storage heading. The amount of space taken up by the application itself (for example, the java compield files and things like that) -->
@@ -4006,6 +4010,17 @@
<!-- Manage applications, text for Move button -->
<string name="move_app">Move</string>
+ <!-- Toast message when archiving an app failed. -->
+ <string name="archiving_failed">Archiving failed</string>
+ <!-- Toast message when archiving an app succeeded. -->
+ <string name="archiving_succeeded">Archived <xliff:g id="package_label" example="Translate">%1$s</xliff:g></string>
+ <!-- Toast message when restoring an app failed. -->
+ <string name="restoring_failed">Restoring failed</string>
+ <!-- Toast message when restoring an app succeeded. -->
+ <string name="restoring_succeeded">Restored <xliff:g id="package_label" example="Translate">%1$s</xliff:g></string>
+ <!-- Toast message when restoring an app has started. -->
+ <string name="restoring_in_progress">Restoring <xliff:g id="package_label" example="Translate">%1$s</xliff:g></string>
+
<!-- Text of pop up message if the request for a "migrate primary storage" operation
(see storage_menu_migrate) is denied as another is already in progress. [CHAR LIMIT=75] -->
<string name="another_migration_already_in_progress">Another migration is already in progress.</string>
@@ -4639,8 +4654,8 @@
<string name="accessibility_tutorial_dialog_title_volume">Hold volume keys to open</string>
<!-- Title for the accessibility tutorial dialog in accessibility service with triple tap. [CHAR LIMIT=100] -->
<string name="accessibility_tutorial_dialog_title_triple">Triple tap screen to open</string>
- <!-- Title for the accessibility tutorial dialog in accessibility service with two finger triple tap. [CHAR LIMIT=100] -->
- <string name="accessibility_tutorial_dialog_title_two_finger_triple">Two finger triple tap screen to open</string>
+ <!-- Title for the accessibility tutorial dialog in accessibility service with two finger double tap. [CHAR LIMIT=100] -->
+ <string name="accessibility_tutorial_dialog_title_two_finger_double">Two finger double tap screen to open</string>
<!-- Title for the accessibility tutorial dialog in accessibility service with gesture. [CHAR LIMIT=50] -->
<string name="accessibility_tutorial_dialog_title_gesture">Use gesture to open</string>
<!-- Title for the accessibility tutorial dialog in gesture navigation settings. [CHAR LIMIT=50] -->
@@ -4653,8 +4668,8 @@
<string name="accessibility_tutorial_dialog_message_volume">To use this feature, press & hold both volume keys.</string>
<!-- Instruction for the accessibility tutorial dialog in accessibility service with triple tap. [CHAR LIMIT=100] -->
<string name="accessibility_tutorial_dialog_message_triple">To start and stop magnification, triple-tap anywhere on your screen.</string>
- <!-- Instruction for the accessibility tutorial dialog in accessibility service with two finger triple tap. [CHAR LIMIT=100] -->
- <string name="accessibility_tutorial_dialog_message_two_finger_triple">To start and stop magnification, triple-tap anywhere on your screen with two fingers.</string>
+ <!-- Instruction for the accessibility tutorial dialog in accessibility service with two finger double tap. [CHAR LIMIT=100] -->
+ <string name="accessibility_tutorial_dialog_message_two_finger_triple">To start and stop magnification, double-tap anywhere on your screen with two fingers.</string>
<!-- Message for the accessibility tutorial dialog when user enables an accessibility service while using gesture navigation and touch exploration is not enabled. [CHAR LIMIT=NONE] -->
<string name="accessibility_tutorial_dialog_message_gesture">To use this feature, swipe up from the bottom of the screen with 2 fingers.\n\nTo switch between features, swipe up with 2 fingers and hold.</string>
<!-- Message for the accessibility tutorial dialog when user enables an accessibility service while using gesture navigation and touch exploration is enabled. [CHAR LIMIT=NONE] -->
@@ -4697,12 +4712,12 @@
<string name="accessibility_shortcut_hardware_keyword">hold volume keys</string>
<!-- Summary for hardware shortcut in accessibility edit shortcut dialog. [CHAR LIMIT=NONE] -->
<string name="accessibility_shortcut_edit_dialog_summary_hardware">Press & hold both volume keys</string>
- <!-- Title for two finger triple tap shortcut in accessibility edit shortcut dialog. [CHAR LIMIT=NONE] -->
- <string name="accessibility_shortcut_edit_dialog_title_two_finger_triple_tap">Two-finger triple-tap screen</string>
+ <!-- Title for two finger double tap shortcut in accessibility edit shortcut dialog. [CHAR LIMIT=NONE] -->
+ <string name="accessibility_shortcut_edit_dialog_title_two_finger_double_tap">Two-finger double-tap screen</string>
<!-- Part of list to compose user's accessibility shortcut list. [CHAR LIMIT=NONE] -->
- <string name="accessibility_shortcut_two_finger_triple_tap_keyword">two-finger triple-tap screen</string>
- <!-- Summary for two finger triple tap shortcut in accessibility edit shortcut dialog. [CHAR LIMIT=NONE] -->
- <string name="accessibility_shortcut_edit_dialog_summary_two_finger_triple_tap">Quickly tap screen {0,number,integer} times with two fingers</string>
+ <string name="accessibility_shortcut_two_finger_double_tap_keyword">two-finger double-tap screen</string>
+ <!-- Summary for two finger double tap shortcut in accessibility edit shortcut dialog. [CHAR LIMIT=NONE] -->
+ <string name="accessibility_shortcut_edit_dialog_summary_two_finger_double_tap">Quickly tap screen {0,number,integer} times with two fingers</string>
<!-- Title for triple tap shortcut in accessibility edit shortcut dialog. [CHAR LIMIT=NONE] -->
<string name="accessibility_shortcut_edit_dialog_title_triple_tap">Triple-tap screen</string>
<!-- Part of list to compose user's accessibility shortcut list. [CHAR LIMIT=NONE] -->
@@ -9100,6 +9115,12 @@
<!-- Summary of the switch preference that controls whether the system will pause app activity when the app has not been used for months [CHAR LIMIT=NONE]-->
<string name="unused_apps_switch_summary">Remove permissions, delete temporary files, and stop notifications</string>
+ <!-- Label of a switch preference that controls whether the system will pause app activity when the app has not been used for a while [CHAR LIMIT=40]-->
+ <string name="unused_apps_switch_v2">Manage app if unused</string>
+
+ <!-- Summary of the switch preference that controls whether the system will pause app activity when the app has not been used for a while [CHAR LIMIT=NONE]-->
+ <string name="unused_apps_switch_summary_v2">Remove permissions, delete temporary files, stop notifications, and archive the app</string>
+
<!-- Label for showing all apps in list [CHAR LIMIT=30] -->
<string name="filter_all_apps">All apps</string>
<!-- Label for showing enabled apps in list [CHAR LIMIT=30] -->
diff --git a/res/xml/audio_stream_details_fragment.xml b/res/xml/audio_stream_details_fragment.xml
new file mode 100644
index 0000000..9727442
--- /dev/null
+++ b/res/xml/audio_stream_details_fragment.xml
@@ -0,0 +1,34 @@
+<?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.
+ -->
+
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:settings="http://schemas.android.com/apk/res-auto"
+ android:title="Audio stream details">
+
+ <com.android.settingslib.widget.LayoutPreference
+ android:key="audio_stream_header"
+ android:layout="@layout/settings_entity_header"
+ android:selectable="false"
+ settings:allowDividerBelow="true"
+ settings:searchable="false" />
+
+ <com.android.settingslib.widget.ActionButtonsPreference
+ android:key="audio_stream_button"
+ settings:allowDividerBelow="true" />
+
+</PreferenceScreen>
diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml
index f0a2881..e1ccad8 100644
--- a/res/xml/development_settings.xml
+++ b/res/xml/development_settings.xml
@@ -727,10 +727,11 @@
android:title="@string/enable_non_resizable_multi_window"
android:summary="@string/enable_non_resizable_multi_window_summary" />
- <SwitchPreferenceCompat
- android:key="back_navigation_animation"
- android:title="@string/back_navigation_animation"
- android:summary="@string/back_navigation_animation_summary" />
+ // TODO(b/315859328): Temporally removed since causing search indexing failure.
+<!-- <SwitchPreferenceCompat-->
+<!-- android:key="back_navigation_animation"-->
+<!-- android:title="@string/back_navigation_animation"-->
+<!-- android:summary="@string/back_navigation_animation_summary" />-->
<Preference
android:key="reset_shortcut_manager_throttling"
diff --git a/res/xml/network_provider_internet.xml b/res/xml/network_provider_internet.xml
index cd44ab1..1a8ee08 100644
--- a/res/xml/network_provider_internet.xml
+++ b/res/xml/network_provider_internet.xml
@@ -31,10 +31,16 @@
settings:keywords="@string/keywords_internet"
settings:useAdminDisabledSummary="true" />
- <com.android.settings.spa.preference.ComposePreference
+ <com.android.settingslib.RestrictedPreference
android:key="calls_and_sms"
android:title="@string/calls_and_sms"
+ android:icon="@drawable/ic_calls_sms"
android:order="-20"
+ android:summary="@string/summary_placeholder"
+ android:fragment="com.android.settings.network.NetworkProviderCallsSmsFragment"
+ settings:userRestriction="no_config_mobile_networks"
+ settings:allowDividerBelow="true"
+ settings:useAdminDisabledSummary="true"
settings:controller="com.android.settings.network.NetworkProviderCallsSmsController" />
<com.android.settingslib.RestrictedPreference
diff --git a/res/xml/privatespace_setup_education_entries.xml b/res/xml/privatespace_setup_education_entries.xml
deleted file mode 100644
index 18fb19d..0000000
--- a/res/xml/privatespace_setup_education_entries.xml
+++ /dev/null
@@ -1,39 +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.
- -->
-
-<ItemGroup
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
-
- <Item
- android:enabled="false"
- android:icon="@drawable/ic_apps"
- android:title="@string/privatespace_access_bottom_text"
- app:sudIconTint="?android:attr/textColorSecondary"/>
-
- <Item
- android:enabled="false"
- android:icon="@drawable/ic_lock_24dp"
- android:title="@string/privatespace_protected_lock_text"
- app:sudIconTint="?android:attr/textColorSecondary"/>
-
- <Item
- android:enabled="false"
- android:icon="@drawable/ic_notifications"
- android:title="@string/privatespace_hidden_notifications_text"
- app:sudIconTint="?android:attr/textColorSecondary"/>
-</ItemGroup>
diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java
index 68ae0ae..86baba4 100644
--- a/src/com/android/settings/Settings.java
+++ b/src/com/android/settings/Settings.java
@@ -478,4 +478,6 @@
public static class OneHandedSettingsActivity extends SettingsActivity { /* empty */ }
public static class PreviouslyConnectedDeviceActivity extends SettingsActivity { /* empty */ }
+
+ public static class ScreenTimeoutActivity extends SettingsActivity { /* empty */ }
}
diff --git a/src/com/android/settings/accessibility/AccessibilityDialogUtils.java b/src/com/android/settings/accessibility/AccessibilityDialogUtils.java
index c429e0b..ca3a7b1 100644
--- a/src/com/android/settings/accessibility/AccessibilityDialogUtils.java
+++ b/src/com/android/settings/accessibility/AccessibilityDialogUtils.java
@@ -249,7 +249,7 @@
initSoftwareShortcut(context, contentView);
initHardwareShortcut(context, contentView);
if (Flags.enableMagnificationMultipleFingerMultipleTapGesture()) {
- initTwoFingerTripleTapMagnificationShortcut(context, contentView);
+ initTwoFingerDoubleTapMagnificationShortcut(context, contentView);
}
initMagnifyShortcut(context, contentView);
initAdvancedWidget(contentView);
@@ -260,7 +260,7 @@
initSoftwareShortcutForSUW(context, contentView);
initHardwareShortcut(context, contentView);
if (Flags.enableMagnificationMultipleFingerMultipleTapGesture()) {
- initTwoFingerTripleTapMagnificationShortcut(context, contentView);
+ initTwoFingerDoubleTapMagnificationShortcut(context, contentView);
}
initMagnifyShortcut(context, contentView);
initAdvancedWidget(contentView);
@@ -365,15 +365,15 @@
R.raw.a11y_shortcut_type_triple_tap);
}
- private static void initTwoFingerTripleTapMagnificationShortcut(Context context, View view) {
+ private static void initTwoFingerDoubleTapMagnificationShortcut(Context context, View view) {
// TODO(b/306153204): Update shortcut string and image when UX provides them
final View dialogView = view.findViewById(R.id.two_finger_triple_tap_shortcut);
final CharSequence title = context.getText(
- R.string.accessibility_shortcut_edit_dialog_title_two_finger_triple_tap);
+ R.string.accessibility_shortcut_edit_dialog_title_two_finger_double_tap);
String summary = context.getString(
- R.string.accessibility_shortcut_edit_dialog_summary_two_finger_triple_tap);
- // Format the number '3' in the summary.
- final Object[] arguments = {3};
+ R.string.accessibility_shortcut_edit_dialog_summary_two_finger_double_tap);
+ // Format the number '2' in the summary.
+ final Object[] arguments = {2};
summary = MessageFormat.format(summary, arguments);
setupShortcutWidgetWithImageRawResource(context, dialogView, title, summary,
diff --git a/src/com/android/settings/accessibility/AccessibilityGestureNavigationTutorial.java b/src/com/android/settings/accessibility/AccessibilityGestureNavigationTutorial.java
index e90ed87..1f71ab0 100644
--- a/src/com/android/settings/accessibility/AccessibilityGestureNavigationTutorial.java
+++ b/src/com/android/settings/accessibility/AccessibilityGestureNavigationTutorial.java
@@ -416,7 +416,7 @@
// TODO(b/308088945): Update tutorial string and image when UX provides them
final int type = UserShortcutType.TWOFINGERTRIPLETAP;
final CharSequence title =
- context.getText(R.string.accessibility_tutorial_dialog_title_two_finger_triple);
+ context.getText(R.string.accessibility_tutorial_dialog_title_two_finger_double);
final View image =
createIllustrationViewWithImageRawResource(context,
R.raw.a11y_shortcut_type_triple_tap);
diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java
index 9f6eec3..1d946fb 100644
--- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java
+++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java
@@ -477,7 +477,7 @@
if (Flags.enableMagnificationMultipleFingerMultipleTapGesture()) {
if (hasShortcutType(shortcutTypes, UserShortcutType.TWOFINGERTRIPLETAP)) {
final CharSequence twoFingerTripleTapTitle = context.getText(
- R.string.accessibility_shortcut_two_finger_triple_tap_keyword);
+ R.string.accessibility_shortcut_two_finger_double_tap_keyword);
list.add(twoFingerTripleTapTitle);
}
}
diff --git a/src/com/android/settings/biometrics/BiometricEnrollBase.java b/src/com/android/settings/biometrics/BiometricEnrollBase.java
index 7df1fe1..c9c8cff 100644
--- a/src/com/android/settings/biometrics/BiometricEnrollBase.java
+++ b/src/com/android/settings/biometrics/BiometricEnrollBase.java
@@ -67,7 +67,6 @@
public static final String EXTRA_FINISHED_ENROLL_FACE = "finished_enrolling_face";
public static final String EXTRA_FINISHED_ENROLL_FINGERPRINT = "finished_enrolling_fingerprint";
public static final String EXTRA_LAUNCHED_POSTURE_GUIDANCE = "launched_posture_guidance";
- public static final String KEY_CALIBRATOR_UUID = "calibrator_uuid";
/**
* Used by the choose fingerprint wizard to indicate the wizard is
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
index 063d55d..6e90885 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
@@ -70,6 +70,7 @@
import com.android.settings.biometrics.BiometricsEnrollEnrolling;
import com.android.settings.biometrics.fingerprint.feature.SfpsEnrollmentFeature;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+import com.android.settings.flags.Flags;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.display.DisplayDensityUtils;
@@ -197,6 +198,8 @@
@NonNull
private SfpsEnrollmentFeature mSfpsEnrollmentFeature = new EmptySfpsEnrollmentFeature();
+ @Nullable
+ private UdfpsEnrollCalibrator mCalibrator;
@VisibleForTesting
protected boolean shouldShowLottie() {
@@ -245,6 +248,12 @@
setContentView(layout);
setDescriptionText(R.string.security_settings_udfps_enroll_start_message);
+
+ if (Flags.udfpsEnrollCalibration()) {
+ mCalibrator = FeatureFactory.getFeatureFactory().getFingerprintFeatureProvider()
+ .getUdfpsEnrollCalibrator(getApplicationContext(), savedInstanceState,
+ getIntent());
+ }
} else if (mCanAssumeSfps) {
mSfpsEnrollmentFeature = FeatureFactory.getFeatureFactory()
.getFingerprintFeatureProvider().getSfpsEnrollmentFeature();
@@ -364,6 +373,11 @@
super.onSaveInstanceState(outState);
outState.putBoolean(KEY_STATE_CANCELED, mIsCanceled);
outState.putInt(KEY_STATE_PREVIOUS_ROTATION, mPreviousRotation);
+ if (Flags.udfpsEnrollCalibration()) {
+ if (mCalibrator != null) {
+ mCalibrator.onSaveInstanceState(outState);
+ }
+ }
}
private void restoreSavedState(Bundle savedInstanceState) {
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
index 276845c..b71330a 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
@@ -25,8 +25,6 @@
import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
import android.util.Log;
import android.view.OrientationEventListener;
import android.view.Surface;
@@ -35,15 +33,12 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.lifecycle.Observer;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.biometrics.BiometricEnrollBase;
import com.android.settings.biometrics.BiometricEnrollSidecar;
import com.android.settings.biometrics.BiometricUtils;
-import com.android.settings.biometrics.fingerprint.UdfpsEnrollCalibrator.Result;
-import com.android.settings.biometrics.fingerprint.UdfpsEnrollCalibrator.Status;
import com.android.settings.flags.Flags;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.password.ChooseLockSettingsHelper;
@@ -56,7 +51,6 @@
import com.google.android.setupcompat.template.FooterButton;
import java.util.List;
-import java.util.UUID;
/**
* Activity explaining the fingerprint sensor location for fingerprint enrollment.
@@ -85,8 +79,6 @@
private boolean mIsReverseDefaultRotation;
@Nullable
private UdfpsEnrollCalibrator mCalibrator;
- @Nullable
- private Observer<Status> mCalibratorStatusObserver;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -172,13 +164,15 @@
if (mCanAssumeUdfps) {
if (Flags.udfpsEnrollCalibration()) {
mCalibrator = FeatureFactory.getFeatureFactory().getFingerprintFeatureProvider()
- .getUdfpsEnrollCalibrator(
- (savedInstanceState != null)
- ? savedInstanceState.getParcelable(KEY_CALIBRATOR_UUID, UUID.class)
- : getIntent().getSerializableExtra(KEY_CALIBRATOR_UUID, UUID.class)
- );
- if (mCalibrator == null
- || mCalibrator.getStatusLiveData().getValue() == Status.FINISHED) {
+ .getUdfpsEnrollCalibrator(getApplicationContext(), savedInstanceState,
+ getIntent());
+ if (mCalibrator != null) {
+ mCalibrator.onFindSensorPage(
+ getLifecycle(),
+ getSupportFragmentManager(),
+ this::enableUdfpsLottieAndNextButton
+ );
+ } else {
enableUdfpsLottieAndNextButton();
}
} else {
@@ -193,14 +187,19 @@
}
private void enableUdfpsLottieAndNextButton() {
- mFooterBarMixin.setPrimaryButton(
- new FooterButton.Builder(this)
- .setText(R.string.security_settings_udfps_enroll_find_sensor_start_button)
- .setListener(this::onStartButtonClick)
- .setButtonType(FooterButton.ButtonType.NEXT)
- .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
- .build()
- );
+ if (isFinishing()) {
+ return;
+ }
+
+ if (mFooterBarMixin.getPrimaryButton() == null) {
+ mFooterBarMixin.setPrimaryButton(new FooterButton.Builder(this)
+ .setText(R.string.security_settings_udfps_enroll_find_sensor_start_button)
+ .setListener(this::onStartButtonClick)
+ .setButtonType(FooterButton.ButtonType.NEXT)
+ .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
+ .build()
+ );
+ }
if (mIllustrationLottie != null) {
mIllustrationLottie.setOnClickListener(this::onStartButtonClick);
}
@@ -285,7 +284,7 @@
outState.putBoolean(SAVED_STATE_IS_NEXT_CLICKED, mNextClicked);
if (Flags.udfpsEnrollCalibration()) {
if (mCalibrator != null) {
- outState.putSerializable(KEY_CALIBRATOR_UUID, mCalibrator.getUuid());
+ mCalibrator.onSaveInstanceState(outState);
}
}
}
@@ -317,39 +316,6 @@
if (mAnimation != null) {
mAnimation.startAnimation();
}
- if (Flags.udfpsEnrollCalibration()) {
- if (mCalibrator != null) {
- final Status current = mCalibrator.getStatusLiveData().getValue();
- if (current == Status.PROCESSING) {
- if (mCalibratorStatusObserver == null) {
- mCalibratorStatusObserver = status -> {
- if (status == Status.GOT_RESULT) {
- onGotCalibrationResult();
- }
- };
- }
- mCalibrator.getStatusLiveData().observe(this, mCalibratorStatusObserver);
- } else if (current == Status.GOT_RESULT) {
- onGotCalibrationResult();
- }
- }
- }
- }
-
- private void onGotCalibrationResult() {
- if (Flags.udfpsEnrollCalibration()) {
- if (mCalibrator != null) {
- mCalibrator.setFinished();
- if (mCalibrator.getResult() == Result.NEED_CALIBRATION) {
- UdfpsEnrollCalibrationDialog.newInstance(
- mCalibrator.getCalibrationDialogTitleTextId(),
- mCalibrator.getCalibrationDialogMessageTextId(),
- mCalibrator.getCalibrationDialogDismissButtonTextId()
- ).show(getSupportFragmentManager(), "findsensor-calibration-dialog");
- }
- }
- new Handler(Looper.getMainLooper()).post(this::enableUdfpsLottieAndNextButton);
- }
}
private void stopLookingForFingerprint() {
@@ -407,12 +373,6 @@
if (mAnimation != null) {
mAnimation.pauseAnimation();
}
- if (Flags.udfpsEnrollCalibration()) {
- if (mCalibrator != null && mCalibratorStatusObserver != null) {
- mCalibrator.getStatusLiveData().removeObserver(mCalibratorStatusObserver);
- mCalibratorStatusObserver = null;
- }
- }
}
@Override
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java
index dc3c65e..bd52b64 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java
@@ -57,7 +57,6 @@
import com.google.android.setupdesign.util.DeviceHelper;
import java.util.List;
-import java.util.UUID;
public class FingerprintEnrollIntroduction extends BiometricEnrollIntroduction {
@@ -92,12 +91,7 @@
if (Flags.udfpsEnrollCalibration()) {
mCalibrator = FeatureFactory.getFeatureFactory().getFingerprintFeatureProvider()
- .getUdfpsEnrollCalibrator(
- (savedInstanceState != null)
- ? savedInstanceState.getParcelable(
- KEY_CALIBRATOR_UUID, UUID.class)
- : null
- );
+ .getUdfpsEnrollCalibrator(getApplicationContext(), savedInstanceState, null);
}
final ImageView iconFingerprint = findViewById(R.id.icon_fingerprint);
@@ -175,7 +169,7 @@
super.onSaveInstanceState(outState);
if (Flags.udfpsEnrollCalibration()) {
if (mCalibrator != null) {
- outState.putSerializable(KEY_CALIBRATOR_UUID, mCalibrator.getUuid());
+ mCalibrator.onSaveInstanceState(outState);
}
}
}
@@ -391,7 +385,7 @@
}
if (Flags.udfpsEnrollCalibration()) {
if (mCalibrator != null) {
- intent.putExtra(KEY_CALIBRATOR_UUID, mCalibrator.getUuid());
+ intent.putExtras(mCalibrator.getExtrasForNextIntent());
}
}
return intent;
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintFeatureProvider.java b/src/com/android/settings/biometrics/fingerprint/FingerprintFeatureProvider.java
index 5a2bf8b..e770220 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintFeatureProvider.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintFeatureProvider.java
@@ -16,12 +16,15 @@
package com.android.settings.biometrics.fingerprint;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settings.biometrics.fingerprint.feature.SfpsEnrollmentFeature;
-import java.util.UUID;
-
public interface FingerprintFeatureProvider {
/**
* Gets the feature implementation of SFPS enrollment.
@@ -29,11 +32,16 @@
*/
SfpsEnrollmentFeature getSfpsEnrollmentFeature();
+
/**
- * Gets calibrator to calibrate the FPS before enrolling udfps
- * @param uuid unique id for passed between different activities
- * @return udfps calibrator
+ * Gets calibrator for udfps pre-enroll
+ * @param appContext application context
+ * @param activitySavedInstanceState activity savedInstanceState
+ * @param activityIntent activity intent
*/
@Nullable
- UdfpsEnrollCalibrator getUdfpsEnrollCalibrator(@Nullable UUID uuid);
+ default UdfpsEnrollCalibrator getUdfpsEnrollCalibrator(@NonNull Context appContext,
+ @Nullable Bundle activitySavedInstanceState, @Nullable Intent activityIntent) {
+ return null;
+ }
}
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintFeatureProviderImpl.java b/src/com/android/settings/biometrics/fingerprint/FingerprintFeatureProviderImpl.java
index 1baabc6..9745ca3 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintFeatureProviderImpl.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintFeatureProviderImpl.java
@@ -21,8 +21,6 @@
import com.android.settings.biometrics.fingerprint.feature.SfpsEnrollmentFeature;
import com.android.settings.biometrics.fingerprint.feature.SfpsEnrollmentFeatureImpl;
-import java.util.UUID;
-
public class FingerprintFeatureProviderImpl implements FingerprintFeatureProvider {
@Nullable
@@ -35,10 +33,4 @@
}
return mSfpsEnrollmentFeatureImpl;
}
-
- @Nullable
- @Override
- public UdfpsEnrollCalibrator getUdfpsEnrollCalibrator(@Nullable UUID uuid) {
- return null;
- }
}
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
index 308b3d5..6904342 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
@@ -93,6 +93,7 @@
import com.google.android.setupdesign.util.DeviceHelper;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -876,6 +877,8 @@
@Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
if (!isFingerprintHardwareDetected(context)) {
+ Log.e(TAG, "Fingerprint hardware is not detected");
+ mControllers = Collections.emptyList();
return null;
}
diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrationDialog.kt b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrationDialog.kt
deleted file mode 100644
index 892996a..0000000
--- a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrationDialog.kt
+++ /dev/null
@@ -1,58 +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.settings.biometrics.fingerprint
-
-import android.app.Dialog
-import android.content.DialogInterface
-import android.os.Bundle
-import androidx.annotation.StringRes
-import androidx.appcompat.app.AlertDialog
-import androidx.fragment.app.DialogFragment
-import com.android.settings.R
-
-class UdfpsEnrollCalibrationDialog : DialogFragment() {
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
- AlertDialog.Builder(requireActivity(), R.style.Theme_AlertDialog)
- .setTitle(arguments!!.getInt(KEY_TITLE_TEXT_ID))
- .setMessage(arguments!!.getInt(KEY_MESSAGE_TEXT_ID))
- .setPositiveButton(arguments!!.getInt(KEY_DISMISS_BUTTON_TEXT_ID)) {
- dialog: DialogInterface?, _: Int -> dialog?.dismiss()
- }
- .create().also {
- isCancelable = false
- }
-
- companion object {
-
- private const val KEY_TITLE_TEXT_ID = "title_text_id"
- private const val KEY_MESSAGE_TEXT_ID = "message_text_id"
- private const val KEY_DISMISS_BUTTON_TEXT_ID = "dismiss_button_text_id"
-
- @JvmStatic
- fun newInstance(
- @StringRes titleTextId: Int,
- @StringRes messageTextId: Int,
- @StringRes dismissButtonTextId: Int
- ) = UdfpsEnrollCalibrationDialog().apply {
- arguments = Bundle().apply {
- putInt(KEY_TITLE_TEXT_ID, titleTextId)
- putInt(KEY_MESSAGE_TEXT_ID, messageTextId)
- putInt(KEY_DISMISS_BUTTON_TEXT_ID, dismissButtonTextId)
- }
- }
- }
-}
diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrator.kt b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrator.kt
index c0626d3..9809bcc 100644
--- a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrator.kt
+++ b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrator.kt
@@ -1,36 +1,18 @@
package com.android.settings.biometrics.fingerprint
-import androidx.annotation.StringRes
-import androidx.lifecycle.LiveData
-import java.util.UUID
+import android.os.Bundle
+import androidx.fragment.app.FragmentManager
+import androidx.lifecycle.Lifecycle
interface UdfpsEnrollCalibrator {
- enum class Status {
- PROCESSING,
- GOT_RESULT,
- FINISHED,
- }
+ val extrasForNextIntent: Bundle
- enum class Result {
- NEED_CALIBRATION,
- NO_NEED_CALIBRATION,
- }
+ fun onSaveInstanceState(outState: Bundle)
- val uuid: UUID
-
- val statusLiveData: LiveData<Status>
-
- val result: Result?
-
- fun setFinished()
-
- @get:StringRes
- val calibrationDialogTitleTextId: Int
-
- @get:StringRes
- val calibrationDialogMessageTextId: Int
-
- @get:StringRes
- val calibrationDialogDismissButtonTextId: Int
+ fun onFindSensorPage(
+ lifecycle: Lifecycle,
+ fragmentManager: FragmentManager,
+ enableEnrollingRunnable: Runnable
+ )
}
\ No newline at end of file
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceAdapter.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceAdapter.java
index bc8ff21..a5f5adb 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceAdapter.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceAdapter.java
@@ -22,7 +22,8 @@
import android.view.ViewGroup;
import android.widget.Button;
-import com.android.internal.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView;
+
import com.android.settings.R;
import java.util.ArrayList;
@@ -32,11 +33,13 @@
private static final String TAG = "AudioSharingDeviceAdapter";
private final ArrayList<AudioSharingDeviceItem> mDevices;
private final OnClickListener mOnClickListener;
+ private final String mPrefix;
public AudioSharingDeviceAdapter(
- ArrayList<AudioSharingDeviceItem> devices, OnClickListener listener) {
+ ArrayList<AudioSharingDeviceItem> devices, OnClickListener listener, String prefix) {
mDevices = devices;
mOnClickListener = listener;
+ mPrefix = prefix;
}
private class AudioSharingDeviceViewHolder extends RecyclerView.ViewHolder {
@@ -49,7 +52,7 @@
public void bindView(int position) {
if (mButtonView != null) {
- mButtonView.setText(mDevices.get(position).getName());
+ mButtonView.setText(mPrefix + mDevices.get(position).getName());
mButtonView.setOnClickListener(
v -> mOnClickListener.onClick(mDevices.get(position)));
} else {
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java
index 5cd86b4..32cd2f8 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java
@@ -22,19 +22,22 @@
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
+import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
-import com.android.internal.widget.LinearLayoutManager;
-import com.android.internal.widget.RecyclerView;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+import com.google.common.collect.Iterables;
+
import java.util.ArrayList;
-import java.util.Locale;
+import java.util.stream.Collectors;
public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
private static final String TAG = "AudioSharingDialog";
@@ -54,8 +57,6 @@
private static DialogEventListener sListener;
- private View mRootView;
-
@Override
public int getMetricsCategory() {
return SettingsEnums.DIALOG_START_AUDIO_SHARING;
@@ -90,40 +91,63 @@
ArrayList<AudioSharingDeviceItem> deviceItems =
arguments.getParcelableArrayList(BUNDLE_KEY_DEVICE_ITEMS);
final AlertDialog.Builder builder =
- new AlertDialog.Builder(getActivity()).setTitle("Share audio").setCancelable(false);
- mRootView =
- LayoutInflater.from(builder.getContext())
- .inflate(R.layout.dialog_audio_sharing, /* parent= */ null);
- TextView subTitle1 = mRootView.findViewById(R.id.share_audio_subtitle1);
- TextView subTitle2 = mRootView.findViewById(R.id.share_audio_subtitle2);
+ new AlertDialog.Builder(getActivity()).setCancelable(false);
+ LayoutInflater inflater = LayoutInflater.from(builder.getContext());
+ View customTitle = inflater.inflate(R.layout.dialog_custom_title_audio_sharing, null);
+ ImageView icon = customTitle.findViewById(R.id.title_icon);
+ icon.setImageResource(R.drawable.ic_bt_audio_sharing);
+ TextView title = customTitle.findViewById(R.id.title_text);
+ View rootView = inflater.inflate(R.layout.dialog_audio_sharing, /* parent= */ null);
+ TextView subTitle1 = rootView.findViewById(R.id.share_audio_subtitle1);
+ TextView subTitle2 = rootView.findViewById(R.id.share_audio_subtitle2);
+ RecyclerView recyclerView = rootView.findViewById(R.id.btn_list);
+ Button shareBtn = rootView.findViewById(R.id.share_btn);
+ Button cancelBtn = rootView.findViewById(R.id.cancel_btn);
if (deviceItems.isEmpty()) {
- subTitle1.setVisibility(View.INVISIBLE);
+ title.setText("Share your audio");
subTitle2.setText(
- "To start sharing audio, connect additional headphones that support LE audio");
- } else {
+ "To start sharing audio, "
+ + "connect two pairs of headphones that support LE Audio");
+ ImageView image = rootView.findViewById(R.id.share_audio_guidance);
+ image.setVisibility(View.VISIBLE);
+ builder.setNegativeButton("Close", null);
+ } else if (deviceItems.size() == 1) {
+ title.setText("Share your audio");
subTitle1.setText(
- String.format(
- Locale.US,
- "%d additional device%s connected",
- deviceItems.size(),
- deviceItems.size() > 1 ? "" : "s"));
+ deviceItems.stream()
+ .map(AudioSharingDeviceItem::getName)
+ .collect(Collectors.joining(" and ")));
subTitle2.setText(
- "The headphones you share audio with will hear videos and music playing on this"
- + " phone");
+ "This device's music and videos will play on both pairs of headphones");
+ shareBtn.setText("Share audio");
+ shareBtn.setOnClickListener(
+ v -> {
+ sListener.onItemClick(Iterables.getOnlyElement(deviceItems));
+ dismiss();
+ });
+ cancelBtn.setOnClickListener(v -> dismiss());
+ subTitle1.setVisibility(View.VISIBLE);
+ shareBtn.setVisibility(View.VISIBLE);
+ cancelBtn.setVisibility(View.VISIBLE);
+ } else {
+ title.setText("Share audio with another device");
+ subTitle2.setText(
+ "This device's music and videos will play on the headphones you connect");
+ recyclerView.setAdapter(
+ new AudioSharingDeviceAdapter(
+ deviceItems,
+ (AudioSharingDeviceItem item) -> {
+ sListener.onItemClick(item);
+ dismiss();
+ },
+ "Connect "));
+ recyclerView.setLayoutManager(
+ new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));
+ recyclerView.setVisibility(View.VISIBLE);
+ cancelBtn.setOnClickListener(v -> dismiss());
+ cancelBtn.setVisibility(View.VISIBLE);
}
- RecyclerView recyclerView = mRootView.findViewById(R.id.btn_list);
- recyclerView.setAdapter(
- new AudioSharingDeviceAdapter(
- deviceItems,
- (AudioSharingDeviceItem item) -> {
- sListener.onItemClick(item);
- dismiss();
- }));
- recyclerView.setLayoutManager(
- new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));
- Button cancelBtn = mRootView.findViewById(R.id.cancel_btn);
- cancelBtn.setOnClickListener(v -> dismiss());
- AlertDialog dialog = builder.setView(mRootView).create();
+ AlertDialog dialog = builder.setCustomTitle(customTitle).setView(rootView).create();
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java
index 461c230..a2b1824 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java
@@ -22,14 +22,15 @@
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
+import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
-import com.android.internal.widget.LinearLayoutManager;
-import com.android.internal.widget.RecyclerView;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
@@ -90,17 +91,18 @@
Bundle arguments = requireArguments();
ArrayList<AudioSharingDeviceItem> deviceItems =
arguments.getParcelableArrayList(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS);
- String newDeviceName = arguments.getString(BUNDLE_KEY_NEW_DEVICE_NAME);
final AlertDialog.Builder builder =
- new AlertDialog.Builder(getActivity())
- .setTitle("Choose headphone to disconnect")
- .setCancelable(false);
+ new AlertDialog.Builder(getActivity()).setCancelable(false);
+ LayoutInflater inflater = LayoutInflater.from(builder.getContext());
+ View customTitle = inflater.inflate(R.layout.dialog_custom_title_audio_sharing, null);
+ ImageView icon = customTitle.findViewById(R.id.title_icon);
+ icon.setImageResource(R.drawable.ic_bt_audio_sharing);
+ TextView title = customTitle.findViewById(R.id.title_text);
+ title.setText("Choose a device to disconnect");
View rootView =
- LayoutInflater.from(builder.getContext())
- .inflate(R.layout.dialog_audio_sharing_disconnect, /* parent= */ null);
+ inflater.inflate(R.layout.dialog_audio_sharing_disconnect, /* parent= */ null);
TextView subTitle = rootView.findViewById(R.id.share_audio_disconnect_description);
- subTitle.setText(
- "To share audio with " + newDeviceName + ", disconnect another pair of headphone");
+ subTitle.setText("Only 2 devices can share audio at a time");
RecyclerView recyclerView = rootView.findViewById(R.id.device_btn_list);
recyclerView.setAdapter(
new AudioSharingDeviceAdapter(
@@ -108,7 +110,8 @@
(AudioSharingDeviceItem item) -> {
sListener.onItemClick(item);
dismiss();
- }));
+ },
+ "Disconnect "));
recyclerView.setLayoutManager(
new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));
Button cancelBtn = rootView.findViewById(R.id.cancel_btn);
@@ -116,7 +119,7 @@
v -> {
dismiss();
});
- AlertDialog dialog = builder.setView(rootView).create();
+ AlertDialog dialog = builder.setCustomTitle(customTitle).setView(rootView).create();
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java
index 2d7b4c4..f3f0fe4 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java
@@ -22,6 +22,7 @@
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
+import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
@@ -48,7 +49,6 @@
}
private static DialogEventListener sListener;
- private View mRootView;
@Override
public int getMetricsCategory() {
@@ -86,14 +86,17 @@
arguments.getParcelableArrayList(BUNDLE_KEY_DEVICE_ITEMS);
String newDeviceName = arguments.getString(BUNDLE_KEY_NEW_DEVICE_NAME);
final AlertDialog.Builder builder =
- new AlertDialog.Builder(getActivity())
- .setTitle("Share audio?")
- .setCancelable(false);
- mRootView =
- LayoutInflater.from(builder.getContext())
- .inflate(R.layout.dialog_audio_sharing_join, null /* parent */);
- TextView subtitle1 = mRootView.findViewById(R.id.share_audio_subtitle1);
- TextView subtitle2 = mRootView.findViewById(R.id.share_audio_subtitle2);
+ new AlertDialog.Builder(getActivity()).setCancelable(false);
+ LayoutInflater inflater = LayoutInflater.from(builder.getContext());
+ View customTitle =
+ inflater.inflate(R.layout.dialog_custom_title_audio_sharing, /* parent= */ null);
+ ImageView icon = customTitle.findViewById(R.id.title_icon);
+ icon.setImageResource(R.drawable.ic_bt_audio_sharing);
+ TextView title = customTitle.findViewById(R.id.title_text);
+ title.setText("Share your audio");
+ View rootView = inflater.inflate(R.layout.dialog_audio_sharing_join, /* parent= */ null);
+ TextView subtitle1 = rootView.findViewById(R.id.share_audio_subtitle1);
+ TextView subtitle2 = rootView.findViewById(R.id.share_audio_subtitle2);
if (deviceItems.isEmpty()) {
subtitle1.setText(newDeviceName);
} else {
@@ -106,10 +109,9 @@
.collect(Collectors.joining(", ")),
newDeviceName));
}
- subtitle2.setText(
- "Connected eligible headphones will hear videos ad music playing on this phone");
- Button shareBtn = mRootView.findViewById(R.id.share_btn);
- Button cancelBtn = mRootView.findViewById(R.id.cancel_btn);
+ subtitle2.setText("This device's music and videos will play on both pairs of headphones");
+ Button shareBtn = rootView.findViewById(R.id.share_btn);
+ Button cancelBtn = rootView.findViewById(R.id.cancel_btn);
shareBtn.setOnClickListener(
v -> {
sListener.onShareClick();
@@ -117,7 +119,7 @@
});
shareBtn.setText("Share audio");
cancelBtn.setOnClickListener(v -> dismiss());
- Dialog dialog = builder.setView(mRootView).create();
+ Dialog dialog = builder.setCustomTitle(customTitle).setView(rootView).create();
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java
index 31125de..1454f76 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java
@@ -19,11 +19,16 @@
import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
+import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment {
@@ -68,22 +73,20 @@
Bundle arguments = requireArguments();
String newDeviceName = arguments.getString(BUNDLE_KEY_NEW_DEVICE_NAME);
final AlertDialog.Builder builder =
- new AlertDialog.Builder(getActivity())
- .setTitle("Stop sharing audio?")
- .setCancelable(false);
+ new AlertDialog.Builder(getActivity()).setCancelable(false);
+ LayoutInflater inflater = LayoutInflater.from(builder.getContext());
+ View customTitle =
+ inflater.inflate(R.layout.dialog_custom_title_audio_sharing, /* parent= */ null);
+ ImageView icon = customTitle.findViewById(R.id.title_icon);
+ icon.setImageResource(R.drawable.ic_warning_24dp);
+ TextView title = customTitle.findViewById(R.id.title_text);
+ title.setText("Stop sharing audio?");
builder.setMessage(
- newDeviceName + " is connected, devices in audio sharing will disconnect.");
+ newDeviceName + " wants to connect, headphones in audio sharing will disconnect.");
builder.setPositiveButton(
- "Stop sharing",
- (dialog, which) -> {
- sListener.onStopSharingClick();
- });
- builder.setNegativeButton(
- "Cancel",
- (dialog, which) -> {
- dismiss();
- });
- AlertDialog dialog = builder.create();
+ "Stop sharing", (dialog, which) -> sListener.onStopSharingClick());
+ builder.setNegativeButton("Cancel", (dialog, which) -> dismiss());
+ AlertDialog dialog = builder.setCustomTitle(customTitle).create();
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java
new file mode 100644
index 0000000..1e69829
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java
@@ -0,0 +1,47 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.content.Context;
+
+import com.android.settings.R;
+import com.android.settings.dashboard.DashboardFragment;
+
+public class AudioStreamDetailsFragment extends DashboardFragment {
+ private static final String TAG = "AudioStreamDetailsFragment";
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ // TODO(chelseahao): update metrics id
+ return 0;
+ }
+
+ @Override
+ protected int getPreferenceScreenResId() {
+ return R.xml.audio_stream_details_fragment;
+ }
+
+ @Override
+ protected String getLogTag() {
+ return TAG;
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
index 8f701a3..ffb0b88 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
@@ -16,6 +16,9 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
+import android.bluetooth.BluetoothLeAudioContentMetadata;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
import android.util.AttributeSet;
@@ -24,11 +27,13 @@
import com.android.settings.R;
import com.android.settingslib.widget.TwoTargetPreference;
+import com.google.common.base.Strings;
+
/**
* Custom preference class for managing audio stream preferences with an optional lock icon. Extends
* {@link TwoTargetPreference}.
*/
-public class AudioStreamPreference extends TwoTargetPreference {
+class AudioStreamPreference extends TwoTargetPreference {
private boolean mIsConnected = false;
/**
@@ -36,7 +41,7 @@
*
* @param isConnected Is this streams connected
*/
- public void setIsConnected(
+ void setIsConnected(
boolean isConnected, @Nullable OnPreferenceClickListener onPreferenceClickListener) {
if (mIsConnected == isConnected
&& getOnPreferenceClickListener() == onPreferenceClickListener) {
@@ -50,7 +55,7 @@
notifyChanged();
}
- public AudioStreamPreference(Context context, @Nullable AttributeSet attrs) {
+ AudioStreamPreference(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
setIcon(R.drawable.ic_bt_audio_sharing);
}
@@ -64,4 +69,34 @@
protected int getSecondTargetResId() {
return R.layout.preference_widget_lock;
}
+
+ static AudioStreamPreference fromMetadata(
+ Context context, BluetoothLeBroadcastMetadata source) {
+ AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
+ preference.setTitle(getBroadcastName(source));
+ return preference;
+ }
+
+ static AudioStreamPreference fromReceiveState(
+ Context context, BluetoothLeBroadcastReceiveState state) {
+ AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
+ preference.setTitle(getBroadcastName(state));
+ return preference;
+ }
+
+ private static String getBroadcastName(BluetoothLeBroadcastMetadata source) {
+ return source.getSubgroups().stream()
+ .map(s -> s.getContentMetadata().getProgramInfo())
+ .filter(i -> !Strings.isNullOrEmpty(i))
+ .findFirst()
+ .orElse("Broadcast Id: " + source.getBroadcastId());
+ }
+
+ private static String getBroadcastName(BluetoothLeBroadcastReceiveState state) {
+ return state.getSubgroupMetadata().stream()
+ .map(BluetoothLeAudioContentMetadata::getProgramInfo)
+ .filter(i -> !Strings.isNullOrEmpty(i))
+ .findFirst()
+ .orElse("Broadcast Id: " + state.getBroadcastId());
+ }
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java
index 788b253..84e753c 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java
@@ -32,7 +32,7 @@
private static final String TAG = "AudioStreamsBroadcastAssistantCallback";
private static final boolean DEBUG = BluetoothUtils.D;
- private AudioStreamsProgressCategoryController mCategoryController;
+ private final AudioStreamsProgressCategoryController mCategoryController;
public AudioStreamsBroadcastAssistantCallback(
AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) {
@@ -52,6 +52,7 @@
+ " state: "
+ state);
}
+ mCategoryController.handleSourceConnected(state);
}
@Override
@@ -94,7 +95,20 @@
@Override
public void onSourceAddFailed(
- BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {}
+ BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "onSourceAddFailed() sink : "
+ + sink.getAddress()
+ + " source: "
+ + source
+ + " reason: "
+ + reason);
+ }
+ mCategoryController.showToast(
+ String.format(Locale.US, "Failed to join broadcast, reason %d", reason));
+ }
@Override
public void onSourceAdded(BluetoothDevice sink, int sourceId, int reason) {
@@ -119,7 +133,7 @@
if (DEBUG) {
Log.d(TAG, "onSourceFound() broadcastId : " + source.getBroadcastId());
}
- mCategoryController.addSourceFound(source);
+ mCategoryController.handleSourceFound(source);
}
@Override
@@ -127,7 +141,7 @@
if (DEBUG) {
Log.d(TAG, "onSourceLost() broadcastId : " + broadcastId);
}
- mCategoryController.removeSourceLost(broadcastId);
+ mCategoryController.handleSourceLost(broadcastId);
}
@Override
@@ -137,8 +151,23 @@
public void onSourceModifyFailed(BluetoothDevice sink, int sourceId, int reason) {}
@Override
- public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {}
+ public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {
+ Log.w(TAG, "onSourceRemoveFailed() sourceId : " + sourceId + " reason : " + reason);
+ mCategoryController.showToast(
+ String.format(
+ Locale.US,
+ "Failed to remove source %d for sink %s",
+ sourceId,
+ sink.getAddress()));
+ }
@Override
- public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {}
+ public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
+ if (DEBUG) {
+ Log.d(TAG, "onSourceRemoved() sourceId : " + sourceId + " reason : " + reason);
+ }
+ mCategoryController.showToast(
+ String.format(
+ Locale.US, "Source %d removed for sink %s", sourceId, sink.getAddress()));
+ }
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
index a673cb4..a418415 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
@@ -16,15 +16,25 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsScanQrCodeController.REQUEST_SCAN_BT_BROADCAST_QR_CODE;
+
+import android.app.Activity;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
+import android.util.Log;
import com.android.settings.R;
+import com.android.settings.connecteddevice.audiosharing.audiostreams.qrcode.QrCodeScanModeFragment;
import com.android.settings.dashboard.DashboardFragment;
+import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
+import com.android.settingslib.bluetooth.BluetoothUtils;
public class AudioStreamsDashboardFragment extends DashboardFragment {
private static final String TAG = "AudioStreamsDashboardFrag";
+ private static final boolean DEBUG = BluetoothUtils.D;
+ private AudioStreamsScanQrCodeController mAudioStreamsScanQrCodeController;
public AudioStreamsDashboardFragment() {
super();
@@ -59,7 +69,8 @@
@Override
public void onAttach(Context context) {
super.onAttach(context);
- use(AudioStreamsScanQrCodeController.class).setFragment(this);
+ mAudioStreamsScanQrCodeController = use(AudioStreamsScanQrCodeController.class);
+ mAudioStreamsScanQrCodeController.setFragment(this);
}
@Override
@@ -70,6 +81,34 @@
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
- // TODO(chelseahao): implementation.
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "onActivityResult() requestCode : "
+ + requestCode
+ + " resultCode : "
+ + resultCode);
+ }
+ if (requestCode == REQUEST_SCAN_BT_BROADCAST_QR_CODE) {
+ if (resultCode == Activity.RESULT_OK) {
+ String broadcastMetadata =
+ data.getStringExtra(QrCodeScanModeFragment.KEY_BROADCAST_METADATA);
+ BluetoothLeBroadcastMetadata source =
+ BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(
+ broadcastMetadata);
+ if (source == null) {
+ Log.w(TAG, "onActivityResult() source is null!");
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "onActivityResult() broadcastId : " + source.getBroadcastId());
+ }
+ if (mAudioStreamsScanQrCodeController == null) {
+ Log.w(TAG, "onActivityResult() AudioStreamsScanQrCodeController is null!");
+ return;
+ }
+ mAudioStreamsScanQrCodeController.addSource(source);
+ }
+ }
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
new file mode 100644
index 0000000..5acbc1f
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
@@ -0,0 +1,156 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import static java.util.Collections.emptyList;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.util.Log;
+
+import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import javax.annotation.Nullable;
+
+/**
+ * A helper class that adds, removes and retrieves LE broadcast sources for all active sink devices.
+ */
+class AudioStreamsHelper {
+
+ private static final String TAG = "AudioStreamsHelper";
+ private static final boolean DEBUG = BluetoothUtils.D;
+
+ private final @Nullable LocalBluetoothManager mBluetoothManager;
+ private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
+
+ AudioStreamsHelper(@Nullable LocalBluetoothManager bluetoothManager) {
+ mBluetoothManager = bluetoothManager;
+ mLeBroadcastAssistant = getLeBroadcastAssistant(mBluetoothManager);
+ }
+
+ /**
+ * Adds the specified LE broadcast source to all active sinks.
+ *
+ * @param source The LE broadcast metadata representing the audio source.
+ */
+ void addSource(BluetoothLeBroadcastMetadata source) {
+ if (mLeBroadcastAssistant == null) {
+ Log.w(TAG, "addSource(): LeBroadcastAssistant is null!");
+ return;
+ }
+ var unused =
+ ThreadUtils.postOnBackgroundThread(
+ () -> {
+ for (var sink : getActiveSinksOnAssistant(mBluetoothManager)) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "addSource(): join broadcast broadcastId"
+ + " : "
+ + source.getBroadcastId()
+ + " sink : "
+ + sink.getAddress());
+ }
+ mLeBroadcastAssistant.addSource(sink, source, false);
+ }
+ });
+ }
+
+ /** Removes all sources from LE broadcasts associated for all active sinks. */
+ void removeSource() {
+ if (mLeBroadcastAssistant == null) {
+ Log.w(TAG, "removeSource(): LeBroadcastAssistant is null!");
+ return;
+ }
+ var unused =
+ ThreadUtils.postOnBackgroundThread(
+ () -> {
+ for (var sink : getActiveSinksOnAssistant(mBluetoothManager)) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "removeSource(): remove all sources from sink : "
+ + sink.getAddress());
+ }
+ var sources = mLeBroadcastAssistant.getAllSources(sink);
+ if (!sources.isEmpty()) {
+ mLeBroadcastAssistant.removeSource(
+ sink, sources.get(0).getSourceId());
+ }
+ }
+ });
+ }
+
+ /** Retrieves a list of all LE broadcast receive states from active sinks. */
+ List<BluetoothLeBroadcastReceiveState> getAllSources() {
+ if (mLeBroadcastAssistant == null) {
+ Log.w(TAG, "getAllSources(): LeBroadcastAssistant is null!");
+ return emptyList();
+ }
+ return getActiveSinksOnAssistant(mBluetoothManager).stream()
+ .flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream())
+ .toList();
+ }
+
+ @Nullable
+ LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() {
+ return mLeBroadcastAssistant;
+ }
+
+ private static List<BluetoothDevice> getActiveSinksOnAssistant(
+ @Nullable LocalBluetoothManager manager) {
+ if (manager == null) {
+ Log.w(TAG, "getActiveSinksOnAssistant(): LocalBluetoothManager is null!");
+ return emptyList();
+ }
+ return AudioSharingUtils.getActiveSinkOnAssistant(manager)
+ .map(
+ cachedBluetoothDevice ->
+ Stream.concat(
+ Stream.of(cachedBluetoothDevice.getDevice()),
+ cachedBluetoothDevice.getMemberDevice().stream()
+ .map(CachedBluetoothDevice::getDevice))
+ .toList())
+ .orElse(emptyList());
+ }
+
+ private static @Nullable LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant(
+ @Nullable LocalBluetoothManager manager) {
+ if (manager == null) {
+ Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothManager is null!");
+ return null;
+ }
+
+ LocalBluetoothProfileManager profileManager = manager.getProfileManager();
+ if (profileManager == null) {
+ Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothProfileManager is null!");
+ return null;
+ }
+
+ return profileManager.getLeAudioBroadcastAssistantProfile();
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
index fef1e7b..45f0c2f 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
@@ -18,12 +18,18 @@
import static java.util.Collections.emptyList;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothLeAudioContentMetadata;
+import android.app.AlertDialog;
+import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
+import android.os.Bundle;
+import android.provider.Settings;
import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
@@ -31,23 +37,19 @@
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
+import com.android.settings.R;
import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
import com.android.settings.core.BasePreferenceController;
+import com.android.settings.core.SubSettingLauncher;
import com.android.settingslib.bluetooth.BluetoothUtils;
-import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
-import com.android.settingslib.bluetooth.LocalBluetoothManager;
-import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.utils.ThreadUtils;
-import com.google.common.base.Strings;
-
-import java.util.List;
+import java.nio.charset.StandardCharsets;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
-import java.util.stream.Stream;
import javax.annotation.Nullable;
@@ -58,17 +60,17 @@
private final Executor mExecutor;
private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback;
- private final LocalBluetoothManager mBluetoothManager;
+ private final AudioStreamsHelper mAudioStreamsHelper;
private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap =
new ConcurrentHashMap<>();
- private @Nullable AudioStreamsProgressCategoryPreference mCategoryPreference;
+ private AudioStreamsProgressCategoryPreference mCategoryPreference;
public AudioStreamsProgressCategoryController(Context context, String preferenceKey) {
super(context, preferenceKey);
mExecutor = Executors.newSingleThreadExecutor();
- mBluetoothManager = Utils.getLocalBtManager(mContext);
- mLeBroadcastAssistant = getLeBroadcastAssistant(mBluetoothManager);
+ mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(mContext));
+ mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
mBroadcastAssistantCallback = new AudioStreamsBroadcastAssistantCallback(this);
}
@@ -104,14 +106,10 @@
// Display currently connected streams
var unused =
ThreadUtils.postOnBackgroundThread(
- () -> {
- for (var sink :
- getActiveSinksOnAssistant(mBluetoothManager)) {
- mLeBroadcastAssistant
- .getAllSources(sink)
- .forEach(this::addSourceConnected);
- }
- });
+ () ->
+ mAudioStreamsHelper
+ .getAllSources()
+ .forEach(this::handleSourceConnected));
});
}
@@ -140,31 +138,36 @@
});
}
- void addSourceFound(BluetoothLeBroadcastMetadata source) {
- Preference.OnPreferenceClickListener onClickListener =
+ void handleSourceFound(BluetoothLeBroadcastMetadata source) {
+ Preference.OnPreferenceClickListener addSourceOrShowDialog =
preference -> {
if (DEBUG) {
Log.d(TAG, "preferenceClicked(): attempt to join broadcast");
}
-
- // TODO(chelseahao): add source to sink
+ if (source.isEncrypted()) {
+ ThreadUtils.postOnMainThread(
+ () -> launchPasswordDialog(source, preference));
+ } else {
+ mAudioStreamsHelper.addSource(source);
+ }
return true;
};
mBroadcastIdToPreferenceMap.computeIfAbsent(
source.getBroadcastId(),
k -> {
- var p = createPreference(source, onClickListener);
+ var preference = AudioStreamPreference.fromMetadata(mContext, source);
ThreadUtils.postOnMainThread(
() -> {
+ preference.setIsConnected(false, addSourceOrShowDialog);
if (mCategoryPreference != null) {
- mCategoryPreference.addPreference(p);
+ mCategoryPreference.addPreference(preference);
}
});
- return p;
+ return preference;
});
}
- void removeSourceLost(int broadcastId) {
+ void handleSourceLost(int broadcastId) {
var toRemove = mBroadcastIdToPreferenceMap.remove(broadcastId);
if (toRemove != null) {
ThreadUtils.postOnMainThread(
@@ -174,92 +177,80 @@
}
});
}
- // TODO(chelseahao): remove source from sink
+ mAudioStreamsHelper.removeSource();
}
- private void addSourceConnected(BluetoothLeBroadcastReceiveState state) {
+ void handleSourceConnected(BluetoothLeBroadcastReceiveState state) {
+ // TODO(chelseahao): only continue when the state indicates a successful connection
mBroadcastIdToPreferenceMap.compute(
state.getBroadcastId(),
(k, v) -> {
- if (v == null) {
- // Create a new preference as the source has not been added.
- var p = createPreference(state);
- ThreadUtils.postOnMainThread(
- () -> {
- if (mCategoryPreference != null) {
- mCategoryPreference.addPreference(p);
- }
- });
- return p;
- } else {
- // This source has been added either by scanning, or it's currently
- // connected to another active sink. Update its connection status to true
- // if needed.
- ThreadUtils.postOnMainThread(() -> v.setIsConnected(true, null));
- return v;
- }
+ // True if this source has been added either by scanning, or it's currently
+ // connected to another active sink.
+ boolean existed = v != null;
+ AudioStreamPreference preference =
+ existed ? v : AudioStreamPreference.fromReceiveState(mContext, state);
+
+ ThreadUtils.postOnMainThread(
+ () -> {
+ preference.setIsConnected(
+ true, p -> launchDetailFragment((AudioStreamPreference) p));
+ if (mCategoryPreference != null && !existed) {
+ mCategoryPreference.addPreference(preference);
+ }
+ });
+
+ return preference;
});
}
- private AudioStreamPreference createPreference(
- BluetoothLeBroadcastMetadata source,
- Preference.OnPreferenceClickListener onPreferenceClickListener) {
- AudioStreamPreference preference = new AudioStreamPreference(mContext, /* attrs= */ null);
- preference.setTitle(
- source.getSubgroups().stream()
- .map(s -> s.getContentMetadata().getProgramInfo())
- .filter(i -> !Strings.isNullOrEmpty(i))
- .findFirst()
- .orElse("Broadcast Id: " + source.getBroadcastId()));
- preference.setIsConnected(false, onPreferenceClickListener);
- return preference;
- }
-
- private AudioStreamPreference createPreference(BluetoothLeBroadcastReceiveState state) {
- AudioStreamPreference preference = new AudioStreamPreference(mContext, /* attrs= */ null);
- preference.setTitle(
- state.getSubgroupMetadata().stream()
- .map(BluetoothLeAudioContentMetadata::getProgramInfo)
- .filter(i -> !Strings.isNullOrEmpty(i))
- .findFirst()
- .orElse("Broadcast Id: " + state.getBroadcastId()));
- preference.setIsConnected(true, null);
- return preference;
- }
-
- private static List<BluetoothDevice> getActiveSinksOnAssistant(LocalBluetoothManager manager) {
- if (manager == null) {
- Log.w(TAG, "getActiveSinksOnAssistant(): LocalBluetoothManager is null!");
- return emptyList();
- }
- return AudioSharingUtils.getActiveSinkOnAssistant(manager)
- .map(
- cachedBluetoothDevice ->
- Stream.concat(
- Stream.of(cachedBluetoothDevice.getDevice()),
- cachedBluetoothDevice.getMemberDevice().stream()
- .map(CachedBluetoothDevice::getDevice))
- .toList())
- .orElse(emptyList());
- }
-
- private static @Nullable LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant(
- LocalBluetoothManager manager) {
- if (manager == null) {
- Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothManager is null!");
- return null;
- }
-
- LocalBluetoothProfileManager profileManager = manager.getProfileManager();
- if (profileManager == null) {
- Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothProfileManager is null!");
- return null;
- }
-
- return profileManager.getLeAudioBroadcastAssistantProfile();
- }
-
void showToast(String msg) {
AudioSharingUtils.toastMessage(mContext, msg);
}
+
+ private boolean launchDetailFragment(AudioStreamPreference preference) {
+ Bundle broadcast = new Bundle();
+ broadcast.putString(
+ Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO,
+ (String) preference.getTitle());
+
+ new SubSettingLauncher(mContext)
+ .setTitleText("Audio stream details")
+ .setDestination(AudioStreamDetailsFragment.class.getName())
+ // TODO(chelseahao): Add logging enum
+ .setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN)
+ .setArguments(broadcast)
+ .launch();
+ return true;
+ }
+
+ private void launchPasswordDialog(BluetoothLeBroadcastMetadata source, Preference preference) {
+ View layout =
+ LayoutInflater.from(mContext)
+ .inflate(R.layout.bluetooth_find_broadcast_password_dialog, null);
+ ((TextView) layout.requireViewById(R.id.broadcast_name_text))
+ .setText(preference.getTitle());
+ AlertDialog alertDialog =
+ new AlertDialog.Builder(mContext)
+ .setTitle(R.string.find_broadcast_password_dialog_title)
+ .setView(layout)
+ .setNeutralButton(android.R.string.cancel, null)
+ .setPositiveButton(
+ R.string.bluetooth_connect_access_dialog_positive,
+ (dialog, which) -> {
+ var code =
+ ((EditText)
+ layout.requireViewById(
+ R.id.broadcast_edit_text))
+ .getText()
+ .toString();
+ mAudioStreamsHelper.addSource(
+ new BluetoothLeBroadcastMetadata.Builder(source)
+ .setBroadcastCode(
+ code.getBytes(StandardCharsets.UTF_8))
+ .build());
+ })
+ .create();
+ alertDialog.show();
+ }
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java
index 12b46e5..549e725 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java
@@ -16,6 +16,7 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
@@ -41,10 +42,10 @@
public class AudioStreamsScanQrCodeController extends BasePreferenceController
implements DefaultLifecycleObserver {
+ static final int REQUEST_SCAN_BT_BROADCAST_QR_CODE = 0;
private static final String TAG = "AudioStreamsProgressCategoryController";
private static final boolean DEBUG = BluetoothUtils.D;
private static final String KEY = "audio_streams_scan_qr_code";
- private static final int REQUEST_SCAN_BT_BROADCAST_QR_CODE = 0;
private final BluetoothCallback mBluetoothCallback =
new BluetoothCallback() {
@Override
@@ -57,12 +58,14 @@
};
private final LocalBluetoothManager mLocalBtManager;
+ private final AudioStreamsHelper mAudioStreamsHelper;
private AudioStreamsDashboardFragment mFragment;
private Preference mPreference;
public AudioStreamsScanQrCodeController(Context context, String preferenceKey) {
super(context, preferenceKey);
mLocalBtManager = Utils.getLocalBtManager(mContext);
+ mAudioStreamsHelper = new AudioStreamsHelper(mLocalBtManager);
}
public void setFragment(AudioStreamsDashboardFragment fragment) {
@@ -121,6 +124,10 @@
});
}
+ void addSource(BluetoothLeBroadcastMetadata source) {
+ mAudioStreamsHelper.addSource(source);
+ }
+
private void updateVisibility() {
ThreadUtils.postOnBackgroundThread(
() -> {
diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java
index 6d1d4e8..d68f2c8 100644
--- a/src/com/android/settings/core/gateway/SettingsGateway.java
+++ b/src/com/android/settings/core/gateway/SettingsGateway.java
@@ -100,6 +100,7 @@
import com.android.settings.deviceinfo.legal.ModuleLicensesDashboard;
import com.android.settings.display.AutoBrightnessSettings;
import com.android.settings.display.NightDisplaySettings;
+import com.android.settings.display.ScreenTimeoutSettings;
import com.android.settings.display.SmartAutoRotatePreferenceFragment;
import com.android.settings.display.darkmode.DarkModeSettingsFragment;
import com.android.settings.dream.DreamSettings;
@@ -369,7 +370,8 @@
LongBackgroundTasksDetails.class.getName(),
RegionalPreferencesEntriesFragment.class.getName(),
BatteryInfoFragment.class.getName(),
- UserAspectRatioDetails.class.getName()
+ UserAspectRatioDetails.class.getName(),
+ ScreenTimeoutSettings.class.getName(),
};
public static final String[] SETTINGS_FOR_RESTRICTED = {
diff --git a/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java b/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java
index 3321d50..0348e11 100644
--- a/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java
+++ b/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java
@@ -130,7 +130,6 @@
if (titleResId > 0) {
activity.setTitle(titleResId);
}
- final int selectedTab = getTabId(activity, getArguments());
final View tabContainer = mContentView.findViewById(R.id.tab_container);
mViewPager = tabContainer.findViewById(R.id.view_pager);
@@ -149,6 +148,7 @@
}
);
tabContainer.setVisibility(View.VISIBLE);
+ final int selectedTab = getTabId(activity, getArguments());
final TabLayout.Tab tab = tabs.getTabAt(selectedTab);
tab.select();
@@ -228,7 +228,7 @@
if (bundle != null) {
final int extraTab = bundle.getInt(SettingsActivity.EXTRA_SHOW_FRAGMENT_TAB, -1);
if (extraTab != -1) {
- return extraTab;
+ return ((ViewPagerAdapter) mViewPager.getAdapter()).getTabForPosition(extraTab);
}
final int userId = bundle.getInt(EXTRA_USER_ID, UserHandle.SYSTEM.getIdentifier());
final boolean isWorkProfile = UserManager.get(activity).isManagedProfile(userId);
diff --git a/src/com/android/settings/development/BackAnimationPreferenceController.java b/src/com/android/settings/development/BackAnimationPreferenceController.java
index aa4faf5..95ffc25 100644
--- a/src/com/android/settings/development/BackAnimationPreferenceController.java
+++ b/src/com/android/settings/development/BackAnimationPreferenceController.java
@@ -16,6 +16,8 @@
package com.android.settings.development;
+import static com.android.window.flags.Flags.predictiveBackSystemAnimations;
+
import android.content.Context;
import android.provider.Settings;
@@ -56,6 +58,11 @@
}
@Override
+ public boolean isAvailable() {
+ return !predictiveBackSystemAnimations();
+ }
+
+ @Override
public String getPreferenceKey() {
return BACK_NAVIGATION_ANIMATION_KEY;
}
diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
index 4c8b2dc..a483f9f 100644
--- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
+++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
@@ -49,6 +49,7 @@
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.Toast;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
@@ -606,8 +607,9 @@
}
private static List<AbstractPreferenceController> buildPreferenceControllers(Context context,
- Activity activity, Lifecycle lifecycle, DevelopmentSettingsDashboardFragment fragment,
- BluetoothA2dpConfigStore bluetoothA2dpConfigStore) {
+ @Nullable Activity activity, @Nullable Lifecycle lifecycle,
+ @Nullable DevelopmentSettingsDashboardFragment fragment,
+ @Nullable BluetoothA2dpConfigStore bluetoothA2dpConfigStore) {
final List<AbstractPreferenceController> controllers = new ArrayList<>();
controllers.add(new MemoryUsagePreferenceController(context));
controllers.add(new BugReportPreferenceController(context));
@@ -735,7 +737,7 @@
controllers.add(new OverlaySettingsPreferenceController(context));
controllers.add(new StylusHandwritingPreferenceController(context));
controllers.add(new IngressRateLimitPreferenceController((context)));
- controllers.add(new BackAnimationPreferenceController(context, fragment));
+ // controllers.add(new BackAnimationPreferenceController(context, fragment));
controllers.add(new PhantomProcessPreferenceController(context));
controllers.add(new ContrastPreferenceController(
context, context.getSystemService(UiModeManager.class)));
diff --git a/src/com/android/settings/development/GrammaticalGenderPreferenceController.java b/src/com/android/settings/development/GrammaticalGenderPreferenceController.java
index 7b8ec65..0540974 100644
--- a/src/com/android/settings/development/GrammaticalGenderPreferenceController.java
+++ b/src/com/android/settings/development/GrammaticalGenderPreferenceController.java
@@ -95,4 +95,9 @@
listPreference.setValue(mListValues[index]);
listPreference.setSummary(mListSummaries[index]);
}
+
+ @Override
+ public boolean isAvailable() {
+ return android.app.Flags.systemTermsOfAddressEnabled();
+ }
}
diff --git a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
index 86ef3cf..23680d9 100644
--- a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
+++ b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
@@ -148,7 +148,8 @@
launchArgs.mShowTimeInformation = showTimeInformation;
if (launchArgs.mShowTimeInformation) {
launchArgs.mForegroundTimeMs = diffEntry.mForegroundUsageTimeInMs;
- launchArgs.mBackgroundTimeMs = diffEntry.mBackgroundUsageTimeInMs;
+ launchArgs.mBackgroundTimeMs =
+ diffEntry.mBackgroundUsageTimeInMs + diffEntry.mForegroundServiceUsageTimeInMs;
launchArgs.mScreenOnTimeMs = diffEntry.mScreenOnTimeInMs;
launchArgs.mAnomalyHintPrefKey = anomalyHintPrefKey;
launchArgs.mAnomalyHintText = anomalyHintText;
diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
index a2ee3e4..0bc6176 100644
--- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
+++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
@@ -22,6 +22,7 @@
import android.util.ArrayMap;
import android.util.SparseIntArray;
+import com.android.settings.fuelgauge.batteryusage.DetectRequestSourceType;
import com.android.settings.fuelgauge.batteryusage.PowerAnomalyEventList;
import com.android.settingslib.fuelgauge.Estimate;
@@ -103,11 +104,9 @@
/** Returns {@code true} if delay the hourly job when device is booting */
boolean delayHourlyJobWhenBooting();
- /** Insert settings configuration data for anomaly detection */
- void insertSettingsData(Context context, double displayDrain);
-
/** Returns {@link Bundle} for settings anomaly detection result */
- PowerAnomalyEventList detectSettingsAnomaly(Context context, double displayDrain);
+ PowerAnomalyEventList detectSettingsAnomaly(
+ Context context, double displayDrain, DetectRequestSourceType detectRequestSourceType);
/** Gets an intent for one time bypass charge limited to resume charging. */
Intent getResumeChargeIntent(boolean isDockDefender);
diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
index 2e2cf12..a8a2f75 100644
--- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
+++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
@@ -27,6 +27,7 @@
import android.util.SparseIntArray;
import com.android.internal.util.ArrayUtils;
+import com.android.settings.fuelgauge.batteryusage.DetectRequestSourceType;
import com.android.settings.fuelgauge.batteryusage.PowerAnomalyEventList;
import com.android.settingslib.fuelgauge.Estimate;
@@ -168,10 +169,8 @@
}
@Override
- public void insertSettingsData(Context context, double displayDrain) {}
-
- @Override
- public PowerAnomalyEventList detectSettingsAnomaly(Context context, double displayDrain) {
+ public PowerAnomalyEventList detectSettingsAnomaly(
+ Context context, double displayDrain, DetectRequestSourceType detectRequestSourceType) {
return null;
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java
index bad1b76..2c376e5 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java
@@ -92,6 +92,7 @@
public String mLegacyLabel;
public int mConsumerType;
public long mForegroundUsageTimeInMs;
+ public long mForegroundServiceUsageTimeInMs;
public long mBackgroundUsageTimeInMs;
public long mScreenOnTimeInMs;
public double mConsumePower;
@@ -125,6 +126,7 @@
String legacyLabel,
int consumerType,
long foregroundUsageTimeInMs,
+ long foregroundServiceUsageTimeInMs,
long backgroundUsageTimeInMs,
long screenOnTimeInMs,
double consumePower,
@@ -142,6 +144,7 @@
mLegacyLabel = legacyLabel;
mConsumerType = consumerType;
mForegroundUsageTimeInMs = foregroundUsageTimeInMs;
+ mForegroundServiceUsageTimeInMs = foregroundServiceUsageTimeInMs;
mBackgroundUsageTimeInMs = backgroundUsageTimeInMs;
mScreenOnTimeInMs = screenOnTimeInMs;
mConsumePower = consumePower;
@@ -164,6 +167,7 @@
legacyLabel,
consumerType,
/* foregroundUsageTimeInMs= */ 0,
+ /* foregroundServiceUsageTimeInMs= */ 0,
/* backgroundUsageTimeInMs= */ 0,
/* screenOnTimeInMs= */ 0,
/* consumePower= */ 0,
@@ -232,6 +236,7 @@
this.mLegacyLabel,
this.mConsumerType,
this.mForegroundUsageTimeInMs,
+ this.mForegroundServiceUsageTimeInMs,
this.mBackgroundUsageTimeInMs,
this.mScreenOnTimeInMs,
this.mConsumePower,
@@ -515,48 +520,50 @@
@Override
public String toString() {
- final StringBuilder builder =
- new StringBuilder()
- .append("BatteryDiffEntry{")
- .append(
- String.format(
- "\n\tname=%s restrictable=%b",
- mAppLabel, mValidForRestriction))
- .append(
- String.format(
- "\n\tconsume=%.2f%% %f/%f",
- mPercentage, mConsumePower, mTotalConsumePower))
- .append(
- String.format(
- "\n\tconsume power= foreground:%f foregroundService:%f",
- mForegroundUsageConsumePower,
- mForegroundServiceUsageConsumePower))
- .append(
- String.format(
- "\n\tconsume power= background:%f cached:%f",
- mBackgroundUsageConsumePower, mCachedUsageConsumePower))
- .append(
- String.format(
- "\n\ttime= foreground:%s background:%s screen-on:%s",
- StringUtil.formatElapsedTime(
- mContext,
- (double) mForegroundUsageTimeInMs,
- /* withSeconds= */ true,
- /* collapseTimeUnit= */ false),
- StringUtil.formatElapsedTime(
- mContext,
- (double) mBackgroundUsageTimeInMs,
- /* withSeconds= */ true,
- /* collapseTimeUnit= */ false),
- StringUtil.formatElapsedTime(
- mContext,
- (double) mScreenOnTimeInMs,
- /* withSeconds= */ true,
- /* collapseTimeUnit= */ false)))
- .append(
- String.format(
- "\n\tpackage:%s|%s uid:%d userId:%d",
- mLegacyPackageName, getPackageName(), mUid, mUserId));
+ final StringBuilder builder = new StringBuilder();
+ builder.append("BatteryDiffEntry{");
+ builder.append(
+ String.format("\n\tname=%s restrictable=%b", mAppLabel, mValidForRestriction));
+ builder.append(
+ String.format(
+ "\n\tconsume=%.2f%% %f/%f",
+ mPercentage, mConsumePower, mTotalConsumePower));
+ builder.append(
+ String.format(
+ "\n\tconsume power= foreground:%f foregroundService:%f",
+ mForegroundUsageConsumePower, mForegroundServiceUsageConsumePower));
+ builder.append(
+ String.format(
+ "\n\tconsume power= background:%f cached:%f",
+ mBackgroundUsageConsumePower, mCachedUsageConsumePower));
+ builder.append(
+ String.format(
+ "\n\ttime= foreground:%s foregroundService:%s "
+ + "background:%s screen-on:%s",
+ StringUtil.formatElapsedTime(
+ mContext,
+ (double) mForegroundUsageTimeInMs,
+ /* withSeconds= */ true,
+ /* collapseTimeUnit= */ false),
+ StringUtil.formatElapsedTime(
+ mContext,
+ (double) mForegroundServiceUsageTimeInMs,
+ /* withSeconds= */ true,
+ /* collapseTimeUnit= */ false),
+ StringUtil.formatElapsedTime(
+ mContext,
+ (double) mBackgroundUsageTimeInMs,
+ /* withSeconds= */ true,
+ /* collapseTimeUnit= */ false),
+ StringUtil.formatElapsedTime(
+ mContext,
+ (double) mScreenOnTimeInMs,
+ /* withSeconds= */ true,
+ /* collapseTimeUnit= */ false)));
+ builder.append(
+ String.format(
+ "\n\tpackage:%s|%s uid:%d userId:%d",
+ mLegacyPackageName, getPackageName(), mUid, mUserId));
return builder.toString();
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java
index 751e7ad..4b65cc9 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java
@@ -111,6 +111,7 @@
@BatteryConsumer.PowerComponent private final int mPowerComponentId;
private long mUsageDurationMs;
private long mTimeInForegroundMs;
+ private long mTimeInForegroundServiceMs;
private long mTimeInBackgroundMs;
public String mName;
@@ -188,9 +189,14 @@
}
}
mTimeInForegroundMs =
- uidBatteryConsumer.getTimeInStateMs(UidBatteryConsumer.STATE_FOREGROUND);
+ uidBatteryConsumer.getTimeInProcessStateMs(
+ UidBatteryConsumer.PROCESS_STATE_FOREGROUND);
+ mTimeInForegroundServiceMs =
+ uidBatteryConsumer.getTimeInProcessStateMs(
+ UidBatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE);
mTimeInBackgroundMs =
- uidBatteryConsumer.getTimeInStateMs(UidBatteryConsumer.STATE_BACKGROUND);
+ uidBatteryConsumer.getTimeInProcessStateMs(
+ UidBatteryConsumer.PROCESS_STATE_BACKGROUND);
mConsumedPowerInForeground =
safeGetConsumedPower(
uidBatteryConsumer, BATTERY_DIMENSIONS[BATTERY_USAGE_INDEX_FOREGROUND]);
@@ -433,20 +439,19 @@
/** Returns foreground time/ms that is attributed to this entry. */
public long getTimeInForegroundMs() {
- if (mBatteryConsumer instanceof UidBatteryConsumer) {
- return mTimeInForegroundMs;
- } else {
- return mUsageDurationMs;
- }
+ return (mBatteryConsumer instanceof UidBatteryConsumer)
+ ? mTimeInForegroundMs
+ : mUsageDurationMs;
+ }
+
+ /** Returns foreground service time/ms that is attributed to this entry. */
+ public long getTimeInForegroundServiceMs() {
+ return (mBatteryConsumer instanceof UidBatteryConsumer) ? mTimeInForegroundServiceMs : 0;
}
/** Returns background activity time/ms that is attributed to this entry. */
public long getTimeInBackgroundMs() {
- if (mBatteryConsumer instanceof UidBatteryConsumer) {
- return mTimeInBackgroundMs;
- } else {
- return 0;
- }
+ return (mBatteryConsumer instanceof UidBatteryConsumer) ? mTimeInBackgroundMs : 0;
}
/** Returns total amount of power (in milli-amp-hours) that is attributed to this entry. */
@@ -510,9 +515,14 @@
if (batteryConsumer instanceof UidBatteryConsumer) {
UidBatteryConsumer uidBatteryConsumer = (UidBatteryConsumer) batteryConsumer;
mTimeInForegroundMs +=
- uidBatteryConsumer.getTimeInStateMs(UidBatteryConsumer.STATE_FOREGROUND);
+ uidBatteryConsumer.getTimeInProcessStateMs(
+ UidBatteryConsumer.PROCESS_STATE_FOREGROUND);
+ mTimeInForegroundServiceMs +=
+ uidBatteryConsumer.getTimeInProcessStateMs(
+ UidBatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE);
mTimeInBackgroundMs +=
- uidBatteryConsumer.getTimeInStateMs(UidBatteryConsumer.STATE_BACKGROUND);
+ uidBatteryConsumer.getTimeInProcessStateMs(
+ UidBatteryConsumer.PROCESS_STATE_BACKGROUND);
mConsumedPowerInForeground +=
safeGetConsumedPower(
uidBatteryConsumer, BATTERY_DIMENSIONS[BATTERY_USAGE_INDEX_FOREGROUND]);
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntry.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntry.java
index 97cdc34..b42d373 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntry.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntry.java
@@ -20,8 +20,6 @@
import android.os.BatteryConsumer;
import android.util.Log;
-import java.time.Duration;
-
/** A container class to carry data from {@link ContentValues}. */
public class BatteryHistEntry {
private static final boolean DEBUG = false;
@@ -57,6 +55,7 @@
public final double mCachedUsageConsumePower;
public final double mPercentOfTotal;
public final long mForegroundUsageTimeInMs;
+ public final long mForegroundServiceUsageTimeInMs;
public final long mBackgroundUsageTimeInMs;
@BatteryConsumer.PowerComponent public final int mDrainType;
@ConvertUtils.ConsumerType public final int mConsumerType;
@@ -89,6 +88,7 @@
mCachedUsageConsumePower = batteryInformation.getCachedUsageConsumePower();
mPercentOfTotal = batteryInformation.getPercentOfTotal();
mForegroundUsageTimeInMs = batteryInformation.getForegroundUsageTimeInMs();
+ mForegroundServiceUsageTimeInMs = batteryInformation.getForegroundServiceUsageTimeInMs();
mBackgroundUsageTimeInMs = batteryInformation.getBackgroundUsageTimeInMs();
mDrainType = batteryInformation.getDrainType();
final DeviceBatteryState deviceBatteryState = batteryInformation.getDeviceBatteryState();
@@ -118,6 +118,7 @@
mCachedUsageConsumePower = batteryInformation.getCachedUsageConsumePower();
mPercentOfTotal = batteryInformation.getPercentOfTotal();
mForegroundUsageTimeInMs = batteryInformation.getForegroundUsageTimeInMs();
+ mForegroundServiceUsageTimeInMs = batteryInformation.getForegroundServiceUsageTimeInMs();
mBackgroundUsageTimeInMs = batteryInformation.getBackgroundUsageTimeInMs();
mDrainType = batteryInformation.getDrainType();
final DeviceBatteryState deviceBatteryState = batteryInformation.getDeviceBatteryState();
@@ -137,6 +138,7 @@
double backgroundUsageConsumePower,
double cachedUsageConsumePower,
long foregroundUsageTimeInMs,
+ long foregroundServiceUsageTimeInMs,
long backgroundUsageTimeInMs,
int batteryLevel) {
mUid = fromEntry.mUid;
@@ -155,6 +157,7 @@
mCachedUsageConsumePower = cachedUsageConsumePower;
mPercentOfTotal = fromEntry.mPercentOfTotal;
mForegroundUsageTimeInMs = foregroundUsageTimeInMs;
+ mForegroundServiceUsageTimeInMs = foregroundServiceUsageTimeInMs;
mBackgroundUsageTimeInMs = backgroundUsageTimeInMs;
mDrainType = fromEntry.mDrainType;
mConsumerType = fromEntry.mConsumerType;
@@ -189,45 +192,40 @@
@Override
public String toString() {
final String recordAtDateTime = ConvertUtils.utcToLocalTimeForLogging(mTimestamp);
- final StringBuilder builder =
- new StringBuilder()
- .append("\nBatteryHistEntry{")
- .append(
- String.format(
- "\n\tpackage=%s|label=%s|uid=%d|userId=%d|isHidden=%b",
- mPackageName, mAppLabel, mUid, mUserId, mIsHidden))
- .append(
- String.format(
- "\n\ttimestamp=%s|zoneId=%s|bootTimestamp=%d",
- recordAtDateTime,
- mZoneId,
- Duration.ofMillis(mBootTimestamp).getSeconds()))
- .append(
- String.format(
- "\n\tusage=%f|total=%f|consume=%f",
- mPercentOfTotal, mTotalPower, mConsumePower))
- .append(
- String.format(
- "\n\tforeground=%f|foregroundService=%f",
- mForegroundUsageConsumePower,
- mForegroundServiceUsageConsumePower))
- .append(
- String.format(
- "\n\tbackground=%f|cached=%f",
- mBackgroundUsageConsumePower, mCachedUsageConsumePower))
- .append(
- String.format(
- "\n\telapsedTime=%d|%d",
- Duration.ofMillis(mForegroundUsageTimeInMs).getSeconds(),
- Duration.ofMillis(mBackgroundUsageTimeInMs).getSeconds()))
- .append(
- String.format(
- "\n\tdrainType=%d|consumerType=%d",
- mDrainType, mConsumerType))
- .append(
- String.format(
- "\n\tbattery=%d|status=%d|health=%d\n}",
- mBatteryLevel, mBatteryStatus, mBatteryHealth));
+ final StringBuilder builder = new StringBuilder();
+ builder.append("\nBatteryHistEntry{");
+ builder.append(
+ String.format(
+ "\n\tpackage=%s|label=%s|uid=%d|userId=%d|isHidden=%b",
+ mPackageName, mAppLabel, mUid, mUserId, mIsHidden));
+ builder.append(
+ String.format(
+ "\n\ttimestamp=%s|zoneId=%s|bootTimestamp=%d",
+ recordAtDateTime, mZoneId, TimestampUtils.getSeconds(mBootTimestamp)));
+ builder.append(
+ String.format(
+ "\n\tusage=%f|total=%f|consume=%f",
+ mPercentOfTotal, mTotalPower, mConsumePower));
+ builder.append(
+ String.format(
+ "\n\tforeground=%f|foregroundService=%f",
+ mForegroundUsageConsumePower, mForegroundServiceUsageConsumePower));
+ builder.append(
+ String.format(
+ "\n\tbackground=%f|cached=%f",
+ mBackgroundUsageConsumePower, mCachedUsageConsumePower));
+ builder.append(
+ String.format(
+ "\n\telapsedTime,fg=%d|fgs=%d|bg=%d",
+ TimestampUtils.getSeconds(mBackgroundUsageTimeInMs),
+ TimestampUtils.getSeconds(mForegroundServiceUsageTimeInMs),
+ TimestampUtils.getSeconds(mBackgroundUsageTimeInMs)));
+ builder.append(
+ String.format("\n\tdrainType=%d|consumerType=%d", mDrainType, mConsumerType));
+ builder.append(
+ String.format(
+ "\n\tbattery=%d|status=%d|health=%d\n}",
+ mBatteryLevel, mBatteryStatus, mBatteryHealth));
return builder.toString();
}
@@ -323,19 +321,20 @@
ratio);
final double foregroundUsageTimeInMs =
interpolate(
- (double)
- (lowerHistEntry == null
- ? 0
- : lowerHistEntry.mForegroundUsageTimeInMs),
- (double) upperHistEntry.mForegroundUsageTimeInMs,
+ (lowerHistEntry == null ? 0 : lowerHistEntry.mForegroundUsageTimeInMs),
+ upperHistEntry.mForegroundUsageTimeInMs,
+ ratio);
+ final double foregroundServiceUsageTimeInMs =
+ interpolate(
+ (lowerHistEntry == null
+ ? 0
+ : lowerHistEntry.mForegroundServiceUsageTimeInMs),
+ upperHistEntry.mForegroundServiceUsageTimeInMs,
ratio);
final double backgroundUsageTimeInMs =
interpolate(
- (double)
- (lowerHistEntry == null
- ? 0
- : lowerHistEntry.mBackgroundUsageTimeInMs),
- (double) upperHistEntry.mBackgroundUsageTimeInMs,
+ (lowerHistEntry == null ? 0 : lowerHistEntry.mBackgroundUsageTimeInMs),
+ upperHistEntry.mBackgroundUsageTimeInMs,
ratio);
// Checks whether there is any abnormal cases!
if (upperHistEntry.mConsumePower < consumePower
@@ -345,6 +344,7 @@
|| upperHistEntry.mBackgroundUsageConsumePower < backgroundUsageConsumePower
|| upperHistEntry.mCachedUsageConsumePower < cachedUsageConsumePower
|| upperHistEntry.mForegroundUsageTimeInMs < foregroundUsageTimeInMs
+ || upperHistEntry.mForegroundServiceUsageTimeInMs < foregroundServiceUsageTimeInMs
|| upperHistEntry.mBackgroundUsageTimeInMs < backgroundUsageTimeInMs) {
if (DEBUG) {
Log.w(
@@ -371,6 +371,7 @@
backgroundUsageConsumePower,
cachedUsageConsumePower,
Math.round(foregroundUsageTimeInMs),
+ Math.round(foregroundServiceUsageTimeInMs),
Math.round(backgroundUsageTimeInMs),
(int) Math.round(batteryLevel));
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownController.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownController.java
index d6f8709..0ffd090 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownController.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownController.java
@@ -400,7 +400,7 @@
mPrefContext,
entry.isSystemEntry(),
entry.mForegroundUsageTimeInMs,
- entry.mBackgroundUsageTimeInMs,
+ entry.mBackgroundUsageTimeInMs + entry.mForegroundServiceUsageTimeInMs,
entry.mScreenOnTimeInMs));
}
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java
index ead580b..fb5b9a1 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java
@@ -135,7 +135,10 @@
.isEmpty()))) {
FeatureFactory.getFeatureFactory()
.getPowerUsageFeatureProvider()
- .detectSettingsAnomaly(context, /* displayDrain= */ 0);
+ .detectSettingsAnomaly(
+ context,
+ /* displayDrain= */ 0,
+ DetectRequestSourceType.TYPE_DATA_LOADER);
}
});
if (batteryLevelData == null) {
diff --git a/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java b/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
index ae93734..e23e219 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
@@ -544,6 +544,7 @@
batteryUsageDiff.getLabel(),
batteryUsageDiff.getConsumerType(),
batteryUsageDiff.getForegroundUsageTime(),
+ batteryUsageDiff.getForegroundServiceUsageTime(),
batteryUsageDiff.getBackgroundUsageTime(),
batteryUsageDiff.getScreenOnTime(),
batteryUsageDiff.getConsumePower(),
@@ -612,6 +613,7 @@
.setPercentOfTotal(entry.mPercent)
.setDrainType(entry.getPowerComponentId())
.setForegroundUsageTimeInMs(entry.getTimeInForegroundMs())
+ .setForegroundServiceUsageTimeInMs(entry.getTimeInForegroundServiceMs())
.setBackgroundUsageTimeInMs(entry.getTimeInBackgroundMs());
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java
index d8f0a77..2ef12f1 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java
@@ -675,6 +675,7 @@
entry.mAppLabel,
entry.mConsumerType,
entry.mForegroundUsageTimeInMs,
+ entry.mForegroundServiceUsageTimeInMs,
entry.mBackgroundUsageTimeInMs,
/* screenOnTimeInMs= */ 0,
entry.mConsumePower,
@@ -1412,6 +1413,7 @@
// Cumulative values is a specific time slot for a specific app.
long foregroundUsageTimeInMs = 0;
+ long foregroundServiceUsageTimeInMs = 0;
long backgroundUsageTimeInMs = 0;
double consumePower = 0;
double foregroundUsageConsumePower = 0;
@@ -1425,6 +1427,10 @@
getDiffValue(
currentEntry.mForegroundUsageTimeInMs,
nextEntry.mForegroundUsageTimeInMs);
+ foregroundServiceUsageTimeInMs +=
+ getDiffValue(
+ currentEntry.mForegroundServiceUsageTimeInMs,
+ nextEntry.mForegroundServiceUsageTimeInMs);
backgroundUsageTimeInMs +=
getDiffValue(
currentEntry.mBackgroundUsageTimeInMs,
@@ -1453,24 +1459,32 @@
foregroundUsageTimeInMs = slotScreenOnTime;
}
// Excludes entry since we don't have enough data to calculate.
- if (foregroundUsageTimeInMs == 0 && backgroundUsageTimeInMs == 0 && consumePower == 0) {
+ if (foregroundUsageTimeInMs == 0
+ && foregroundServiceUsageTimeInMs == 0
+ && backgroundUsageTimeInMs == 0
+ && consumePower == 0) {
continue;
}
// Forces refine the cumulative value since it may introduce deviation error since we
// will apply the interpolation arithmetic.
- final float totalUsageTimeInMs = foregroundUsageTimeInMs + backgroundUsageTimeInMs;
+ final float totalUsageTimeInMs =
+ foregroundUsageTimeInMs
+ + backgroundUsageTimeInMs
+ + foregroundServiceUsageTimeInMs;
if (totalUsageTimeInMs > slotDuration) {
final float ratio = slotDuration / totalUsageTimeInMs;
if (sDebug) {
Log.w(
TAG,
String.format(
- "abnormal usage time %d|%d for:\n%s",
+ "abnormal usage time %d|%d|%d for:\n%s",
Duration.ofMillis(foregroundUsageTimeInMs).getSeconds(),
+ Duration.ofMillis(foregroundServiceUsageTimeInMs).getSeconds(),
Duration.ofMillis(backgroundUsageTimeInMs).getSeconds(),
selectedBatteryEntry));
}
foregroundUsageTimeInMs = Math.round(foregroundUsageTimeInMs * ratio);
+ foregroundServiceUsageTimeInMs = Math.round(foregroundServiceUsageTimeInMs * ratio);
backgroundUsageTimeInMs = Math.round(backgroundUsageTimeInMs * ratio);
consumePower = consumePower * ratio;
foregroundUsageConsumePower = foregroundUsageConsumePower * ratio;
@@ -1487,9 +1501,14 @@
appUsageMap,
selectedBatteryEntry.mUserId,
selectedBatteryEntry.mPackageName));
- // Make sure the background + screen-on time will not exceed the threshold.
+ // Ensure the following value will not exceed the threshold.
+ // value = background + foregroundService + screen-on
backgroundUsageTimeInMs =
Math.min(backgroundUsageTimeInMs, (long) slotDuration - screenOnTime);
+ foregroundServiceUsageTimeInMs =
+ Math.min(
+ foregroundServiceUsageTimeInMs,
+ (long) slotDuration - screenOnTime - backgroundUsageTimeInMs);
final BatteryDiffEntry currentBatteryDiffEntry =
new BatteryDiffEntry(
context,
@@ -1502,6 +1521,7 @@
selectedBatteryEntry.mAppLabel,
selectedBatteryEntry.mConsumerType,
foregroundUsageTimeInMs,
+ foregroundServiceUsageTimeInMs,
backgroundUsageTimeInMs,
screenOnTime,
consumePower,
@@ -1647,6 +1667,8 @@
} else {
// Sums up some field data into the existing one.
oldBatteryDiffEntry.mForegroundUsageTimeInMs += entry.mForegroundUsageTimeInMs;
+ oldBatteryDiffEntry.mForegroundServiceUsageTimeInMs +=
+ entry.mForegroundServiceUsageTimeInMs;
oldBatteryDiffEntry.mBackgroundUsageTimeInMs += entry.mBackgroundUsageTimeInMs;
oldBatteryDiffEntry.mScreenOnTimeInMs += entry.mScreenOnTimeInMs;
oldBatteryDiffEntry.mConsumePower += entry.mConsumePower;
diff --git a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
index 7160da4..ee0e449 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
@@ -660,36 +660,39 @@
// Creates the ContentValues list to insert them into provider.
final List<ContentValues> valuesList = new ArrayList<>();
if (batteryEntryList != null) {
- batteryEntryList.stream()
- .filter(
- entry -> {
- final long foregroundMs = entry.getTimeInForegroundMs();
- final long backgroundMs = entry.getTimeInBackgroundMs();
- if (entry.getConsumedPower() == 0
- && (foregroundMs != 0 || backgroundMs != 0)) {
- Log.w(
- TAG,
- String.format(
- "no consumed power but has running time for %s"
- + " time=%d|%d",
- entry.getLabel(), foregroundMs, backgroundMs));
- }
- return entry.getConsumedPower() != 0
- || foregroundMs != 0
- || backgroundMs != 0;
- })
- .forEach(
- entry ->
- valuesList.add(
- ConvertUtils.convertBatteryEntryToContentValues(
- entry,
- batteryUsageStats,
- batteryLevel,
- batteryStatus,
- batteryHealth,
- snapshotBootTimestamp,
- snapshotTimestamp,
- isFullChargeStart)));
+ for (BatteryEntry entry : batteryEntryList) {
+ final long foregroundMs = entry.getTimeInForegroundMs();
+ final long foregroundServiceMs = entry.getTimeInForegroundServiceMs();
+ final long backgroundMs = entry.getTimeInBackgroundMs();
+ if (entry.getConsumedPower() == 0
+ && (foregroundMs != 0 || foregroundServiceMs != 0 || backgroundMs != 0)) {
+ Log.w(
+ TAG,
+ String.format(
+ "no consumed power but has running time for %s"
+ + " time=%d|%d|%d",
+ entry.getLabel(),
+ foregroundMs,
+ foregroundServiceMs,
+ backgroundMs));
+ }
+ if (entry.getConsumedPower() == 0
+ && foregroundMs == 0
+ && foregroundServiceMs == 0
+ && backgroundMs == 0) {
+ continue;
+ }
+ valuesList.add(
+ ConvertUtils.convertBatteryEntryToContentValues(
+ entry,
+ batteryUsageStats,
+ batteryLevel,
+ batteryStatus,
+ batteryHealth,
+ snapshotBootTimestamp,
+ snapshotTimestamp,
+ isFullChargeStart));
+ }
}
int size = 1;
diff --git a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvanced.java b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvanced.java
index 072040d..1482117 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvanced.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvanced.java
@@ -263,7 +263,9 @@
FeatureFactory.getFeatureFactory().getPowerUsageFeatureProvider();
final PowerAnomalyEventList anomalyEventList =
powerUsageFeatureProvider.detectSettingsAnomaly(
- getContext(), /* displayDrain= */ 0);
+ getContext(),
+ /* displayDrain= */ 0,
+ DetectRequestSourceType.TYPE_USAGE_UI);
mHandler.post(() -> onAnomalyDetected(anomalyEventList));
});
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/TimestampUtils.java b/src/com/android/settings/fuelgauge/batteryusage/TimestampUtils.java
index 594a0ef..41a2254 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/TimestampUtils.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/TimestampUtils.java
@@ -16,6 +16,7 @@
package com.android.settings.fuelgauge.batteryusage;
+import java.time.Duration;
import java.util.Calendar;
/** A utility class for timestamp operations. */
@@ -48,6 +49,10 @@
return calendar.getTimeInMillis();
}
+ static long getSeconds(final long timeInMs) {
+ return Duration.ofMillis(timeInMs).getSeconds();
+ }
+
static boolean isMidnight(final long timestamp) {
final Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(timestamp);
diff --git a/src/com/android/settings/fuelgauge/protos/battery_usage_slot.proto b/src/com/android/settings/fuelgauge/protos/battery_usage_slot.proto
index 5bc1a3e..7f67770 100644
--- a/src/com/android/settings/fuelgauge/protos/battery_usage_slot.proto
+++ b/src/com/android/settings/fuelgauge/protos/battery_usage_slot.proto
@@ -31,4 +31,5 @@
optional int64 foreground_usage_time = 14;
optional int64 background_usage_time = 15;
optional int64 screen_on_time = 16;
+ optional int64 foreground_service_usage_time = 17;
}
diff --git a/src/com/android/settings/fuelgauge/protos/fuelgauge_usage_state.proto b/src/com/android/settings/fuelgauge/protos/fuelgauge_usage_state.proto
index b9b05a3..d53b814 100644
--- a/src/com/android/settings/fuelgauge/protos/fuelgauge_usage_state.proto
+++ b/src/com/android/settings/fuelgauge/protos/fuelgauge_usage_state.proto
@@ -36,4 +36,5 @@
optional double foreground_service_usage_consume_power = 17;
optional double background_usage_consume_power = 18;
optional double cached_usage_consume_power = 19;
+ optional int64 foreground_service_usage_time_in_ms = 20;
}
\ No newline at end of file
diff --git a/src/com/android/settings/fuelgauge/protos/power_anomaly_event.proto b/src/com/android/settings/fuelgauge/protos/power_anomaly_event.proto
index 930a21b..3c0705f 100644
--- a/src/com/android/settings/fuelgauge/protos/power_anomaly_event.proto
+++ b/src/com/android/settings/fuelgauge/protos/power_anomaly_event.proto
@@ -21,18 +21,12 @@
optional string dismiss_record_key = 8;
}
-// NOTE: Please DO NOT delete enum items or change enum values. Use [deprecated = true] instead.
-// The enum value will be used to decide the tips card style like icons and colors.
-//
// Next id: 2
enum PowerAnomalyType{
TYPE_SETTINGS_BANNER = 0;
TYPE_APPS_ITEM = 1;
}
-// NOTE: Please DO NOT delete enum items or change enum values. Use [deprecated = true] instead.
-// The enum value will be used to decide pre-defined title and button labels.
-//
// Next id: 8
enum PowerAnomalyKey{
KEY_BRIGHTNESS = 0;
@@ -45,6 +39,13 @@
KEY_APP_FOREGROUND_HIGHER_THAN_USUAL = 7;
}
+// Next id: 3
+enum DetectRequestSourceType{
+ TYPE_UNKNOWN_SOURCE = 0;
+ TYPE_USAGE_UI = 1;
+ TYPE_DATA_LOADER = 2;
+}
+
message WarningBannerInfo {
optional string title_string = 1;
optional string description_string = 2;
diff --git a/src/com/android/settings/network/NetworkDashboardFragment.java b/src/com/android/settings/network/NetworkDashboardFragment.java
index e5d9242..323d935 100644
--- a/src/com/android/settings/network/NetworkDashboardFragment.java
+++ b/src/com/android/settings/network/NetworkDashboardFragment.java
@@ -59,6 +59,7 @@
super.onAttach(context);
use(AirplaneModePreferenceController.class).setFragment(this);
+ use(NetworkProviderCallsSmsController.class).init(this);
}
@Override
diff --git a/src/com/android/settings/network/NetworkProviderCallsSmsController.kt b/src/com/android/settings/network/NetworkProviderCallsSmsController.kt
index a265041..7346e23 100644
--- a/src/com/android/settings/network/NetworkProviderCallsSmsController.kt
+++ b/src/com/android/settings/network/NetworkProviderCallsSmsController.kt
@@ -16,35 +16,23 @@
package com.android.settings.network
-import android.app.settings.SettingsEnums
import android.content.Context
import android.content.IntentFilter
-import android.os.UserManager
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager
import androidx.annotation.VisibleForTesting
-import androidx.compose.foundation.layout.Column
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.PermPhoneMsg
-import androidx.compose.material3.HorizontalDivider
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.ui.res.stringResource
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.LifecycleOwner
+import androidx.preference.PreferenceScreen
import com.android.settings.R
-import com.android.settings.core.SubSettingLauncher
-import com.android.settings.spa.preference.ComposePreferenceController
+import com.android.settings.core.BasePreferenceController
+import com.android.settingslib.RestrictedPreference
import com.android.settingslib.Utils
-import com.android.settingslib.spa.widget.preference.PreferenceModel
-import com.android.settingslib.spa.widget.ui.SettingsIcon
+import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverFlow
import com.android.settingslib.spaprivileged.framework.common.userManager
-import com.android.settingslib.spaprivileged.framework.compose.placeholder
-import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
-import com.android.settingslib.spaprivileged.template.preference.RestrictedPreference
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@@ -64,7 +52,14 @@
SubscriptionUtil.getUniqueSubscriptionDisplayName(subInfo, context)
},
private val isInService: (Int) -> Boolean = IsInServiceImpl(context)::isInService,
-) : ComposePreferenceController(context, preferenceKey) {
+) : BasePreferenceController(context, preferenceKey) {
+
+ private lateinit var lazyViewModel: Lazy<SubscriptionInfoListViewModel>
+ private lateinit var preference: RestrictedPreference
+
+ fun init(fragment: Fragment) {
+ lazyViewModel = fragment.viewModels()
+ }
override fun getAvailabilityStatus() = when {
!SubscriptionUtil.isSimHardwareVisible(mContext) -> UNSUPPORTED_ON_DEVICE
@@ -72,35 +67,23 @@
else -> AVAILABLE
}
- @Composable
- override fun Content() {
- Column {
- CallsAndSms()
- HorizontalDivider()
- }
+ override fun displayPreference(screen: PreferenceScreen) {
+ super.displayPreference(screen)
+ preference = screen.findPreference(preferenceKey)!!
}
- @Composable
- private fun CallsAndSms() {
- val viewModel: SubscriptionInfoListViewModel = viewModel()
- val subscriptionInfos by viewModel.subscriptionInfoListFlow.collectAsStateWithLifecycle()
- val summary by remember { summaryFlow(viewModel.subscriptionInfoListFlow) }
- .collectAsStateWithLifecycle(initialValue = placeholder())
- RestrictedPreference(
- model = object : PreferenceModel {
- override val title = stringResource(R.string.calls_and_sms)
- override val icon = @Composable { SettingsIcon(Icons.Outlined.PermPhoneMsg) }
- override val summary = { summary }
- override val enabled = { subscriptionInfos.isNotEmpty() }
- override val onClick = {
- SubSettingLauncher(mContext).apply {
- setDestination(NetworkProviderCallsSmsFragment::class.qualifiedName)
- setSourceMetricsCategory(SettingsEnums.SETTINGS_NETWORK_CATEGORY)
- }.launch()
+ override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) {
+ val viewModel by lazyViewModel
+
+ summaryFlow(viewModel.subscriptionInfoListFlow)
+ .collectLatestWithLifecycle(viewLifecycleOwner) { preference.summary = it }
+
+ viewModel.subscriptionInfoListFlow
+ .collectLatestWithLifecycle(viewLifecycleOwner) { subscriptionInfoList ->
+ if (!preference.isDisabledByAdmin) {
+ preference.isEnabled = subscriptionInfoList.isNotEmpty()
}
- },
- restrictions = Restrictions(keys = listOf(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)),
- )
+ }
}
private fun summaryFlow(subscriptionInfoListFlow: Flow<List<SubscriptionInfo>>) = combine(
diff --git a/src/com/android/settings/network/SubscriptionInfoListViewModel.kt b/src/com/android/settings/network/SubscriptionInfoListViewModel.kt
index d30b21d..ee88177 100644
--- a/src/com/android/settings/network/SubscriptionInfoListViewModel.kt
+++ b/src/com/android/settings/network/SubscriptionInfoListViewModel.kt
@@ -19,8 +19,10 @@
import android.app.Application
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
+
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
+
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
@@ -32,13 +34,12 @@
class SubscriptionInfoListViewModel(application: Application) : AndroidViewModel(application) {
private val scope = viewModelScope + Dispatchers.Default
-
val subscriptionInfoListFlow = callbackFlow<List<SubscriptionInfo>> {
val subscriptionManager = application.getSystemService(SubscriptionManager::class.java)!!
val listener = object : SubscriptionManager.OnSubscriptionsChangedListener() {
override fun onSubscriptionsChanged() {
- trySend(subscriptionManager.activeSubscriptionInfoList ?: emptyList())
+ trySend(SubscriptionUtil.getActiveSubscriptions(subscriptionManager))
}
}
diff --git a/src/com/android/settings/network/SubscriptionUtil.java b/src/com/android/settings/network/SubscriptionUtil.java
index 9974ba2..e416760 100644
--- a/src/com/android/settings/network/SubscriptionUtil.java
+++ b/src/com/android/settings/network/SubscriptionUtil.java
@@ -18,6 +18,8 @@
import static android.telephony.SubscriptionManager.INVALID_SIM_SLOT_INDEX;
import static android.telephony.UiccSlotInfo.CARD_STATE_INFO_PRESENT;
+import static android.telephony.SubscriptionManager.PROFILE_CLASS_PROVISIONING;
+
import static com.android.internal.util.CollectionUtils.emptyIfNull;
import android.annotation.Nullable;
@@ -36,9 +38,11 @@
import android.text.TextUtils;
import android.util.Log;
+import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.internal.telephony.MccTable;
+import com.android.internal.telephony.flags.Flags;
import com.android.settings.R;
import com.android.settings.network.helper.SelectableSubscriptions;
import com.android.settings.network.helper.SubscriptionAnnotation;
@@ -84,6 +88,8 @@
}
public static List<SubscriptionInfo> getActiveSubscriptions(SubscriptionManager manager) {
+ //TODO (b/315499317) : Refactor the subscription utils.
+
if (sActiveResultsForTesting != null) {
return sActiveResultsForTesting;
}
@@ -94,7 +100,12 @@
if (subscriptions == null) {
return new ArrayList<>();
}
- return subscriptions;
+ // Since the SubscriptionManager.getActiveSubscriptionInfoList() has checked whether the
+ // sim visible by the SubscriptionManager.isSubscriptionVisible(), here only checks whether
+ // the esim visible here.
+ return subscriptions.stream()
+ .filter(subInfo -> subInfo != null && isEmbeddedSubscriptionVisible(subInfo))
+ .collect(Collectors.toList());
}
/**
@@ -128,7 +139,7 @@
}
/**
- * Get subscription which is available to be displayed to the user
+ * Get subscriptionInfo which is available to be displayed to the user
* per subscription id.
*
* @param context {@code Context}
@@ -138,13 +149,20 @@
* @return {@code SubscriptionInfo} based on the given subscription id. Null of subscription
* is invalid or not allowed to be displayed to the user.
*/
- public static SubscriptionInfo getAvailableSubscription(Context context,
+ public static SubscriptionInfo getAvailableSubscriptionBySubIdAndShowingForUser(Context context,
ProxySubscriptionManager subscriptionManager, int subId) {
+ //TODO (b/315499317) : Refactor the subscription utils.
final SubscriptionInfo subInfo = subscriptionManager.getAccessibleSubscriptionInfo(subId);
if (subInfo == null) {
return null;
}
+ // hide provisioning/bootstrap and satellite profiles for user
+ if (!isEmbeddedSubscriptionVisible(subInfo)) {
+ Log.d(TAG, "Do not insert the provision eSIM or NTN eSim");
+ return null;
+ }
+
final ParcelUuid groupUuid = subInfo.getGroupUuid();
if (groupUuid != null) {
@@ -567,6 +585,12 @@
public static boolean isSubscriptionVisible(
SubscriptionManager subscriptionManager, Context context, SubscriptionInfo info) {
if (info == null) return false;
+
+ // hide provisioning/bootstrap and satellite profiles for user
+ if (!isEmbeddedSubscriptionVisible(info)) {
+ return false;
+ }
+
// If subscription is NOT grouped opportunistic subscription, it's visible.
if (info.getGroupUuid() == null || !info.isOpportunistic()) return true;
@@ -786,4 +810,14 @@
}
return (currentSubInfo == null) ? null : currentSubInfo.getSubInfo();
}
+
+ private static boolean isEmbeddedSubscriptionVisible(@NonNull SubscriptionInfo subInfo) {
+ if (subInfo.isEmbedded()
+ && (subInfo.getProfileClass() == PROFILE_CLASS_PROVISIONING
+ || (Flags.oemEnabledSatelliteFlag()
+ && subInfo.isOnlyNonTerrestrialNetwork()))) {
+ return false;
+ }
+ return true;
+ }
}
diff --git a/src/com/android/settings/network/SubscriptionsPreferenceController.java b/src/com/android/settings/network/SubscriptionsPreferenceController.java
index 0c3e6bd..6601828 100644
--- a/src/com/android/settings/network/SubscriptionsPreferenceController.java
+++ b/src/com/android/settings/network/SubscriptionsPreferenceController.java
@@ -521,7 +521,7 @@
* Uses to inject function and value for class and test class.
*/
public boolean canSubscriptionBeDisplayed(Context context, int subId) {
- return (SubscriptionUtil.getAvailableSubscription(context,
+ return (SubscriptionUtil.getAvailableSubscriptionBySubIdAndShowingForUser(context,
ProxySubscriptionManager.getInstance(context), subId) != null);
}
diff --git a/src/com/android/settings/network/apn/ApnEditPageProvider.kt b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
index 5026958..bb19c59 100644
--- a/src/com/android/settings/network/apn/ApnEditPageProvider.kt
+++ b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
@@ -150,6 +150,7 @@
SettingsOutlinedTextField(
value = apnData.mmsc,
label = stringResource(R.string.apn_mmsc),
+ errorMessage = validateMMSC(apnData.mmsc, context),
enabled = apnData.mmscEnabled
) { apnData = apnData.copy(mmsc = it) }
SettingsOutlinedTextField(
diff --git a/src/com/android/settings/network/apn/ApnStatus.kt b/src/com/android/settings/network/apn/ApnStatus.kt
index d81a8e3..668ea9b 100644
--- a/src/com/android/settings/network/apn/ApnStatus.kt
+++ b/src/com/android/settings/network/apn/ApnStatus.kt
@@ -239,7 +239,12 @@
if (apnData.customizedConfig.readOnlyApn) {
return true
}
- val errorMsg = validateApnData(apnData, context)
+ var errorMsg = validateApnData(apnData, context)
+ if (errorMsg != null) {
+ //TODO: showError(this)
+ return false
+ }
+ errorMsg = validateMMSC(apnData.mmsc, context)
if (errorMsg != null) {
//TODO: showError(this)
return false
@@ -529,4 +534,9 @@
fun deleteApn(uri: Uri, context: Context) {
val contentResolver = context.contentResolver
contentResolver.delete(uri, null, null)
+}
+
+fun validateMMSC(mmsc: String, context: Context): String? {
+ return if (mmsc.matches(Regex("^https?:\\/\\/.+"))) null
+ else context.resources.getString(R.string.error_mmsc_valid)
}
\ No newline at end of file
diff --git a/src/com/android/settings/network/telephony/ConvertToEsimPreferenceController.java b/src/com/android/settings/network/telephony/ConvertToEsimPreferenceController.java
index a17144f..799543f 100644
--- a/src/com/android/settings/network/telephony/ConvertToEsimPreferenceController.java
+++ b/src/com/android/settings/network/telephony/ConvertToEsimPreferenceController.java
@@ -43,6 +43,7 @@
import com.android.internal.telephony.util.TelephonyUtils;
import com.android.settings.network.MobileNetworkRepository;
+import com.android.settings.network.SubscriptionUtil;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.mobile.dataservice.SubscriptionInfoEntity;
@@ -98,10 +99,21 @@
@Override
public int getAvailabilityStatus(int subId) {
- // TODO(b/262195754): Need the intent to enabled the feature.
+ // TODO(b/315073761) : Add a new API to set whether the profile has been
+ // converted/transferred. Remove any confusion to the user according to the set value.
+
+ /*
+ * If pSIM is set to preferred SIM and there is an active eSIM, convert the pSIM to eSIM
+ * and then disable the pSIM.
+ * This causes a dialog to switch the preferred SIM to downloaded new eSIM.
+ * This may cause confusion for the user about the seamless conversion.
+ * To avoid showing users dialogs that can cause confusion,
+ * add conditions to allow conversion in the absence of active eSIM.
+ */
if (findConversionSupportComponent()) {
return mSubscriptionInfoEntity != null && mSubscriptionInfoEntity.isActiveSubscriptionId
&& !mSubscriptionInfoEntity.isEmbedded && isActiveSubscription(subId)
+ && !hasActiveEsimProfiles()
? AVAILABLE
: CONDITIONALLY_UNAVAILABLE;
}
@@ -135,7 +147,6 @@
@Override
public void onActiveSubInfoChanged(List<SubscriptionInfoEntity> subInfoEntityList) {
- // TODO(b/262195754): Need the intent to enabled the feature.
mSubscriptionInfoEntityList = subInfoEntityList;
mSubscriptionInfoEntityList.forEach(entity -> {
if (Integer.parseInt(entity.subId) == mSubId) {
@@ -155,6 +166,24 @@
return true;
}
+ private boolean hasActiveEsimProfiles() {
+ SubscriptionManager subscriptionManager = mContext.getSystemService(
+ SubscriptionManager.class);
+ List<SubscriptionInfo> subscriptionInfoList =
+ SubscriptionUtil.getActiveSubscriptions(subscriptionManager);
+ if (subscriptionInfoList == null || subscriptionInfoList.isEmpty()) {
+ return false;
+ }
+ int activatedEsimCount = (int) subscriptionInfoList
+ .stream()
+ .filter(SubscriptionInfo::isEmbedded)
+ .count();
+ if (activatedEsimCount > 0) {
+ return true;
+ }
+ return false;
+ }
+
private boolean findConversionSupportComponent() {
Intent intent = new Intent(EuiccService.ACTION_CONVERT_TO_EMBEDDED_SUBSCRIPTION);
PackageManager packageManager = mContext.getPackageManager();
diff --git a/src/com/android/settings/privatespace/AutoAdvanceSetupFragment.java b/src/com/android/settings/privatespace/AutoAdvanceSetupFragment.java
index 77d57f7..9e905f2 100644
--- a/src/com/android/settings/privatespace/AutoAdvanceSetupFragment.java
+++ b/src/com/android/settings/privatespace/AutoAdvanceSetupFragment.java
@@ -52,16 +52,17 @@
private static final String TITLE_INDEX = "title_index";
private static final int DELAY_BETWEEN_SCREENS = 5000; // 5 seconds in millis
private static final int ANIMATION_DURATION_MILLIS = 500;
+ private static final int HEADER_TEXT_MAX_LINES = 4;
private GlifLayout mRootView;
private Handler mHandler;
private int mScreenTitleIndex;
private static final List<Pair<Integer, Integer>> HEADER_IMAGE_PAIRS =
ImmutableList.of(
- new Pair(R.string.privatespace_apps_hidden_title,
+ new Pair(R.string.private_space_notifications_hidden_title,
R.drawable.privatespace_setup_flow_placeholder),
- new Pair(R.string.privatespace_access_from_apps_title,
+ new Pair(R.string.private_space_share_photos_title,
R.drawable.privatespace_setup_flow_placeholder),
- new Pair(R.string.privatespace_system_apps_installed_title,
+ new Pair(R.string.private_space_apps_installed_title,
R.drawable.privatespace_setup_flow_placeholder));
private Runnable mUpdateScreenResources =
@@ -116,6 +117,7 @@
mRootView =
(GlifLayout)
inflater.inflate(R.layout.privatespace_advancing_screen, container, false);
+ mRootView.getHeaderTextView().setMaxLines(HEADER_TEXT_MAX_LINES);
updateHeaderAndImage();
mHandler = new Handler(Looper.getMainLooper());
mHandler.postDelayed(mUpdateScreenResources, DELAY_BETWEEN_SCREENS);
diff --git a/src/com/android/settings/privatespace/PrivateProfileCreationError.java b/src/com/android/settings/privatespace/PrivateProfileCreationError.java
index 91c35a3..74beef4 100644
--- a/src/com/android/settings/privatespace/PrivateProfileCreationError.java
+++ b/src/com/android/settings/privatespace/PrivateProfileCreationError.java
@@ -47,14 +47,14 @@
final FooterBarMixin mixin = rootView.getMixin(FooterBarMixin.class);
mixin.setPrimaryButton(
new FooterButton.Builder(getContext())
- .setText(R.string.privatespace_tryagain_label)
+ .setText(R.string.private_space_tryagain_label)
.setListener(onTryAgain())
.setButtonType(FooterButton.ButtonType.NEXT)
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
.build());
mixin.setSecondaryButton(
new FooterButton.Builder(getContext())
- .setText(R.string.privatespace_cancel_label)
+ .setText(R.string.private_space_cancel_label)
.setListener(onCancel())
.setButtonType(FooterButton.ButtonType.CANCEL)
.setTheme(
diff --git a/src/com/android/settings/privatespace/PrivateSpaceAccountLoginError.java b/src/com/android/settings/privatespace/PrivateSpaceAccountLoginError.java
index ac3518e..e445a7f 100644
--- a/src/com/android/settings/privatespace/PrivateSpaceAccountLoginError.java
+++ b/src/com/android/settings/privatespace/PrivateSpaceAccountLoginError.java
@@ -51,7 +51,7 @@
final FooterBarMixin mixin = rootView.getMixin(FooterBarMixin.class);
mixin.setPrimaryButton(
new FooterButton.Builder(getContext())
- .setText(R.string.privatespace_tryagain_label)
+ .setText(R.string.private_space_tryagain_label)
.setListener(nextScreen())
.setButtonType(FooterButton.ButtonType.NEXT)
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
diff --git a/src/com/android/settings/privatespace/PrivateSpaceEducation.java b/src/com/android/settings/privatespace/PrivateSpaceEducation.java
index 887854b..eb562af 100644
--- a/src/com/android/settings/privatespace/PrivateSpaceEducation.java
+++ b/src/com/android/settings/privatespace/PrivateSpaceEducation.java
@@ -50,14 +50,14 @@
final FooterBarMixin mixin = rootView.getMixin(FooterBarMixin.class);
mixin.setPrimaryButton(
new FooterButton.Builder(getContext())
- .setText(R.string.privatespace_setup_button_label)
+ .setText(R.string.private_space_setup_button_label)
.setListener(onSetup())
.setButtonType(FooterButton.ButtonType.NEXT)
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
.build());
mixin.setSecondaryButton(
new FooterButton.Builder(getContext())
- .setText(R.string.privatespace_cancel_label)
+ .setText(R.string.private_space_cancel_label)
.setListener(onCancel())
.setButtonType(FooterButton.ButtonType.CANCEL)
.setTheme(
diff --git a/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java b/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java
index 3ec7c92..b5e76920 100644
--- a/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java
+++ b/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java
@@ -34,6 +34,7 @@
import android.util.Log;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.internal.annotations.GuardedBy;
@@ -226,6 +227,26 @@
HIDE_PRIVATE_SPACE_ENTRY_POINT_DISABLED_VAL);
}
+ /**
+ * Returns true if private space exists and quiet mode is successfully enabled, otherwise
+ * returns false
+ */
+ public synchronized boolean lockPrivateSpace() {
+ if (isPrivateProfileRunning()) {
+ return mUserManager.requestQuietModeEnabled(true, mUserHandle);
+ }
+ return false;
+ }
+
+ /** Returns true if private space exists and is running, otherwise returns false */
+ @VisibleForTesting
+ synchronized boolean isPrivateProfileRunning() {
+ if (doesPrivateSpaceExist() && mUserHandle != null) {
+ return mUserManager.isUserRunning(mUserHandle);
+ }
+ return false;
+ }
+
private void resetPrivateSpaceSettings() {
setHidePrivateSpaceEntryPointSetting(HIDE_PRIVATE_SPACE_ENTRY_POINT_DISABLED_VAL);
}
diff --git a/src/com/android/settings/privatespace/PrivateSpaceSetLockFragment.java b/src/com/android/settings/privatespace/PrivateSpaceSetLockFragment.java
index dd4bc7f..a256ea6 100644
--- a/src/com/android/settings/privatespace/PrivateSpaceSetLockFragment.java
+++ b/src/com/android/settings/privatespace/PrivateSpaceSetLockFragment.java
@@ -60,14 +60,14 @@
final FooterBarMixin mixin = rootView.getMixin(FooterBarMixin.class);
mixin.setPrimaryButton(
new FooterButton.Builder(getContext())
- .setText(R.string.privatespace_use_screenlock_label)
+ .setText(R.string.private_space_use_screenlock_label)
.setListener(onClickUse())
.setButtonType(FooterButton.ButtonType.NEXT)
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
.build());
mixin.setSecondaryButton(
new FooterButton.Builder(getContext())
- .setText(R.string.privatespace_set_lock_label)
+ .setText(R.string.private_space_set_lock_label)
.setListener(onClickNewLock())
.setButtonType(FooterButton.ButtonType.NEXT)
.setTheme(
diff --git a/src/com/android/settings/privatespace/SetupSuccessFragment.java b/src/com/android/settings/privatespace/SetupSuccessFragment.java
index fc2ce46..ebeae7a 100644
--- a/src/com/android/settings/privatespace/SetupSuccessFragment.java
+++ b/src/com/android/settings/privatespace/SetupSuccessFragment.java
@@ -55,7 +55,7 @@
final FooterBarMixin mixin = rootView.getMixin(FooterBarMixin.class);
mixin.setPrimaryButton(
new FooterButton.Builder(getContext())
- .setText(R.string.privatespace_done_label)
+ .setText(R.string.private_space_done_label)
.setListener(onClickNext())
.setButtonType(FooterButton.ButtonType.NEXT)
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
@@ -84,6 +84,8 @@
if (activity != null) {
mMetricsFeatureProvider.action(
getContext(), SettingsEnums.ACTION_PRIVATE_SPACE_SETUP_DONE);
+ //TODO(b/307729746): Add a test to verify PS is locked after setup completion.
+ PrivateSpaceMaintainer.getInstance(activity).lockPrivateSpace();
Intent allAppsIntent = new Intent(Intent.ACTION_ALL_APPS);
ResolveInfo resolveInfo =
activity.getPackageManager()
@@ -104,6 +106,7 @@
}
private void accessPrivateSpaceToast() {
- Toast.makeText(getContext(), R.string.scrolldown_to_access, Toast.LENGTH_SHORT).show();
+ Toast.makeText(getContext(), R.string.private_space_scrolldown_to_access,
+ Toast.LENGTH_SHORT).show();
}
}
diff --git a/src/com/android/settings/privatespace/onelock/UseOneLockControllerSwitch.java b/src/com/android/settings/privatespace/onelock/UseOneLockControllerSwitch.java
index 04101b2..54b0374 100644
--- a/src/com/android/settings/privatespace/onelock/UseOneLockControllerSwitch.java
+++ b/src/com/android/settings/privatespace/onelock/UseOneLockControllerSwitch.java
@@ -192,7 +192,7 @@
new AlertDialog.Builder(mContext)
.setMessage(R.string.private_space_new_lock_title)
.setPositiveButton(
- R.string.privatespace_set_lock_label,
+ R.string.private_space_set_lock_label,
(dialog, which) -> {
Intent intent = new Intent(mContext,
PrivateProfileContextHelperActivity.class);
@@ -201,7 +201,7 @@
UNUNIFY_PRIVATE_LOCK_FROM_DEVICE_REQUEST,
/*Options*/ null, mUserHandle);
})
- .setNegativeButton(R.string.privatespace_cancel_label,
+ .setNegativeButton(R.string.private_space_cancel_label,
(DialogInterface dialog, int which) -> {
mUnifyProfile.setChecked(true);
dialog.dismiss();
diff --git a/src/com/android/settings/search/BaseSearchIndexProvider.java b/src/com/android/settings/search/BaseSearchIndexProvider.java
index 581eb2e..d21d983 100644
--- a/src/com/android/settings/search/BaseSearchIndexProvider.java
+++ b/src/com/android/settings/search/BaseSearchIndexProvider.java
@@ -136,7 +136,7 @@
try {
controllersFromCode = createPreferenceControllers(context);
} catch (Exception e) {
- Log.w(TAG, "Error initial controller");
+ Log.w(TAG, "Error initializing controller in fragment: " + this + ", e: " + e);
}
final List<SearchIndexableResource> res = getXmlResourcesToIndex(context, true);
diff --git a/src/com/android/settings/slices/SliceBackgroundWorker.java b/src/com/android/settings/slices/SliceBackgroundWorker.java
index 2b02999..5145f18 100644
--- a/src/com/android/settings/slices/SliceBackgroundWorker.java
+++ b/src/com/android/settings/slices/SliceBackgroundWorker.java
@@ -29,6 +29,8 @@
import android.util.ArrayMap;
import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+
import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
@@ -172,7 +174,8 @@
/**
* Notify that data was updated and attempt to sync changes to the Slice.
*/
- protected final void notifySliceChange() {
+ @VisibleForTesting
+ public final void notifySliceChange() {
NotifySliceChangeHandler.getInstance().updateSlice(this);
}
diff --git a/src/com/android/settings/spa/SpaActivity.kt b/src/com/android/settings/spa/SpaActivity.kt
index e5bee8b..5eade81 100644
--- a/src/com/android/settings/spa/SpaActivity.kt
+++ b/src/com/android/settings/spa/SpaActivity.kt
@@ -18,9 +18,11 @@
import android.content.Context
import android.content.Intent
+import android.os.Bundle
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider
+import com.android.settingslib.core.lifecycle.HideNonSystemOverlayMixin
import com.android.settingslib.spa.framework.BrowseActivity
import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.util.SESSION_BROWSE
@@ -31,6 +33,11 @@
override fun isPageEnabled(page: SettingsPage) =
super.isPageEnabled(page) && !isSuwAndPageBlocked(page.sppName)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycle.addObserver(HideNonSystemOverlayMixin(this))
+ }
+
companion object {
private const val TAG = "SpaActivity"
diff --git a/src/com/android/settings/spa/app/appinfo/AppArchiveButton.kt b/src/com/android/settings/spa/app/appinfo/AppArchiveButton.kt
new file mode 100644
index 0000000..913da65
--- /dev/null
+++ b/src/com/android/settings/spa/app/appinfo/AppArchiveButton.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.settings.spa.app.appinfo
+
+import android.app.PendingIntent
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInstaller
+import android.os.UserHandle
+import android.util.Log
+import android.widget.Toast
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.CloudUpload
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.settings.R
+import com.android.settingslib.spa.widget.button.ActionButton
+import com.android.settingslib.spaprivileged.framework.compose.DisposableBroadcastReceiverAsUser
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+
+class AppArchiveButton(packageInfoPresenter: PackageInfoPresenter) {
+ private companion object {
+ private const val LOG_TAG = "AppArchiveButton"
+ private const val INTENT_ACTION = "com.android.settings.archive.action"
+ }
+
+ private val context = packageInfoPresenter.context
+ private val appButtonRepository = AppButtonRepository(context)
+ private val userPackageManager = packageInfoPresenter.userPackageManager
+ private val packageInstaller = userPackageManager.packageInstaller
+ private val packageName = packageInfoPresenter.packageName
+ private val userHandle = UserHandle.of(packageInfoPresenter.userId)
+ private var broadcastReceiverIsCreated = false
+
+ @Composable
+ fun getActionButton(app: ApplicationInfo): ActionButton {
+ if (!broadcastReceiverIsCreated) {
+ val intentFilter = IntentFilter(INTENT_ACTION)
+ DisposableBroadcastReceiverAsUser(intentFilter, userHandle) { intent ->
+ if (app.packageName == intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)) {
+ onReceive(intent, app)
+ }
+ }
+ broadcastReceiverIsCreated = true
+ }
+ return ActionButton(
+ text = context.getString(R.string.archive),
+ imageVector = Icons.Outlined.CloudUpload,
+ enabled = remember(app) {
+ flow {
+ emit(
+ app.isActionButtonEnabled() && appButtonRepository.isAllowUninstallOrArchive(
+ context,
+ app
+ )
+ )
+ }.flowOn(Dispatchers.Default)
+ }.collectAsStateWithLifecycle(false).value
+ ) { onArchiveClicked(app) }
+ }
+
+ private fun ApplicationInfo.isActionButtonEnabled(): Boolean {
+ return !isArchived
+ && userPackageManager.isAppArchivable(packageName)
+ // We apply the same device policy for both the uninstallation and archive
+ // button.
+ && !appButtonRepository.isUninstallBlockedByAdmin(this)
+ }
+
+ private fun onArchiveClicked(app: ApplicationInfo) {
+ val intent = Intent(INTENT_ACTION)
+ intent.setPackage(context.packageName)
+ val pendingIntent = PendingIntent.getBroadcastAsUser(
+ context, 0, intent,
+ // FLAG_MUTABLE is required by PackageInstaller#requestArchive
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_MUTABLE,
+ userHandle
+ )
+ try {
+ packageInstaller.requestArchive(app.packageName, pendingIntent.intentSender, 0)
+ } catch (e: Exception) {
+ Log.e(LOG_TAG, "Request archive failed", e)
+ Toast.makeText(
+ context,
+ context.getString(R.string.archiving_failed),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+
+ private fun onReceive(intent: Intent, app: ApplicationInfo) {
+ when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, Int.MIN_VALUE)) {
+ PackageInstaller.STATUS_SUCCESS -> {
+ val appLabel = userPackageManager.getApplicationLabel(app)
+ Toast.makeText(
+ context,
+ context.getString(R.string.archiving_succeeded, appLabel),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+
+ else -> {
+ Log.e(LOG_TAG, "Request archiving failed for $packageName with code $status")
+ Toast.makeText(
+ context,
+ context.getString(R.string.archiving_failed),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+}
diff --git a/src/com/android/settings/spa/app/appinfo/AppButtonRepository.kt b/src/com/android/settings/spa/app/appinfo/AppButtonRepository.kt
index 2383ddb..f01c31c 100644
--- a/src/com/android/settings/spa/app/appinfo/AppButtonRepository.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppButtonRepository.kt
@@ -19,6 +19,7 @@
import android.app.ActivityManager
import android.content.ComponentName
import android.content.Context
+import android.content.om.OverlayManager
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
@@ -26,7 +27,9 @@
import com.android.settingslib.RestrictedLockUtilsInternal
import com.android.settingslib.Utils
import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager
+import com.android.settingslib.spaprivileged.model.app.hasFlag
import com.android.settingslib.spaprivileged.model.app.isDisallowControl
+import com.android.settingslib.spaprivileged.model.app.userHandle
import com.android.settingslib.spaprivileged.model.app.userId
class AppButtonRepository(private val context: Context) {
@@ -77,6 +80,55 @@
false
}
+ /** Gets whether a package can be uninstalled or archived. */
+ fun isAllowUninstallOrArchive(
+ context: Context, app: ApplicationInfo
+ ): Boolean {
+ val overlayManager = checkNotNull(context.getSystemService(OverlayManager::class.java))
+ when {
+ !app.hasFlag(ApplicationInfo.FLAG_INSTALLED) && !app.isArchived -> return false
+
+ com.android.settings.Utils.isProfileOrDeviceOwner(
+ context.devicePolicyManager, app.packageName, app.userId
+ ) -> return false
+
+ isDisallowControl(app) -> return false
+
+ uninstallDisallowedDueToHomeApp(app.packageName) -> return false
+
+ // Resource overlays can be uninstalled iff they are public (installed on /data) and
+ // disabled. ("Enabled" means they are in use by resource management.)
+ app.isEnabledResourceOverlay(overlayManager) -> return false
+
+ else -> return true
+ }
+ }
+
+ /**
+ * Checks whether the given package cannot be uninstalled due to home app restrictions.
+ *
+ * Home launcher apps need special handling, we can't allow uninstallation of the only home
+ * app, and we don't want to allow uninstallation of an explicitly preferred one -- the user
+ * can go to Home settings and pick a different one, after which we'll permit uninstallation
+ * of the now-not-default one.
+ */
+ private fun uninstallDisallowedDueToHomeApp(packageName: String): Boolean {
+ val homePackageInfo = getHomePackageInfo()
+ return when {
+ packageName !in homePackageInfo.homePackages -> false
+
+ // Disallow uninstall when this is the only home app.
+ homePackageInfo.homePackages.size == 1 -> true
+
+ // Disallow if this is the explicit default home app.
+ else -> packageName == homePackageInfo.currentDefaultHome?.packageName
+ }
+ }
+
+ private fun ApplicationInfo.isEnabledResourceOverlay(overlayManager: OverlayManager): Boolean =
+ isResourceOverlay &&
+ overlayManager.getOverlayInfo(packageName, userHandle)?.isEnabled == true
+
data class HomePackages(
val homePackages: Set<String>,
val currentDefaultHome: ComponentName?,
diff --git a/src/com/android/settings/spa/app/appinfo/AppButtons.kt b/src/com/android/settings/spa/app/appinfo/AppButtons.kt
index 307ff11..403263c 100644
--- a/src/com/android/settings/spa/app/appinfo/AppButtons.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppButtons.kt
@@ -30,7 +30,10 @@
/**
* @param featureFlags can be overridden in tests
*/
-fun AppButtons(packageInfoPresenter: PackageInfoPresenter, featureFlags: FeatureFlags = FeatureFlagsImpl()) {
+fun AppButtons(
+ packageInfoPresenter: PackageInfoPresenter,
+ featureFlags: FeatureFlags = FeatureFlagsImpl()
+) {
if (remember(packageInfoPresenter) { packageInfoPresenter.isMainlineModule() }) return
val presenter = remember { AppButtonsPresenter(packageInfoPresenter, featureFlags) }
ActionButtons(actionButtons = presenter.getActionButtons())
@@ -49,6 +52,8 @@
private val appUninstallButton = AppUninstallButton(packageInfoPresenter)
private val appClearButton = AppClearButton(packageInfoPresenter)
private val appForceStopButton = AppForceStopButton(packageInfoPresenter)
+ private val appArchiveButton = AppArchiveButton(packageInfoPresenter)
+ private val appRestoreButton = AppRestoreButton(packageInfoPresenter)
@Composable
fun getActionButtons() =
@@ -58,7 +63,15 @@
@Composable
private fun getActionButtons(app: ApplicationInfo): List<ActionButton> = listOfNotNull(
- if (featureFlags.archiving()) null else appLaunchButton.getActionButton(app),
+ if (featureFlags.archiving()) {
+ if (app.isArchived) {
+ appRestoreButton.getActionButton(app)
+ } else {
+ appArchiveButton.getActionButton(app)
+ }
+ } else {
+ appLaunchButton.getActionButton(app)
+ },
appInstallButton.getActionButton(app),
appDisableButton.getActionButton(app),
appUninstallButton.getActionButton(app),
diff --git a/src/com/android/settings/spa/app/appinfo/AppRestoreButton.kt b/src/com/android/settings/spa/app/appinfo/AppRestoreButton.kt
new file mode 100644
index 0000000..c47fdac
--- /dev/null
+++ b/src/com/android/settings/spa/app/appinfo/AppRestoreButton.kt
@@ -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.settings.spa.app.appinfo
+
+import android.app.PendingIntent
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInstaller
+import android.os.UserHandle
+import android.util.Log
+import android.widget.Toast
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.CloudDownload
+import androidx.compose.runtime.Composable
+import com.android.settings.R
+import com.android.settingslib.spa.widget.button.ActionButton
+import com.android.settingslib.spaprivileged.framework.compose.DisposableBroadcastReceiverAsUser
+
+class AppRestoreButton(packageInfoPresenter: PackageInfoPresenter) {
+ private companion object {
+ private const val LOG_TAG = "AppRestoreButton"
+ private const val INTENT_ACTION = "com.android.settings.unarchive.action"
+ }
+
+ private val context = packageInfoPresenter.context
+ private val userPackageManager = packageInfoPresenter.userPackageManager
+ private val packageInstaller = userPackageManager.packageInstaller
+ private val packageName = packageInfoPresenter.packageName
+ private val userHandle = UserHandle.of(packageInfoPresenter.userId)
+ private var broadcastReceiverIsCreated = false
+
+ @Composable
+ fun getActionButton(app: ApplicationInfo): ActionButton {
+ if (!broadcastReceiverIsCreated) {
+ val intentFilter = IntentFilter(INTENT_ACTION)
+ DisposableBroadcastReceiverAsUser(intentFilter, userHandle) { intent ->
+ if (app.packageName == intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)) {
+ onReceive(intent, app)
+ }
+ }
+ broadcastReceiverIsCreated = true
+ }
+ return ActionButton(
+ text = context.getString(R.string.restore),
+ imageVector = Icons.Outlined.CloudDownload,
+ enabled = app.isArchived
+ ) { onRestoreClicked(app) }
+ }
+
+ private fun onRestoreClicked(app: ApplicationInfo) {
+ val intent = Intent(INTENT_ACTION)
+ intent.setPackage(context.packageName)
+ val pendingIntent = PendingIntent.getBroadcastAsUser(
+ context, 0, intent,
+ // FLAG_MUTABLE is required by PackageInstaller#requestUnarchive
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_MUTABLE,
+ userHandle
+ )
+ try {
+ packageInstaller.requestUnarchive(app.packageName, pendingIntent.intentSender)
+ val appLabel = userPackageManager.getApplicationLabel(app)
+ Toast.makeText(
+ context,
+ context.getString(R.string.restoring_in_progress, appLabel),
+ Toast.LENGTH_SHORT
+ ).show()
+ } catch (e: Exception) {
+ Log.e(LOG_TAG, "Request unarchive failed", e)
+ Toast.makeText(
+ context,
+ context.getString(R.string.restoring_failed),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+
+ private fun onReceive(intent: Intent, app: ApplicationInfo) {
+ when (val unarchiveStatus =
+ intent.getIntExtra(PackageInstaller.EXTRA_UNARCHIVE_STATUS, Int.MIN_VALUE)) {
+ PackageInstaller.STATUS_PENDING_USER_ACTION -> {
+ Log.e(
+ LOG_TAG,
+ "Request unarchiving failed for $packageName with code $unarchiveStatus"
+ )
+ Toast.makeText(
+ context,
+ context.getString(R.string.restoring_failed),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+
+ PackageInstaller.STATUS_SUCCESS -> {
+ val appLabel = userPackageManager.getApplicationLabel(app)
+ Toast.makeText(
+ context,
+ context.getString(R.string.restoring_succeeded, appLabel),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+
+ else -> {
+ Log.e(
+ LOG_TAG,
+ "Request unarchiving failed for $packageName with code $unarchiveStatus"
+ )
+ val errorDialogIntent =
+ intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
+ if (errorDialogIntent != null) {
+ context.startActivityAsUser(errorDialogIntent, userHandle)
+ } else {
+ Toast.makeText(
+ context,
+ context.getString(R.string.restoring_failed),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt b/src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt
index 5f6f097..ce72840 100644
--- a/src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt
@@ -18,7 +18,6 @@
import android.app.settings.SettingsEnums
import android.content.Intent
-import android.content.om.OverlayManager
import android.content.pm.ApplicationInfo
import android.os.UserHandle
import android.os.UserManager
@@ -28,11 +27,8 @@
import androidx.compose.ui.res.vectorResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settings.R
-import com.android.settings.Utils
import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminAdd
import com.android.settingslib.spa.widget.button.ActionButton
-import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager
-import com.android.settingslib.spaprivileged.model.app.hasFlag
import com.android.settingslib.spaprivileged.model.app.isActiveAdmin
import com.android.settingslib.spaprivileged.model.app.userHandle
import kotlinx.coroutines.Dispatchers
@@ -42,7 +38,6 @@
class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) {
private val context = packageInfoPresenter.context
private val appButtonRepository = AppButtonRepository(context)
- private val overlayManager = context.getSystemService(OverlayManager::class.java)!!
private val userManager = context.getSystemService(UserManager::class.java)!!
@Composable
@@ -51,49 +46,6 @@
return uninstallButton(app)
}
- /** Gets whether a package can be uninstalled. */
- private fun isUninstallButtonEnabled(app: ApplicationInfo): Boolean = when {
- !app.hasFlag(ApplicationInfo.FLAG_INSTALLED) -> false
-
- Utils.isProfileOrDeviceOwner(
- context.devicePolicyManager, app.packageName, packageInfoPresenter.userId) -> false
-
- appButtonRepository.isDisallowControl(app) -> false
-
- uninstallDisallowedDueToHomeApp(app.packageName) -> false
-
- // Resource overlays can be uninstalled iff they are public (installed on /data) and
- // disabled. ("Enabled" means they are in use by resource management.)
- app.isEnabledResourceOverlay() -> false
-
- else -> true
- }
-
- /**
- * Checks whether the given package cannot be uninstalled due to home app restrictions.
- *
- * Home launcher apps need special handling, we can't allow uninstallation of the only home
- * app, and we don't want to allow uninstallation of an explicitly preferred one -- the user
- * can go to Home settings and pick a different one, after which we'll permit uninstallation
- * of the now-not-default one.
- */
- private fun uninstallDisallowedDueToHomeApp(packageName: String): Boolean {
- val homePackageInfo = appButtonRepository.getHomePackageInfo()
- return when {
- packageName !in homePackageInfo.homePackages -> false
-
- // Disallow uninstall when this is the only home app.
- homePackageInfo.homePackages.size == 1 -> true
-
- // Disallow if this is the explicit default home app.
- else -> packageName == homePackageInfo.currentDefaultHome?.packageName
- }
- }
-
- private fun ApplicationInfo.isEnabledResourceOverlay(): Boolean =
- isResourceOverlay &&
- overlayManager.getOverlayInfo(packageName, userHandle)?.isEnabled == true
-
@Composable
private fun uninstallButton(app: ApplicationInfo) = ActionButton(
text = if (isCloneApp(app)) context.getString(R.string.delete) else
@@ -101,7 +53,7 @@
imageVector = ImageVector.vectorResource(R.drawable.ic_settings_delete),
enabled = remember(app) {
flow {
- emit(isUninstallButtonEnabled(app))
+ emit(appButtonRepository.isAllowUninstallOrArchive(context, app))
}.flowOn(Dispatchers.Default)
}.collectAsStateWithLifecycle(false).value,
) { onUninstallClicked(app) }
diff --git a/src/com/android/settings/spa/app/appinfo/HibernationSwitchPreference.kt b/src/com/android/settings/spa/app/appinfo/HibernationSwitchPreference.kt
index 78ca15b..efa88b5 100644
--- a/src/com/android/settings/spa/app/appinfo/HibernationSwitchPreference.kt
+++ b/src/com/android/settings/spa/app/appinfo/HibernationSwitchPreference.kt
@@ -22,6 +22,7 @@
import android.app.AppOpsManager.OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED
import android.content.Context
import android.content.pm.ApplicationInfo
+import android.content.pm.Flags
import android.os.Build
import android.permission.PermissionControllerManager.HIBERNATION_ELIGIBILITY_EXEMPT_BY_SYSTEM
import android.permission.PermissionControllerManager.HIBERNATION_ELIGIBILITY_UNKNOWN
@@ -60,8 +61,17 @@
val isCheckedState = presenter.isCheckedFlow.collectAsStateWithLifecycle(initialValue = null)
SwitchPreference(remember {
object : SwitchPreferenceModel {
- override val title = context.getString(R.string.unused_apps_switch)
- override val summary = { context.getString(R.string.unused_apps_switch_summary) }
+ override val title =
+ if (isArchivingEnabled())
+ context.getString(R.string.unused_apps_switch_v2)
+ else
+ context.getString(R.string.unused_apps_switch)
+ override val summary = {
+ if (isArchivingEnabled())
+ context.getString(R.string.unused_apps_switch_summary_v2)
+ else
+ context.getString(R.string.unused_apps_switch_summary)
+ }
override val changeable = { isEligibleState }
override val checked = { if (changeable()) isCheckedState.value else false }
override val onCheckedChange = presenter::onCheckedChange
@@ -69,6 +79,9 @@
})
}
+private fun isArchivingEnabled() =
+ Flags.archiving() || "true" == System.getProperty("pm.archiving.enabled")
+
private class HibernationSwitchPresenter(context: Context, private val app: ApplicationInfo) {
private val appOpsManager = context.appOpsManager
private val permissionControllerManager =
diff --git a/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt b/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt
index a6bd8f0..8c802d1 100644
--- a/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt
+++ b/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt
@@ -87,19 +87,9 @@
).filter(::isInterestedAppChange).filter(::isForThisApp)
@VisibleForTesting
- fun isInterestedAppChange(intent: Intent) = when {
- intent.action != Intent.ACTION_PACKAGE_REMOVED -> true
-
- // filter out the fully removed case, in which the page will be closed, so no need to
- // refresh
- intent.getBooleanExtra(Intent.EXTRA_DATA_REMOVED, false) -> false
-
- // filter out the updates are uninstalled (system app), which will followed by a replacing
- // broadcast, we can refresh at that time
- intent.getBooleanExtra(Intent.EXTRA_REPLACING, false) -> false
-
- else -> true // App archived
- }
+ fun isInterestedAppChange(intent: Intent) =
+ intent.action != Intent.ACTION_PACKAGE_REMOVED ||
+ intent.getBooleanExtra(Intent.EXTRA_ARCHIVAL, false)
val flow: StateFlow<PackageInfo?> = merge(flowOf(null), appChangeFlow)
.map { getPackageInfo() }
diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java
index 5e90e55..ff851eb 100644
--- a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java
@@ -262,6 +262,13 @@
mFragment.onResume();
}
+ @Test
+ public void testFragmentVisibleWhenNoHardwareDetected() {
+ doReturn(false).when(mFingerprintManager).isHardwareDetected();
+ setUpFragment(false);
+ assertThat(mFragment.isVisible()).isTrue();
+ }
+
private void setSensor(@FingerprintSensorProperties.SensorType int sensorType) {
final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
props.add(new FingerprintSensorPropertiesInternal(
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java
new file mode 100644
index 0000000..fa6cc6c
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java
@@ -0,0 +1,228 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothStatusCodes;
+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.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.settings.R;
+import com.android.settings.flags.Flags;
+import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.androidx.fragment.FragmentController;
+
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+ shadows = {
+ ShadowAlertDialogCompat.class,
+ ShadowBluetoothAdapter.class,
+ })
+public class AudioSharingDialogFragmentTest {
+
+ @Rule public final MockitoRule mocks = MockitoJUnit.rule();
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ private static final String TEST_DEVICE_NAME1 = "test1";
+
+ private static final String TEST_DEVICE_NAME2 = "test2";
+ private static final String TEST_DEVICE_NAME3 = "test3";
+ private static final AudioSharingDeviceItem TEST_DEVICE_ITEM1 =
+ new AudioSharingDeviceItem(TEST_DEVICE_NAME1, /* groupId= */ 1, /* isActive= */ false);
+ private static final AudioSharingDeviceItem TEST_DEVICE_ITEM2 =
+ new AudioSharingDeviceItem(TEST_DEVICE_NAME2, /* groupId= */ 2, /* isActive= */ false);
+ private static final AudioSharingDeviceItem TEST_DEVICE_ITEM3 =
+ new AudioSharingDeviceItem(TEST_DEVICE_NAME3, /* groupId= */ 3, /* isActive= */ false);
+
+ private Fragment mParent;
+ private AudioSharingDialogFragment mFragment;
+ private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+
+ @Before
+ public void setUp() {
+ ShadowAlertDialogCompat.reset();
+ mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+ mShadowBluetoothAdapter.setEnabled(true);
+ mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+ BluetoothStatusCodes.FEATURE_SUPPORTED);
+ mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+ BluetoothStatusCodes.FEATURE_SUPPORTED);
+ mFragment = new AudioSharingDialogFragment();
+ mParent = new Fragment();
+ FragmentController.setupFragment(
+ mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void onCreateDialog_flagOff_dialogNotExist() {
+ mFragment.show(mParent, new ArrayList<>(), (item) -> {});
+ shadowMainLooper().idle();
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNull();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void onCreateDialog_flagOn_noConnectedDevice() {
+ mFragment.show(mParent, new ArrayList<>(), (item) -> {});
+ shadowMainLooper().idle();
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+ View rootView = shadowDialog.getView();
+ TextView subtitle1 = rootView.findViewById(R.id.share_audio_subtitle1);
+ ImageView guidance = rootView.findViewById(R.id.share_audio_guidance);
+ Button shareBtn = rootView.findViewById(R.id.share_btn);
+ assertThat(dialog.isShowing()).isTrue();
+ assertThat(subtitle1.getVisibility()).isEqualTo(View.GONE);
+ assertThat(guidance.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(shareBtn.getVisibility()).isEqualTo(View.GONE);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void onCreateDialog_noConnectedDevice_dialogDismiss() {
+ mFragment.show(mParent, new ArrayList<>(), (item) -> {});
+ shadowMainLooper().idle();
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ dialog.findViewById(android.R.id.button2).performClick();
+ shadowMainLooper().idle();
+ assertThat(dialog.isShowing()).isFalse();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void onCreateDialog_flagOn_singleConnectedDevice() {
+ ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
+ list.add(TEST_DEVICE_ITEM1);
+ mFragment.show(mParent, list, (item) -> {});
+ shadowMainLooper().idle();
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+ View rootView = shadowDialog.getView();
+ TextView subtitle1 = rootView.findViewById(R.id.share_audio_subtitle1);
+ ImageView guidance = rootView.findViewById(R.id.share_audio_guidance);
+ Button shareBtn = rootView.findViewById(R.id.share_btn);
+ assertThat(dialog.isShowing()).isTrue();
+ assertThat(subtitle1.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(subtitle1.getText().toString()).isEqualTo(TEST_DEVICE_NAME1);
+ assertThat(guidance.getVisibility()).isEqualTo(View.GONE);
+ assertThat(shareBtn.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void onCreateDialog_singleConnectedDevice_dialogDismiss() {
+ mFragment.show(mParent, new ArrayList<>(), (item) -> {});
+ shadowMainLooper().idle();
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+ View rootView = shadowDialog.getView();
+ rootView.findViewById(R.id.cancel_btn).performClick();
+ assertThat(dialog.isShowing()).isFalse();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void onCreateDialog_singleConnectedDevice_shareClicked() {
+ AtomicBoolean isShareBtnClicked = new AtomicBoolean(false);
+ mFragment.show(
+ mParent,
+ new ArrayList<>(),
+ (item) -> {
+ isShareBtnClicked.set(true);
+ });
+ shadowMainLooper().idle();
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+ View rootView = shadowDialog.getView();
+ rootView.findViewById(R.id.share_btn).performClick();
+ assertThat(dialog.isShowing()).isFalse();
+ assertThat(isShareBtnClicked.get()).isTrue();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void onCreateDialog_flagOn_multipleConnectedDevice() {
+ ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
+ list.add(TEST_DEVICE_ITEM1);
+ list.add(TEST_DEVICE_ITEM2);
+ list.add(TEST_DEVICE_ITEM3);
+ mFragment.show(mParent, list, (item) -> {});
+ shadowMainLooper().idle();
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+ View rootView = shadowDialog.getView();
+ TextView subtitle1 = rootView.findViewById(R.id.share_audio_subtitle1);
+ ImageView guidance = rootView.findViewById(R.id.share_audio_guidance);
+ Button shareBtn = rootView.findViewById(R.id.share_btn);
+ RecyclerView recyclerView = rootView.findViewById(R.id.btn_list);
+ assertThat(dialog.isShowing()).isTrue();
+ assertThat(subtitle1.getVisibility()).isEqualTo(View.GONE);
+ assertThat(guidance.getVisibility()).isEqualTo(View.GONE);
+ assertThat(shareBtn.getVisibility()).isEqualTo(View.GONE);
+ assertThat(recyclerView.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(recyclerView.getAdapter().getItemCount()).isEqualTo(3);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void onCreateDialog_multipleConnectedDevice_dialogDismiss() {
+ ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
+ list.add(TEST_DEVICE_ITEM1);
+ list.add(TEST_DEVICE_ITEM2);
+ list.add(TEST_DEVICE_ITEM3);
+ mFragment.show(mParent, list, (item) -> {});
+ shadowMainLooper().idle();
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+ View rootView = shadowDialog.getView();
+ rootView.findViewById(R.id.cancel_btn).performClick();
+ assertThat(dialog.isShowing()).isFalse();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragmentTest.java
new file mode 100644
index 0000000..335bbe3
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragmentTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothStatusCodes;
+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.View;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.settings.R;
+import com.android.settings.flags.Flags;
+import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.androidx.fragment.FragmentController;
+
+import java.util.ArrayList;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+ shadows = {
+ ShadowAlertDialogCompat.class,
+ ShadowBluetoothAdapter.class,
+ })
+public class AudioSharingDisconnectDialogFragmentTest {
+
+ @Rule public final MockitoRule mocks = MockitoJUnit.rule();
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ private static final String TEST_DEVICE_NAME1 = "test1";
+ private static final String TEST_DEVICE_NAME2 = "test2";
+ private static final String TEST_DEVICE_NAME3 = "test3";
+ private static final AudioSharingDeviceItem TEST_DEVICE_ITEM1 =
+ new AudioSharingDeviceItem(TEST_DEVICE_NAME1, /* groupId= */ 1, /* isActive= */ true);
+ private static final AudioSharingDeviceItem TEST_DEVICE_ITEM2 =
+ new AudioSharingDeviceItem(TEST_DEVICE_NAME2, /* groupId= */ 2, /* isActive= */ false);
+
+ private Fragment mParent;
+ private AudioSharingDisconnectDialogFragment mFragment;
+ private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+
+ @Before
+ public void setUp() {
+ ShadowAlertDialogCompat.reset();
+ mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+ mShadowBluetoothAdapter.setEnabled(true);
+ mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+ BluetoothStatusCodes.FEATURE_SUPPORTED);
+ mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+ BluetoothStatusCodes.FEATURE_SUPPORTED);
+ mFragment = new AudioSharingDisconnectDialogFragment();
+ mParent = new Fragment();
+ FragmentController.setupFragment(
+ mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
+ ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
+ list.add(TEST_DEVICE_ITEM1);
+ list.add(TEST_DEVICE_ITEM2);
+ mFragment.show(mParent, list, TEST_DEVICE_NAME3, (item) -> {});
+ shadowMainLooper().idle();
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void onCreateDialog_flagOff_dialogNotExist() {
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNull();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void onCreateDialog_flagOn_dialogShowBtnForTwoDevices() {
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+ View rootView = shadowDialog.getView();
+ RecyclerView view = rootView.findViewById(R.id.device_btn_list);
+ assertThat(view.getAdapter().getItemCount()).isEqualTo(2);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void onCreateDialog_clickCancel_dialogDismiss() {
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+ View rootView = shadowDialog.getView();
+ rootView.findViewById(R.id.cancel_btn).performClick();
+ assertThat(dialog.isShowing()).isFalse();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragmentTest.java
new file mode 100644
index 0000000..38f80e0
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragmentTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothStatusCodes;
+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.View;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.settings.R;
+import com.android.settings.flags.Flags;
+import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.androidx.fragment.FragmentController;
+
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+ shadows = {
+ ShadowAlertDialogCompat.class,
+ ShadowBluetoothAdapter.class,
+ })
+public class AudioSharingJoinDialogFragmentTest {
+
+ @Rule public final MockitoRule mocks = MockitoJUnit.rule();
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ private static final String TEST_DEVICE_NAME1 = "test1";
+ private static final String TEST_DEVICE_NAME2 = "test2";
+ private static final AudioSharingDeviceItem TEST_DEVICE_ITEM =
+ new AudioSharingDeviceItem(TEST_DEVICE_NAME1, /* groupId= */ 1, /* isActive= */ true);
+
+ private Fragment mParent;
+ private AudioSharingJoinDialogFragment mFragment;
+ private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+
+ @Before
+ public void setUp() {
+ ShadowAlertDialogCompat.reset();
+ mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+ mShadowBluetoothAdapter.setEnabled(true);
+ mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+ BluetoothStatusCodes.FEATURE_SUPPORTED);
+ mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+ BluetoothStatusCodes.FEATURE_SUPPORTED);
+ mFragment = new AudioSharingJoinDialogFragment();
+ mParent = new Fragment();
+ FragmentController.setupFragment(
+ mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void onCreateDialog_flagOff_dialogNotExist() {
+ mFragment.show(mParent, new ArrayList<>(), TEST_DEVICE_NAME2, () -> {});
+ shadowMainLooper().idle();
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNull();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void onCreateDialog_flagOn_dialogShowTextForSingleDevice() {
+ mFragment.show(mParent, new ArrayList<>(), TEST_DEVICE_NAME2, () -> {});
+ shadowMainLooper().idle();
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNotNull();
+ assertThat(dialog.isShowing()).isTrue();
+ ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+ View rootView = shadowDialog.getView();
+ TextView subtitle1 = rootView.findViewById(R.id.share_audio_subtitle1);
+ assertThat(subtitle1.getText()).isEqualTo(TEST_DEVICE_NAME2);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void onCreateDialog_flagOn_dialogShowTextForTwoDevice() {
+ ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
+ list.add(TEST_DEVICE_ITEM);
+ mFragment.show(mParent, list, TEST_DEVICE_NAME2, () -> {});
+ shadowMainLooper().idle();
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNotNull();
+ assertThat(dialog.isShowing()).isTrue();
+ ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+ View rootView = shadowDialog.getView();
+ TextView subtitle1 = rootView.findViewById(R.id.share_audio_subtitle1);
+ assertThat(subtitle1.getText()).isEqualTo(TEST_DEVICE_NAME1 + " and " + TEST_DEVICE_NAME2);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void onCreateDialog_clickCancel_dialogDismiss() {
+ mFragment.show(mParent, new ArrayList<>(), TEST_DEVICE_NAME2, () -> {});
+ shadowMainLooper().idle();
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+ View rootView = shadowDialog.getView();
+ rootView.findViewById(R.id.cancel_btn).performClick();
+ assertThat(dialog.isShowing()).isFalse();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void onCreateDialog_clickShare_callbackTriggered() {
+ AtomicBoolean isShareBtnClicked = new AtomicBoolean(false);
+ mFragment.show(
+ mParent, new ArrayList<>(), TEST_DEVICE_NAME2, () -> isShareBtnClicked.set(true));
+ shadowMainLooper().idle();
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+ View rootView = shadowDialog.getView();
+ rootView.findViewById(R.id.share_btn).performClick();
+ assertThat(dialog.isShowing()).isFalse();
+ assertThat(isShareBtnClicked.get()).isTrue();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragmentTest.java
new file mode 100644
index 0000000..61bc88a
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragmentTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothStatusCodes;
+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 androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.settings.flags.Flags;
+import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.androidx.fragment.FragmentController;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+ shadows = {
+ ShadowAlertDialogCompat.class,
+ ShadowBluetoothAdapter.class,
+ })
+public class AudioSharingStopDialogFragmentTest {
+
+ @Rule public final MockitoRule mocks = MockitoJUnit.rule();
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ private static final String TEST_DEVICE_NAME = "test";
+
+ private Fragment mParent;
+ private AudioSharingStopDialogFragment mFragment;
+ private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+
+ @Before
+ public void setUp() {
+ ShadowAlertDialogCompat.reset();
+ mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+ mShadowBluetoothAdapter.setEnabled(true);
+ mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+ BluetoothStatusCodes.FEATURE_SUPPORTED);
+ mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+ BluetoothStatusCodes.FEATURE_SUPPORTED);
+ mFragment = new AudioSharingStopDialogFragment();
+ mParent = new Fragment();
+ FragmentController.setupFragment(
+ mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void onCreateDialog_flagOff_dialogNotExist() {
+ mFragment.show(mParent, TEST_DEVICE_NAME, () -> {});
+ shadowMainLooper().idle();
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNull();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void onCreateDialog_clickCancel_dialogDismiss() {
+ mFragment.show(mParent, TEST_DEVICE_NAME, () -> {});
+ shadowMainLooper().idle();
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ dialog.findViewById(android.R.id.button2).performClick();
+ shadowMainLooper().idle();
+ assertThat(dialog.isShowing()).isFalse();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void onCreateDialog_clickShare_callbackTriggered() {
+ AtomicBoolean isStopBtnClicked = new AtomicBoolean(false);
+ mFragment.show(mParent, TEST_DEVICE_NAME, () -> isStopBtnClicked.set(true));
+ shadowMainLooper().idle();
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ dialog.findViewById(android.R.id.button1).performClick();
+ shadowMainLooper().idle();
+ assertThat(dialog.isShowing()).isFalse();
+ assertThat(isStopBtnClicked.get()).isTrue();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java b/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java
index d01d7e0..2f83da6 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java
@@ -49,7 +49,9 @@
import com.android.settings.R;
import com.android.settings.SettingsActivity;
+import com.android.settings.fuelgauge.batteryusage.BatteryDiffEntry;
import com.android.settings.fuelgauge.batteryusage.BatteryEntry;
+import com.android.settings.fuelgauge.batteryusage.ConvertUtils;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.shadow.ShadowActivityManager;
import com.android.settings.testutils.shadow.ShadowEntityHeaderController;
@@ -91,11 +93,10 @@
private static final String USAGE_PERCENT = "16%";
private static final int ICON_ID = 123;
private static final int UID = 1;
+ private static final long FOREGROUND_TIME_MS = 444;
+ private static final long FOREGROUND_SERVICE_TIME_MS = 123;
private static final long BACKGROUND_TIME_MS = 100;
- private static final long FOREGROUND_ACTIVITY_TIME_MS = 123;
- private static final long FOREGROUND_SERVICE_TIME_MS = 444;
- private static final long FOREGROUND_TIME_MS =
- FOREGROUND_ACTIVITY_TIME_MS + FOREGROUND_SERVICE_TIME_MS;
+ private static final long SCREEN_ON_TIME_MS = 321;
private static final String KEY_ALLOW_BACKGROUND_USAGE = "allow_background_usage";
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
@@ -105,7 +106,6 @@
@Mock private LayoutPreference mHeaderPreference;
@Mock private ApplicationsState mState;
@Mock private ApplicationsState.AppEntry mAppEntry;
- @Mock private Bundle mBundle;
@Mock private BatteryEntry mBatteryEntry;
@Mock private PackageManager mPackageManager;
@Mock private InstallSourceInfo mInstallSourceInfo;
@@ -120,6 +120,8 @@
private SettingsActivity mTestActivity;
private FakeFeatureFactory mFeatureFactory;
private MetricsFeatureProvider mMetricsFeatureProvider;
+ private BatteryDiffEntry mBatteryDiffEntry;
+ private Bundle mBundle;
@Before
public void setUp() {
@@ -131,6 +133,7 @@
mMetricsFeatureProvider = mFeatureFactory.metricsFeatureProvider;
mFragment = spy(new AdvancedPowerUsageDetail());
+ mBundle = spy(new Bundle());
doReturn(mContext).when(mFragment).getContext();
doReturn(mActivity).when(mFragment).getActivity();
doReturn(SUMMARY).when(mFragment).getString(anyInt());
@@ -163,10 +166,35 @@
when(mBatteryEntry.getUid()).thenReturn(UID);
when(mBatteryEntry.getLabel()).thenReturn(APP_LABEL);
- when(mBatteryEntry.getTimeInBackgroundMs()).thenReturn(BACKGROUND_TIME_MS);
when(mBatteryEntry.getTimeInForegroundMs()).thenReturn(FOREGROUND_TIME_MS);
+ when(mBatteryEntry.getTimeInForegroundServiceMs()).thenReturn(FOREGROUND_SERVICE_TIME_MS);
+ when(mBatteryEntry.getTimeInBackgroundMs()).thenReturn(BACKGROUND_TIME_MS);
mBatteryEntry.mIconId = ICON_ID;
+ mBatteryDiffEntry =
+ spy(
+ new BatteryDiffEntry(
+ mContext,
+ /* uid= */ UID,
+ /* userId= */ 0,
+ /* key= */ "key",
+ /* isHidden= */ false,
+ /* componentId= */ -1,
+ /* legacyPackageName= */ null,
+ /* legacyLabel= */ null,
+ /*consumerType*/ ConvertUtils.CONSUMER_TYPE_USER_BATTERY,
+ /* foregroundUsageTimeInMs= */ FOREGROUND_TIME_MS,
+ /* foregroundSerUsageTimeInMs= */ FOREGROUND_SERVICE_TIME_MS,
+ /* backgroundUsageTimeInMs= */ BACKGROUND_TIME_MS,
+ /* screenOnTimeInMs= */ SCREEN_ON_TIME_MS,
+ /* consumePower= */ 0,
+ /* foregroundUsageConsumePower= */ 0,
+ /* foregroundServiceUsageConsumePower= */ 0,
+ /* backgroundUsageConsumePower= */ 0,
+ /* cachedUsageConsumePower= */ 0));
+ when(mBatteryDiffEntry.getAppLabel()).thenReturn(APP_LABEL);
+ when(mBatteryDiffEntry.getAppIconId()).thenReturn(ICON_ID);
+
mFragment.mHeaderPreference = mHeaderPreference;
mFragment.mState = mState;
mFragment.mBatteryOptimizeUtils = mBatteryOptimizeUtils;
@@ -191,6 +219,7 @@
.when(mActivity)
.startActivityAsUser(captor.capture(), nullable(UserHandle.class));
doAnswer(callable).when(mActivity).startActivity(captor.capture());
+ doAnswer(callable).when(mContext).startActivity(captor.capture());
mAllowBackgroundUsagePreference = new PrimarySwitchPreference(mContext);
mAllowBackgroundUsagePreference.setKey(KEY_ALLOW_BACKGROUND_USAGE);
@@ -256,6 +285,7 @@
@Test
public void startBatteryDetailPage_invalidToShowSummary_noFGBDData() {
+ mBundle.clear();
AdvancedPowerUsageDetail.startBatteryDetailPage(
mActivity, mFragment, mBatteryEntry, USAGE_PERCENT);
@@ -268,6 +298,35 @@
}
@Test
+ public void startBatteryDetailPage_showSummary_hasFGBDData() {
+ final ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+ mBundle.clear();
+ AdvancedPowerUsageDetail.startBatteryDetailPage(
+ mContext,
+ mFragment.getMetricsCategory(),
+ mBatteryDiffEntry,
+ USAGE_PERCENT,
+ /* slotInformation= */ null,
+ /* showTimeInformation= */ true,
+ /* anomalyHintPrefKey= */ null,
+ /* anomalyHintText= */ null);
+
+ verify(mContext).startActivity(captor.capture());
+ assertThat(mBundle.getInt(AdvancedPowerUsageDetail.EXTRA_UID)).isEqualTo(UID);
+ assertThat(mBundle.getLong(AdvancedPowerUsageDetail.EXTRA_BACKGROUND_TIME))
+ .isEqualTo(BACKGROUND_TIME_MS + FOREGROUND_SERVICE_TIME_MS);
+ assertThat(mBundle.getLong(AdvancedPowerUsageDetail.EXTRA_FOREGROUND_TIME))
+ .isEqualTo(FOREGROUND_TIME_MS);
+ assertThat(mBundle.getLong(AdvancedPowerUsageDetail.EXTRA_SCREEN_ON_TIME))
+ .isEqualTo(SCREEN_ON_TIME_MS);
+ assertThat(mBundle.getString(AdvancedPowerUsageDetail.EXTRA_POWER_USAGE_PERCENT))
+ .isEqualTo(USAGE_PERCENT);
+ assertThat(mBundle.getString(AdvancedPowerUsageDetail.EXTRA_SLOT_TIME))
+ .isEqualTo(null);
+ }
+
+
+ @Test
public void startBatteryDetailPage_noBatteryUsage_hasBasicData() {
final ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
@@ -292,6 +351,7 @@
@Test
public void startBatteryDetailPage_batteryEntryNotExisted_extractUidFromPackageName()
throws PackageManager.NameNotFoundException {
+ mBundle.clear();
doReturn(UID).when(mPackageManager).getPackageUid(PACKAGE_NAME[0], 0 /* no flag */);
AdvancedPowerUsageDetail.startBatteryDetailPage(
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffDataTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffDataTest.java
index ffe3c44..d351ca3 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffDataTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffDataTest.java
@@ -207,6 +207,7 @@
batteryHistEntry.mAppLabel,
batteryHistEntry.mConsumerType,
/* foregroundUsageTimeInMs= */ 0,
+ /* foregroundServiceUsageTimeInMs= */ 0,
/* backgroundUsageTimeInMs= */ 0,
/* screenOnTimeInMs= */ 0,
consumePower,
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntryTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntryTest.java
index d8b733c..6f1dce6 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntryTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntryTest.java
@@ -111,8 +111,9 @@
/* legacyLabel= */ null,
/*consumerType*/ ConvertUtils.CONSUMER_TYPE_UID_BATTERY,
/* foregroundUsageTimeInMs= */ 10001L,
- /* backgroundUsageTimeInMs= */ 20002L,
- /* screenOnTimeInMs= */ 30003L,
+ /* foregroundServiceUsageTimeInMs= */ 20002L,
+ /* backgroundUsageTimeInMs= */ 30003L,
+ /* screenOnTimeInMs= */ 40004L,
/* consumePower= */ 22.0,
/* foregroundUsageConsumePower= */ 10.0,
/* foregroundServiceUsageConsumePower= */ 10.0,
@@ -137,8 +138,9 @@
/* legacyLabel= */ null,
/*consumerType*/ ConvertUtils.CONSUMER_TYPE_UID_BATTERY,
/* foregroundUsageTimeInMs= */ 10001L,
- /* backgroundUsageTimeInMs= */ 20002L,
- /* screenOnTimeInMs= */ 30003L,
+ /* foregroundServiceUsageTimeInMs= */ 20002L,
+ /* backgroundUsageTimeInMs= */ 30003L,
+ /* screenOnTimeInMs= */ 40004L,
/* consumePower= */ 22.0,
/* foregroundUsageConsumePower= */ 10.0,
/* foregroundServiceUsageConsumePower= */ 10.0,
@@ -165,6 +167,7 @@
/* legacyLabel= */ BatteryDiffEntry.SYSTEM_APPS_KEY,
/*consumerType*/ ConvertUtils.CONSUMER_TYPE_UID_BATTERY,
/* foregroundUsageTimeInMs= */ 0,
+ /* foregroundServiceUsageTimeInMs= */ 0,
/* backgroundUsageTimeInMs= */ 0,
/* screenOnTimeInMs= */ 0,
/* consumePower= */ 0,
@@ -552,6 +555,7 @@
/* legacyLabel= */ null,
/*consumerType*/ consumerType,
/* foregroundUsageTimeInMs= */ 0,
+ /* foregroundServiceUsageTimeInMs= */ 0,
/* backgroundUsageTimeInMs= */ 0,
/* screenOnTimeInMs= */ 0,
/* consumePower= */ 0,
@@ -576,6 +580,7 @@
batteryHistEntry.mConsumerType,
/* foregroundUsageTimeInMs= */ 0,
/* backgroundUsageTimeInMs= */ 0,
+ /* foregroundServiceUsageTimeInMs= */ 0,
/* screenOnTimeInMs= */ 0,
consumePower,
/* foregroundUsageConsumePower= */ 0,
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryEntryTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryEntryTest.java
index 83b4458..450d058 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryEntryTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryEntryTest.java
@@ -195,7 +195,8 @@
@Test
public void getTimeInForegroundMs_app() {
- when(mUidBatteryConsumer.getTimeInStateMs(UidBatteryConsumer.STATE_FOREGROUND))
+ when(mUidBatteryConsumer.getTimeInProcessStateMs(
+ UidBatteryConsumer.PROCESS_STATE_FOREGROUND))
.thenReturn(100L);
final BatteryEntry entry =
@@ -226,8 +227,9 @@
@Test
public void getTimeInBackgroundMs_app() {
- when(mUidBatteryConsumer.getTimeInStateMs(UidBatteryConsumer.STATE_BACKGROUND))
- .thenReturn(100L);
+ when(mUidBatteryConsumer.getTimeInProcessStateMs(
+ UidBatteryConsumer.PROCESS_STATE_BACKGROUND))
+ .thenReturn(30L);
final BatteryEntry entry =
new BatteryEntry(
@@ -239,7 +241,26 @@
null,
null);
- assertThat(entry.getTimeInBackgroundMs()).isEqualTo(100L);
+ assertThat(entry.getTimeInBackgroundMs()).isEqualTo(30L);
+ }
+
+ @Test
+ public void getTimeInForegroundServiceMs_app() {
+ when(mUidBatteryConsumer.getTimeInProcessStateMs(
+ UidBatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE))
+ .thenReturn(70L);
+
+ final BatteryEntry entry =
+ new BatteryEntry(
+ RuntimeEnvironment.application,
+ mMockUserManager,
+ mUidBatteryConsumer,
+ false,
+ 0,
+ null,
+ null);
+
+ assertThat(entry.getTimeInForegroundServiceMs()).isEqualTo(70L);
}
@Test
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntryTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntryTest.java
index 02800f7..dc868c8 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntryTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntryTest.java
@@ -65,6 +65,7 @@
when(mMockBatteryEntry.getConsumedPowerInCached()).thenReturn(1.5);
mMockBatteryEntry.mPercent = 0.3;
when(mMockBatteryEntry.getTimeInForegroundMs()).thenReturn(1234L);
+ when(mMockBatteryEntry.getTimeInForegroundServiceMs()).thenReturn(3456L);
when(mMockBatteryEntry.getTimeInBackgroundMs()).thenReturn(5689L);
when(mMockBatteryEntry.getPowerComponentId()).thenReturn(expectedType);
when(mMockBatteryEntry.getConsumerType())
@@ -105,6 +106,7 @@
/* backgroundUsageConsumePower= */ 1.4,
/* cachedUsageConsumePower= */ 1.5,
/* foregroundUsageTimeInMs= */ 1234L,
+ /* foregroundServiceUsageTimeInMs= */ 3456L,
/* backgroundUsageTimeInMs= */ 5689L,
/* batteryLevel= */ 12),
/* drainType= */ 3,
@@ -211,6 +213,7 @@
/* backgroundUsageConsumePower= */ 3,
/* cachedUsageConsumePower= */ 4,
/* foregroundUsageTimeInMs= */ 100,
+ /* foregroundServiceUsageTimeInMs= */ 150,
/* backgroundUsageTimeInMs= */ 200,
/* batteryLevel= */ 90);
final BatteryHistEntry upperHistEntry =
@@ -224,6 +227,7 @@
/* backgroundUsageConsumePower= */ 6,
/* cachedUsageConsumePower= */ 5,
/* foregroundUsageTimeInMs= */ 200,
+ /* foregroundServiceUsageTimeInMs= */ 250,
/* backgroundUsageTimeInMs= */ 300,
/* batteryLevel= */ 80);
@@ -244,6 +248,7 @@
/* backgroundUsageConsumePower= */ 3 + 0.5 * (6 - 3),
/* cachedUsageConsumePower= */ 4 + 0.5 * (5 - 4),
/* foregroundUsageTimeInMs= */ Math.round(100 + 0.5 * (200 - 100)),
+ /* foregroundServiceUsageTimeInMs= */ Math.round(150 + 0.5 * (250 - 150)),
/* backgroundUsageTimeInMs= */ Math.round(200 + 0.5 * (300 - 200)),
/* batteryLevel= */ (int) Math.round(90 + 0.5 * (80 - 90)));
}
@@ -264,6 +269,7 @@
/* backgroundUsageConsumePower= */ 6,
/* cachedUsageConsumePower= */ 5,
/* foregroundUsageTimeInMs= */ 200,
+ /* foregroundServiceUsageTimeInMs= */ 250,
/* backgroundUsageTimeInMs= */ 300,
/* batteryLevel= */ 80);
@@ -288,6 +294,7 @@
/* backgroundUsageConsumePower= */ 0.5 * 6,
/* cachedUsageConsumePower= */ 0.5 * 5,
/* foregroundUsageTimeInMs= */ Math.round(0.5 * 200),
+ /* foregroundServiceUsageTimeInMs= */ Math.round(0.5 * 250),
/* backgroundUsageTimeInMs= */ Math.round(0.5 * 300),
/* batteryLevel= */ upperHistEntry.mBatteryLevel);
}
@@ -317,6 +324,7 @@
/* backgroundUsageConsumePower= */ 1.4,
/* cachedUsageConsumePower= */ 1.5,
/* foregroundUsageTimeInMs= */ 1234L,
+ /*foregroundServiceUsageTimeInMs=*/ 3456L,
/* backgroundUsageTimeInMs= */ 5689L,
/* batteryLevel= */ 12);
}
@@ -334,6 +342,7 @@
double backgroundUsageConsumePower,
double cachedUsageConsumePower,
long foregroundUsageTimeInMs,
+ long foregroundServiceUsageTimeInMs,
long backgroundUsageTimeInMs,
int batteryLevel) {
assertThat(entry.isValidEntry()).isTrue();
@@ -354,6 +363,7 @@
assertThat(entry.mCachedUsageConsumePower).isEqualTo(cachedUsageConsumePower);
assertThat(entry.mPercentOfTotal).isEqualTo(percentOfTotal);
assertThat(entry.mForegroundUsageTimeInMs).isEqualTo(foregroundUsageTimeInMs);
+ assertThat(entry.mForegroundServiceUsageTimeInMs).isEqualTo(foregroundServiceUsageTimeInMs);
assertThat(entry.mBackgroundUsageTimeInMs).isEqualTo(backgroundUsageTimeInMs);
assertThat(entry.mDrainType).isEqualTo(drainType);
assertThat(entry.mConsumerType).isEqualTo(ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY);
@@ -372,6 +382,7 @@
double backgroundUsageConsumePower,
double cachedUsageConsumePower,
long foregroundUsageTimeInMs,
+ long foregroundServiceUsageTimeInMs,
long backgroundUsageTimeInMs,
int batteryLevel) {
final MatrixCursor cursor =
@@ -406,6 +417,7 @@
.setPercentOfTotal(0.3)
.setDrainType(3)
.setForegroundUsageTimeInMs(foregroundUsageTimeInMs)
+ .setForegroundServiceUsageTimeInMs(foregroundServiceUsageTimeInMs)
.setBackgroundUsageTimeInMs(backgroundUsageTimeInMs)
.build();
cursor.addRow(
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownControllerTest.java
index fc30702..5704be9 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownControllerTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownControllerTest.java
@@ -100,7 +100,8 @@
/* legacyLabel= */ null,
/* consumerType= */ ConvertUtils.CONSUMER_TYPE_UID_BATTERY,
/* foregroundUsageTimeInMs= */ 1,
- /* backgroundUsageTimeInMs= */ 2,
+ /* foregroundServiceUsageTimeInMs= */ 2,
+ /* backgroundUsageTimeInMs= */ 3,
/* screenOnTimeInMs= */ 0,
/* consumePower= */ 3,
/* foregroundUsageConsumePower= */ 0,
@@ -275,6 +276,7 @@
/* isSystem= */ true,
/* screenOnTimeInMs= */ 0,
/* foregroundUsageTimeInMs= */ 0,
+ /* foregroundServiceUsageTimeInMs= */ 0,
/* backgroundUsageTimeInMs= */ 0);
batteryDiffEntry.mConsumePower = 0.8;
batteryDiffEntry.setTotalConsumePower(100);
@@ -293,6 +295,7 @@
/* isSystem= */ true,
/* screenOnTimeInMs= */ 0,
/* foregroundUsageTimeInMs= */ 0,
+ /* foregroundServiceUsageTimeInMs= */ 0,
/* backgroundUsageTimeInMs= */ 0);
batteryDiffEntry.mConsumePower = 16;
batteryDiffEntry.setTotalConsumePower(100);
@@ -314,6 +317,7 @@
/* isSystem= */ true,
/* screenOnTimeInMs= */ 0,
/* foregroundUsageTimeInMs= */ 0,
+ /* foregroundServiceUsageTimeInMs= */ 0,
/* backgroundUsageTimeInMs= */ 0));
assertThat(pref.getSummary().toString().isEmpty()).isTrue();
}
@@ -329,6 +333,7 @@
/* isSystem= */ true,
/* screenOnTimeInMs= */ 0,
/* foregroundUsageTimeInMs= */ DateUtils.MINUTE_IN_MILLIS - 1,
+ /* foregroundServiceUsageTimeInMs= */ 0,
/* backgroundUsageTimeInMs= */ 0));
assertThat(pref.getSummary().toString()).isEqualTo("Total: less than a min");
}
@@ -344,6 +349,7 @@
/* isSystem= */ true,
/* screenOnTimeInMs= */ 0,
/* foregroundUsageTimeInMs= */ DateUtils.MINUTE_IN_MILLIS * 2,
+ /* foregroundServiceUsageTimeInMs= */ 0,
/* backgroundUsageTimeInMs= */ 0));
assertThat(pref.getSummary().toString()).isEqualTo("Total: 2 min");
}
@@ -359,6 +365,7 @@
/* isSystem= */ false,
/* screenOnTimeInMs= */ 0,
/* foregroundUsageTimeInMs= */ 0,
+ /* foregroundServiceUsageTimeInMs= */ 0,
/* backgroundUsageTimeInMs= */ 0));
assertThat(pref.getSummary().toString().isEmpty()).isTrue();
}
@@ -374,11 +381,28 @@
/* isSystem= */ false,
/* screenOnTimeInMs= */ 0,
/* foregroundUsageTimeInMs= */ 0,
+ /* foregroundServiceUsageTimeInMs= */ 0,
/* backgroundUsageTimeInMs= */ DateUtils.MINUTE_IN_MILLIS));
assertThat(pref.getSummary().toString()).isEqualTo("Background: 1 min");
}
@Test
+ public void setPreferenceSummary_appEntryWithFGSTime_expectedSummary() {
+ final PowerGaugePreference pref = new PowerGaugePreference(mContext);
+ pref.setSummary(PREF_SUMMARY);
+
+ mBatteryUsageBreakdownController.setPreferenceSummary(
+ pref,
+ createBatteryDiffEntry(
+ /* isSystem= */ false,
+ /* screenOnTimeInMs= */ 0,
+ /* foregroundUsageTimeInMs= */ 0,
+ /* foregroundServiceUsageTimeInMs= */ DateUtils.MINUTE_IN_MILLIS / 2,
+ /* backgroundUsageTimeInMs= */ DateUtils.MINUTE_IN_MILLIS / 2));
+ assertThat(pref.getSummary().toString()).isEqualTo("Background: 1 min");
+ }
+
+ @Test
public void setPreferenceSummary_appEntryScreenOnTimeOnly_expectedSummary() {
final PowerGaugePreference pref = new PowerGaugePreference(mContext);
pref.setSummary(PREF_SUMMARY);
@@ -389,6 +413,7 @@
/* isSystem= */ false,
/* screenOnTimeInMs= */ DateUtils.MINUTE_IN_MILLIS,
/* foregroundUsageTimeInMs= */ 0,
+ /* foregroundServiceUsageTimeInMs= */ 0,
/* backgroundUsageTimeInMs= */ 0));
assertThat(pref.getSummary().toString()).isEqualTo("Screen time: 1 min");
}
@@ -404,6 +429,7 @@
/* isSystem= */ false,
/* screenOnTimeInMs= */ DateUtils.MINUTE_IN_MILLIS - 1,
/* foregroundUsageTimeInMs= */ DateUtils.MINUTE_IN_MILLIS - 1,
+ /* foregroundServiceUsageTimeInMs= */ 0,
/* backgroundUsageTimeInMs= */ DateUtils.MINUTE_IN_MILLIS - 1));
assertThat(pref.getSummary().toString())
.isEqualTo("Screen time: less than a min\nBackground: less than a min");
@@ -413,6 +439,7 @@
boolean isSystem,
long screenOnTimeInMs,
long foregroundUsageTimeInMs,
+ long foregroundServiceUsageTimeInMs,
long backgroundUsageTimeInMs) {
final ContentValues contentValues = new ContentValues();
contentValues.put(
@@ -435,6 +462,7 @@
batteryHistEntry.mConsumerType,
foregroundUsageTimeInMs,
backgroundUsageTimeInMs,
+ foregroundServiceUsageTimeInMs,
screenOnTimeInMs,
/* consumePower= */ 0,
/* foregroundUsageConsumePower= */ 0,
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java
index 95b950f..950f828 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java
@@ -290,6 +290,7 @@
.setPercentOfTotal(0.9)
.setForegroundUsageTimeInMs(1000)
.setBackgroundUsageTimeInMs(2000)
+ .setForegroundServiceUsageTimeInMs(1500)
.setDrainType(1)
.build();
final String expectedBatteryInformationString =
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java
index 672bc54..e68b892 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java
@@ -92,6 +92,7 @@
mMockBatteryEntry.mPercent = 0.3;
when(mMockBatteryEntry.getTimeInForegroundMs()).thenReturn(1234L);
when(mMockBatteryEntry.getTimeInBackgroundMs()).thenReturn(5689L);
+ when(mMockBatteryEntry.getTimeInForegroundServiceMs()).thenReturn(3456L);
when(mMockBatteryEntry.getPowerComponentId()).thenReturn(-1);
when(mMockBatteryEntry.getConsumerType())
.thenReturn(ConvertUtils.CONSUMER_TYPE_UID_BATTERY);
@@ -133,6 +134,7 @@
assertThat(batteryInformation.getPercentOfTotal()).isEqualTo(0.3);
assertThat(batteryInformation.getForegroundUsageTimeInMs()).isEqualTo(1234L);
assertThat(batteryInformation.getBackgroundUsageTimeInMs()).isEqualTo(5689L);
+ assertThat(batteryInformation.getForegroundServiceUsageTimeInMs()).isEqualTo(3456L);
assertThat(batteryInformation.getDrainType()).isEqualTo(-1);
assertThat(deviceBatteryState.getBatteryLevel()).isEqualTo(12);
assertThat(deviceBatteryState.getBatteryStatus())
@@ -157,6 +159,7 @@
mMockBatteryEntry.mPercent = 0.3;
when(mMockBatteryEntry.getTimeInForegroundMs()).thenReturn(1234L);
when(mMockBatteryEntry.getTimeInBackgroundMs()).thenReturn(5689L);
+ when(mMockBatteryEntry.getTimeInForegroundServiceMs()).thenReturn(3456L);
when(mMockBatteryEntry.getConsumerType())
.thenReturn(ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY);
@@ -196,6 +199,7 @@
assertThat(batteryInformation.getPercentOfTotal()).isEqualTo(0.3);
assertThat(batteryInformation.getForegroundUsageTimeInMs()).isEqualTo(1234L);
assertThat(batteryInformation.getBackgroundUsageTimeInMs()).isEqualTo(5689L);
+ assertThat(batteryInformation.getForegroundServiceUsageTimeInMs()).isEqualTo(3456L);
assertThat(batteryInformation.getDrainType())
.isEqualTo(BatteryConsumer.POWER_COMPONENT_CPU);
assertThat(deviceBatteryState.getBatteryLevel()).isEqualTo(12);
@@ -307,6 +311,7 @@
mMockBatteryEntry.mPercent = 0.3;
when(mMockBatteryEntry.getTimeInForegroundMs()).thenReturn(1234L);
when(mMockBatteryEntry.getTimeInBackgroundMs()).thenReturn(5689L);
+ when(mMockBatteryEntry.getTimeInForegroundServiceMs()).thenReturn(3456L);
when(mMockBatteryEntry.getPowerComponentId()).thenReturn(expectedType);
when(mMockBatteryEntry.getConsumerType())
.thenReturn(ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY);
@@ -331,6 +336,7 @@
assertThat(batteryHistEntry.mPercentOfTotal).isEqualTo(0.3);
assertThat(batteryHistEntry.mForegroundUsageTimeInMs).isEqualTo(1234L);
assertThat(batteryHistEntry.mBackgroundUsageTimeInMs).isEqualTo(5689L);
+ assertThat(batteryHistEntry.mForegroundServiceUsageTimeInMs).isEqualTo(3456L);
assertThat(batteryHistEntry.mDrainType).isEqualTo(expectedType);
assertThat(batteryHistEntry.mConsumerType)
.isEqualTo(ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY);
diff --git a/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/BluetoothUpdateWorkerTest.java b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/BluetoothUpdateWorkerTest.java
index e34737b..43630e8 100644
--- a/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/BluetoothUpdateWorkerTest.java
+++ b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/BluetoothUpdateWorkerTest.java
@@ -16,12 +16,9 @@
package com.android.settings.homepage.contextualcards.slices;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
-import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
@@ -42,49 +39,46 @@
private static final Uri URI = Uri.parse("content://com.android.settings.slices/test");
private BluetoothUpdateWorker mBluetoothUpdateWorker;
- private ContentResolver mResolver;
private Context mContext;
@Before
public void setUp() {
- mContext = spy(RuntimeEnvironment.application);
- mBluetoothUpdateWorker = new BluetoothUpdateWorker(mContext, URI);
- mResolver = mock(ContentResolver.class);
- doReturn(mResolver).when(mContext).getContentResolver();
+ mContext = RuntimeEnvironment.getApplication();
+ mBluetoothUpdateWorker = spy(new BluetoothUpdateWorker(mContext, URI));
}
@Test
public void onAclConnectionStateChanged_shouldNotifyChange() {
mBluetoothUpdateWorker.onAclConnectionStateChanged(null, 0);
- verify(mResolver).notifyChange(URI, null);
+ verify(mBluetoothUpdateWorker).notifySliceChange();
}
@Test
public void onActiveDeviceChanged_shouldNotifyChange() {
mBluetoothUpdateWorker.onActiveDeviceChanged(null, 0);
- verify(mResolver).notifyChange(URI, null);
+ verify(mBluetoothUpdateWorker).notifySliceChange();
}
@Test
public void onBluetoothStateChanged_shouldNotifyChange() {
mBluetoothUpdateWorker.onBluetoothStateChanged(0);
- verify(mResolver).notifyChange(URI, null);
+ verify(mBluetoothUpdateWorker).notifySliceChange();
}
@Test
public void onConnectionStateChanged_shouldNotifyChange() {
mBluetoothUpdateWorker.onConnectionStateChanged(null, 0);
- verify(mResolver).notifyChange(URI, null);
+ verify(mBluetoothUpdateWorker).notifySliceChange();
}
@Test
public void onProfileConnectionStateChanged_shouldNotifyChange() {
mBluetoothUpdateWorker.onProfileConnectionStateChanged(null, 0, 0);
- verify(mResolver).notifyChange(URI, null);
+ verify(mBluetoothUpdateWorker).notifySliceChange();
}
}
\ No newline at end of file
diff --git a/tests/screenshot/Android.bp b/tests/screenshot/Android.bp
index e20b5d3..5989381 100644
--- a/tests/screenshot/Android.bp
+++ b/tests/screenshot/Android.bp
@@ -29,42 +29,46 @@
"androidx.fragment_fragment",
"androidx.test.runner",
"androidx.test.core",
- ],
- uses_libs: ["org.apache.http.legacy"],
-
- aaptflags: ["--extra-packages com.android.settings"],
- manifest: "AndroidManifest.xml",
-}
-
-android_test {
- name: "SettingsScreenshotTests",
- platform_apis: true,
- certificate: "platform",
- test_suites: ["device-tests"],
- srcs: [
- "src/**/*.kt",
- ],
- static_libs: [
- "androidx.fragment_fragment-testing",
- "androidx.fragment_fragment",
"androidx.test.rules",
"androidx.test.ext.junit",
"platform-screenshot-diff-core",
"Settings-testutils2",
- "androidx.test.core",
"androidx.test.espresso.core",
"kotlinx-coroutines-android",
"androidx.lifecycle_lifecycle-runtime-testing",
"kotlinx_coroutines_test",
- "Settings-core",
- "androidx.test.runner",
],
uses_libs: ["org.apache.http.legacy"],
- compile_multilib: "both",
+ aaptflags: ["--extra-packages com.android.settings"],
manifest: "AndroidManifest.xml",
- test_config: "AndroidTest.xml",
- use_embedded_native_libs: false,
- asset_dirs: ["assets"],
+}
+
+// This is a RNG (Robolectric native graphics) test target.
+android_robolectric_test {
+ name: "SettingsScreenshotRNGTests",
+ srcs: [
+ "src/**/*.kt",
+ ],
+ test_suites: ["general-tests"],
+
+ // Do not add any libraries here, instead add them to the ScreenshotTestStub
+ static_libs: [
+ "androidx.compose.runtime_runtime",
+ "androidx.test.uiautomator_uiautomator",
+ "androidx.test.ext.junit",
+ "inline-mockito-robolectric-prebuilt",
+ "platform-parametric-runner-lib",
+ "uiautomator-helpers",
+
+ ],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ "android.test.mock",
+ "truth",
+ ],
+
+ upstream: true,
instrumentation_for: "ScreenshotTestStub",
- data: [":ScreenshotTestStub"],
+ java_resource_dirs: ["config"],
}
diff --git a/tests/screenshot/AndroidManifest.xml b/tests/screenshot/AndroidManifest.xml
index 9cbc882..6c8bb88 100644
--- a/tests/screenshot/AndroidManifest.xml
+++ b/tests/screenshot/AndroidManifest.xml
@@ -16,32 +16,13 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- package="com.android.settings.tests.screenshot"
- >
-
- <application android:debuggable="true">
- <provider
- android:name="com.android.settings.slices.SettingsSliceProvider"
- android:authorities="com.android.settings.tests.screenshot.disabled"
- android:enabled="false"
- tools:node="remove"
- tools:replace="android:authorities" />
-
- </application>
-
- <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
- <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
- <uses-permission android:name="android.permission.READ_LOGS" />
- <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
- <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ coreApp="true"
+ package="com.android.settings">
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
- <instrumentation
- android:name="androidx.test.runner.AndroidJUnitRunner"
- android:label="Android Settings Screenshot tests"
- android:targetPackage="com.android.settings.tests.screenshot" />
+ <application>
+ <activity android:name="com.android.settings.test.screenshot.ContainerActivity" android:exported="true" />
+ </application>
-</manifest>
+</manifest>
\ No newline at end of file
diff --git a/tests/screenshot/AndroidTest.xml b/tests/screenshot/AndroidTest.xml
deleted file mode 100644
index 7496ffd..0000000
--- a/tests/screenshot/AndroidTest.xml
+++ /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.
- -->
-
-<configuration description="Runs settings screendiff tests.">
- <option name="test-suite-tag" value="apct-instrumentation" />
- <option name="test-suite-tag" value="apct" />
- <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
- <option name="optimized-property-setting" value="true" />
- </target_preparer>
- <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
- <option name="cleanup-apks" value="true" />
- <option name="test-file-name" value="SettingsScreenshotTests.apk" />
- </target_preparer>
- <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
- <option name="directory-keys"
- value="/data/user/0/com.android.settings.tests.screenshot/" />
- <option name="collect-on-run-ended-only" value="true" />
- </metrics_collector>
- <test class="com.android.tradefed.testtype.AndroidJUnitTest">
- <option name="package" value="com.android.settings.tests.screenshot" />
- <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
- </test>
-</configuration>
diff --git "a/tests/screenshot/assets/pixel_4a_\0505g\051/fp_enroll_intro.png" "b/tests/screenshot/assets/pixel_4a_\0505g\051/fp_enroll_intro.png"
deleted file mode 100644
index 1129250..0000000
--- "a/tests/screenshot/assets/pixel_4a_\0505g\051/fp_enroll_intro.png"
+++ /dev/null
Binary files differ
diff --git a/tests/screenshot/assets/robolectric/fp_enroll_intro.png b/tests/screenshot/assets/robolectric/fp_enroll_intro.png
new file mode 100644
index 0000000..308ab55
--- /dev/null
+++ b/tests/screenshot/assets/robolectric/fp_enroll_intro.png
Binary files differ
diff --git a/tests/screenshot/config/robolectric.properties b/tests/screenshot/config/robolectric.properties
new file mode 100644
index 0000000..88443e3
--- /dev/null
+++ b/tests/screenshot/config/robolectric.properties
@@ -0,0 +1,16 @@
+# 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.
+#
+sdk=NEWEST_SDK
+graphicsMode=NATIVE
\ No newline at end of file
diff --git a/tests/screenshot/src/com/android/settings/tests/screenshot/ContainerActivity.kt b/tests/screenshot/src/com/android/settings/tests/screenshot/ContainerActivity.kt
new file mode 100644
index 0000000..a505ef5
--- /dev/null
+++ b/tests/screenshot/src/com/android/settings/tests/screenshot/ContainerActivity.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.settings.tests.screenshot
+
+import android.os.Bundle
+import androidx.fragment.app.FragmentActivity
+import androidx.fragment.app.FragmentContainerView
+
+/**
+ * This activity is a container for all RNG (Robolectric Native Graphic) Settings screenshot tests.
+ */
+class ContainerActivity : FragmentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val contentView = FragmentContainerView(this)
+ contentView.setId(CONTAINER_VIEW_ID)
+ setContentView(contentView)
+ }
+
+ companion object {
+ const val CONTAINER_VIEW_ID = 1234
+ }
+}
\ No newline at end of file
diff --git a/tests/spa_unit/src/com/android/settings/network/SubscriptionInfoListViewModelTest.kt b/tests/spa_unit/src/com/android/settings/network/SubscriptionInfoListViewModelTest.kt
new file mode 100644
index 0000000..020a470
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/network/SubscriptionInfoListViewModelTest.kt
@@ -0,0 +1,153 @@
+/*
+ * 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.settings.network
+
+import android.telephony.SubscriptionManager.PROFILE_CLASS_PROVISIONING
+
+import android.app.Application
+import android.content.Context
+import android.platform.test.flag.junit.SetFlagsRule
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyManager
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.internal.telephony.flags.Flags
+import com.android.settings.network.telephony.CallStateFlowTest
+import com.android.settingslib.spa.testutils.toListWithTimeout
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.stub
+
+@RunWith(AndroidJUnit4::class)
+class SubscriptionInfoListViewModelTest {
+ @get:Rule
+ val mSetFlagsRule = SetFlagsRule()
+ private var subInfoListener: SubscriptionManager.OnSubscriptionsChangedListener? = null
+ private val mockSubscriptionManager = mock<SubscriptionManager> {
+ on { activeSubscriptionInfoList } doAnswer { activeSubscriptionInfoList }
+ on { addOnSubscriptionsChangedListener(any(), any()) } doAnswer {
+ subInfoListener =
+ it.arguments[1] as SubscriptionManager.OnSubscriptionsChangedListener
+ subInfoListener?.onSubscriptionsChanged()
+ }
+ }
+
+ private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+ on { getSystemService(SubscriptionManager::class.java) } doReturn mockSubscriptionManager
+ }
+
+ private val subscriptionInfoListViewModel: SubscriptionInfoListViewModel =
+ SubscriptionInfoListViewModel(context as Application);
+
+ private var activeSubscriptionInfoList: List<SubscriptionInfo>? = null
+
+ @Test
+ fun onSubscriptionsChanged_noProvisioning_resultSameAsInput() = runBlocking {
+ activeSubscriptionInfoList = listOf(SUB_INFO_1, SUB_INFO_2)
+
+ val listDeferred = async {
+ subscriptionInfoListViewModel.subscriptionInfoListFlow.toListWithTimeout()
+ }
+ delay(100)
+ subInfoListener?.onSubscriptionsChanged()
+
+ assertThat(listDeferred.await()).contains(activeSubscriptionInfoList)
+ }
+
+ @Test
+ fun onSubscriptionsChanged_hasProvisioning_filterProvisioning() = runBlocking {
+ activeSubscriptionInfoList = listOf(SUB_INFO_1, SUB_INFO_2, SUB_INFO_3)
+ val expectation = listOf(SUB_INFO_1, SUB_INFO_2)
+
+ val listDeferred = async {
+ subscriptionInfoListViewModel.subscriptionInfoListFlow.toListWithTimeout()
+ }
+ delay(100)
+ subInfoListener?.onSubscriptionsChanged()
+
+ assertThat(listDeferred.await()).contains(expectation)
+ }
+
+ @Test
+ fun onSubscriptionsChanged_flagOffHasNonTerrestrialNetwork_filterNonTerrestrialNetwork() =
+ runBlocking {
+ mSetFlagsRule.disableFlags(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+
+ activeSubscriptionInfoList = listOf(SUB_INFO_1, SUB_INFO_2, SUB_INFO_4)
+ val expectation = listOf(SUB_INFO_1, SUB_INFO_2, SUB_INFO_4)
+
+ val listDeferred = async {
+ subscriptionInfoListViewModel.subscriptionInfoListFlow.toListWithTimeout()
+ }
+ delay(100)
+ subInfoListener?.onSubscriptionsChanged()
+
+ assertThat(listDeferred.await()).contains(expectation)
+ }
+
+ @Test
+ fun onSubscriptionsChanged_flagOnHasNonTerrestrialNetwork_filterNonTerrestrialNetwork() =
+ runBlocking {
+ mSetFlagsRule.enableFlags(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+
+ activeSubscriptionInfoList = listOf(SUB_INFO_1, SUB_INFO_2, SUB_INFO_4)
+ val expectation = listOf(SUB_INFO_1, SUB_INFO_2)
+
+ val listDeferred = async {
+ subscriptionInfoListViewModel.subscriptionInfoListFlow.toListWithTimeout()
+ }
+ delay(100)
+ subInfoListener?.onSubscriptionsChanged()
+
+ assertThat(listDeferred.await()).contains(expectation)
+ }
+
+ private companion object {
+ val SUB_INFO_1: SubscriptionInfo = SubscriptionInfo.Builder().apply {
+ setId(1)
+ }.build()
+
+ val SUB_INFO_2: SubscriptionInfo = SubscriptionInfo.Builder().apply {
+ setId(2)
+ }.build()
+
+ val SUB_INFO_3: SubscriptionInfo = SubscriptionInfo.Builder().apply {
+ setId(3)
+ setEmbedded(true)
+ setProfileClass(PROFILE_CLASS_PROVISIONING)
+ setOnlyNonTerrestrialNetwork(false)
+ }.build()
+
+ val SUB_INFO_4: SubscriptionInfo = SubscriptionInfo.Builder().apply {
+ setId(4)
+ setEmbedded(true)
+ setOnlyNonTerrestrialNetwork(true)
+ }.build()
+ }
+}
\ No newline at end of file
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppArchiveButtonTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppArchiveButtonTest.kt
new file mode 100644
index 0000000..cc5e365
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppArchiveButtonTest.kt
@@ -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.settings.spa.app.appinfo
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInstaller
+import android.content.pm.PackageManager
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.CloudUpload
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.R
+import com.android.settingslib.spa.widget.button.ActionButton
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+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 AppArchiveButtonTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ private val context: Context = spy(ApplicationProvider.getApplicationContext()) {}
+
+ private val packageInfoPresenter = mock<PackageInfoPresenter>()
+
+ private val userPackageManager = mock<PackageManager>()
+
+ private val packageInstaller = mock<PackageInstaller>()
+
+ private lateinit var appArchiveButton: AppArchiveButton
+
+ @Before
+ fun setUp() {
+ whenever(packageInfoPresenter.context).thenReturn(context)
+ whenever(packageInfoPresenter.userPackageManager).thenReturn(userPackageManager)
+ whenever(userPackageManager.packageInstaller).thenReturn(packageInstaller)
+ whenever(packageInfoPresenter.packageName).thenReturn(PACKAGE_NAME)
+ appArchiveButton = AppArchiveButton(packageInfoPresenter)
+ }
+
+ @Test
+ fun appArchiveButton_whenIsArchived_isDisabled() {
+ val app = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ isArchived = true
+ }
+ whenever(userPackageManager.isAppArchivable(app.packageName)).thenReturn(true)
+
+ val actionButton = setContent(app)
+
+ assertThat(actionButton.enabled).isFalse()
+ }
+
+ @Test
+ fun appArchiveButton_whenIsNotAppArchivable_isDisabled() {
+ val app = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ isArchived = false
+ }
+ whenever(userPackageManager.isAppArchivable(app.packageName)).thenReturn(false)
+
+ val actionButton = setContent(app)
+
+ assertThat(actionButton.enabled).isFalse()
+ }
+
+ @Test
+ fun appArchiveButton_displaysRightTextAndIcon() {
+ val app = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ isArchived = false
+ }
+ whenever(userPackageManager.isAppArchivable(app.packageName)).thenReturn(true)
+
+ val actionButton = setContent(app)
+
+ assertThat(actionButton.text).isEqualTo(context.getString(R.string.archive))
+ assertThat(actionButton.imageVector).isEqualTo(Icons.Outlined.CloudUpload)
+ }
+
+ @Test
+ fun appArchiveButton_clicked() {
+ val app = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ isArchived = false
+ }
+ whenever(userPackageManager.isAppArchivable(app.packageName)).thenReturn(true)
+
+ val actionButton = setContent(app)
+ actionButton.onClick()
+
+ verify(packageInstaller).requestArchive(
+ eq(PACKAGE_NAME),
+ any(),
+ eq(0)
+ )
+ }
+
+ private fun setContent(app: ApplicationInfo): ActionButton {
+ lateinit var actionButton: ActionButton
+ composeTestRule.setContent {
+ actionButton = appArchiveButton.getActionButton(app)
+ }
+ return actionButton
+ }
+
+ private companion object {
+ const val PACKAGE_NAME = "package.name"
+ }
+}
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt
index e2f55ef..6d22c92 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt
@@ -22,8 +22,10 @@
import android.content.pm.FakeFeatureFlagsImpl
import android.content.pm.Flags
import android.content.pm.PackageInfo
+import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
@@ -62,6 +64,9 @@
@Mock
private lateinit var packageManager: PackageManager
+ @Mock
+ private lateinit var packageInstaller: PackageInstaller
+
private val featureFlags = FakeFeatureFlagsImpl()
@Before
@@ -74,6 +79,7 @@
whenever(packageInfoPresenter.context).thenReturn(context)
whenever(packageInfoPresenter.packageName).thenReturn(PACKAGE_NAME)
whenever(packageInfoPresenter.userPackageManager).thenReturn(packageManager)
+ whenever(packageManager.packageInstaller).thenReturn(packageInstaller)
whenever(packageManager.getPackageInfo(PACKAGE_NAME, 0)).thenReturn(PACKAGE_INFO)
whenever(AppUtils.isMainlineModule(packageManager, PACKAGE_NAME)).thenReturn(false)
featureFlags.setFlag(Flags.FLAG_ARCHIVING, true)
@@ -118,8 +124,56 @@
composeTestRule.onNodeWithText(context.getString(R.string.launch_instant_app)).assertIsNotDisplayed()
}
- private fun setContent() {
- whenever(packageInfoPresenter.flow).thenReturn(MutableStateFlow(PACKAGE_INFO))
+ @Test
+ fun uninstallButton_enabled_whenAppIsArchived() {
+ whenever(packageManager.getLaunchIntentForPackage(PACKAGE_NAME)).thenReturn(Intent())
+ featureFlags.setFlag(Flags.FLAG_ARCHIVING, true)
+ val packageInfo = PackageInfo().apply {
+ applicationInfo = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ isArchived = true
+ }
+ packageName = PACKAGE_NAME
+ }
+ setContent(packageInfo)
+
+ composeTestRule.onNodeWithText(context.getString(R.string.uninstall_text)).assertIsEnabled()
+ }
+
+ @Test
+ fun archiveButton_displayed_whenAppIsNotArchived() {
+ featureFlags.setFlag(Flags.FLAG_ARCHIVING, true)
+ val packageInfo = PackageInfo().apply {
+ applicationInfo = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ isArchived = false
+ }
+ packageName = PACKAGE_NAME
+ }
+ setContent(packageInfo)
+
+ composeTestRule.onNodeWithText(context.getString(R.string.archive)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(context.getString(R.string.restore)).assertIsNotDisplayed()
+ }
+
+ @Test
+ fun restoreButton_displayed_whenAppIsArchived() {
+ featureFlags.setFlag(Flags.FLAG_ARCHIVING, true)
+ val packageInfo = PackageInfo().apply {
+ applicationInfo = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ isArchived = true
+ }
+ packageName = PACKAGE_NAME
+ }
+ setContent(packageInfo)
+
+ composeTestRule.onNodeWithText(context.getString(R.string.restore)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(context.getString(R.string.archive)).assertIsNotDisplayed()
+ }
+
+ private fun setContent(packageInfo: PackageInfo = PACKAGE_INFO) {
+ whenever(packageInfoPresenter.flow).thenReturn(MutableStateFlow(packageInfo))
composeTestRule.setContent {
AppButtons(packageInfoPresenter, featureFlags)
}
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreferenceTest.kt
index d1c318a..31722c9 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreferenceTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreferenceTest.kt
@@ -21,14 +21,11 @@
import android.content.pm.ApplicationInfo
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotDisplayed
-import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.isEnabled
+import androidx.compose.ui.test.isNotEnabled
import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
import androidx.test.core.app.ApplicationProvider
@@ -122,9 +119,8 @@
.thenReturn(null)
setContent()
- waitUntilDisplayed()
- composeTestRule.onNode(preferenceNode).assertIsNotEnabled()
+ composeTestRule.waitUntilExists(preferenceNode.and(isNotEnabled()))
}
@Test
@@ -143,17 +139,16 @@
@Test
fun whenNotInstantApp() {
setContent()
- waitUntilDisplayed()
- composeTestRule.onNodeWithText("App installed from installer label")
- .assertIsDisplayed()
- .assertIsEnabled()
+ composeTestRule.waitUntilExists(hasText("App installed from installer label"))
+ composeTestRule.waitUntilExists(preferenceNode.and(isEnabled()))
}
@Test
fun whenClick_startActivity() {
setContent()
- waitUntilDisplayed()
+ composeTestRule.delay()
+
composeTestRule.onRoot().performClick()
composeTestRule.delay()
@@ -168,14 +163,10 @@
}
}
- private fun waitUntilDisplayed() {
- composeTestRule.waitUntilExists(preferenceNode)
- }
-
private val preferenceNode = hasText(context.getString(R.string.app_install_details_title))
private companion object {
- const val PACKAGE_NAME = "packageName"
+ const val PACKAGE_NAME = "package.name"
const val INSTALLER_PACKAGE_NAME = "installer"
const val INSTALLER_PACKAGE_LABEL = "installer label"
val STORE_LINK = Intent("store/link")
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppRestoreButtonTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppRestoreButtonTest.kt
new file mode 100644
index 0000000..9d30521
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppRestoreButtonTest.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.settings.spa.app.appinfo
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInstaller
+import android.content.pm.PackageManager
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.CloudDownload
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.annotation.UiThreadTest
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.R
+import com.android.settingslib.spa.widget.button.ActionButton
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+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 AppRestoreButtonTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ private val context: Context = spy(ApplicationProvider.getApplicationContext()) {}
+
+ private val packageInfoPresenter = mock<PackageInfoPresenter>()
+
+ private val userPackageManager = mock<PackageManager>()
+
+ private val packageInstaller = mock<PackageInstaller>()
+
+ private lateinit var appRestoreButton: AppRestoreButton
+
+ @Before
+ fun setUp() {
+ whenever(packageInfoPresenter.context).thenReturn(context)
+ whenever(packageInfoPresenter.userPackageManager).thenReturn(userPackageManager)
+ whenever(userPackageManager.packageInstaller).thenReturn(packageInstaller)
+ whenever(packageInfoPresenter.packageName).thenReturn(PACKAGE_NAME)
+ appRestoreButton = AppRestoreButton(packageInfoPresenter)
+ }
+
+ @Test
+ fun appRestoreButton_whenIsNotArchived_isDisabled() {
+ val app = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ isArchived = false
+ }
+
+ val actionButton = setContent(app)
+
+ assertThat(actionButton.enabled).isFalse()
+ }
+
+ @Test
+ fun appRestoreButton_whenIsArchived_isEnabled() {
+ val app = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ isArchived = true
+ }
+
+ val actionButton = setContent(app)
+
+ assertThat(actionButton.enabled).isTrue()
+ }
+
+ @Test
+ fun appRestoreButton_displaysRightTextAndIcon() {
+ val app = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ isArchived = false
+ }
+
+ val actionButton = setContent(app)
+
+ assertThat(actionButton.text).isEqualTo(context.getString(R.string.restore))
+ assertThat(actionButton.imageVector).isEqualTo(Icons.Outlined.CloudDownload)
+ }
+
+ @Test
+ @UiThreadTest
+ fun appRestoreButton_clicked() {
+ val app = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ isArchived = true
+ }
+
+ val actionButton = setContent(app)
+ actionButton.onClick()
+
+ verify(packageInstaller).requestUnarchive(
+ eq(PACKAGE_NAME),
+ any()
+ )
+ }
+
+ private fun setContent(app: ApplicationInfo): ActionButton {
+ lateinit var actionButton: ActionButton
+ composeTestRule.setContent {
+ actionButton = appRestoreButton.getActionButton(app)
+ }
+ return actionButton
+ }
+
+ private companion object {
+ const val PACKAGE_NAME = "package.name"
+ }
+}
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/PackageInfoPresenterTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/PackageInfoPresenterTest.kt
index d81bb1a..5dd66e8 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/PackageInfoPresenterTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/PackageInfoPresenterTest.kt
@@ -105,6 +105,7 @@
fun isInterestedAppChange_archived_interested() {
val intent = Intent(Intent.ACTION_PACKAGE_REMOVED).apply {
data = Uri.parse("package:$PACKAGE_NAME")
+ putExtra(Intent.EXTRA_ARCHIVAL, true)
}
val isInterestedAppChange = packageInfoPresenter.isInterestedAppChange(intent)
diff --git a/tests/unit/src/com/android/settings/development/BackAnimationPreferenceControllerTest.java b/tests/unit/src/com/android/settings/development/BackAnimationPreferenceControllerTest.java
index 3669358..dc4f56a 100644
--- a/tests/unit/src/com/android/settings/development/BackAnimationPreferenceControllerTest.java
+++ b/tests/unit/src/com/android/settings/development/BackAnimationPreferenceControllerTest.java
@@ -29,6 +29,10 @@
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
+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 androidx.preference.PreferenceManager;
@@ -37,8 +41,11 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.window.flags.Flags;
+
import org.junit.Assert;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
@@ -58,6 +65,9 @@
private BackAnimationPreferenceController mController;
private Looper mLooper;
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
@@ -81,6 +91,18 @@
}
@Test
+ @RequiresFlagsEnabled(Flags.FLAG_PREDICTIVE_BACK_SYSTEM_ANIMATIONS)
+ public void controllerNotAvailable_whenAconfigFlagEnabled() {
+ assertFalse(mController.isAvailable());
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_PREDICTIVE_BACK_SYSTEM_ANIMATIONS)
+ public void controllerAvailable_whenAconfigFlagDisabled() {
+ assertTrue(mController.isAvailable());
+ }
+
+ @Test
public void onPreferenceChange_switchEnabled_shouldEnableBackAnimations() {
mController.onPreferenceChange(mPreference, true /* new value */);
diff --git a/tests/unit/src/com/android/settings/privatespace/PrivateSpaceMaintainerTest.java b/tests/unit/src/com/android/settings/privatespace/PrivateSpaceMaintainerTest.java
index 0a2f3d1..1d27326 100644
--- a/tests/unit/src/com/android/settings/privatespace/PrivateSpaceMaintainerTest.java
+++ b/tests/unit/src/com/android/settings/privatespace/PrivateSpaceMaintainerTest.java
@@ -21,8 +21,11 @@
import static com.google.common.truth.Truth.assertThat;
+import android.app.ActivityManager;
+import android.app.IActivityManager;
import android.content.ContentResolver;
import android.content.Context;
+import android.os.RemoteException;
import android.provider.Settings;
import androidx.test.core.app.ApplicationProvider;
@@ -30,6 +33,8 @@
import com.android.settings.privatespace.PrivateSpaceMaintainer.ErrorDeletingPrivateSpace;
+import org.junit.After;
+import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -37,6 +42,7 @@
@RunWith(AndroidJUnit4.class)
public class PrivateSpaceMaintainerTest {
+ private static final String TAG = "PSMaintainerTest";
private Context mContext;
private ContentResolver mContentResolver;
@@ -48,6 +54,13 @@
mContentResolver = mContext.getContentResolver();
}
+ @After
+ public void tearDown() {
+ PrivateSpaceMaintainer privateSpaceMaintainer =
+ PrivateSpaceMaintainer.getInstance(mContext);
+ privateSpaceMaintainer.deletePrivateSpace();
+ }
+
/** Tests that {@link PrivateSpaceMaintainer#deletePrivateSpace()} deletes PS when PS exists. */
@Test
public void deletePrivateSpace_psExists_deletesPS() {
@@ -137,4 +150,52 @@
assertThat(privateSpaceMaintainer.getHidePrivateSpaceEntryPointSetting())
.isEqualTo(HIDE_PRIVATE_SPACE_ENTRY_POINT_ENABLED_VAL);
}
+
+ /**
+ * Tests that {@link PrivateSpaceMaintainer#lockPrivateSpace()} when PS exists and is running
+ * locks the private profile.
+ */
+ @Test
+ public void lockPrivateSpace_psExistsAndPrivateProfileRunning_locksCreatedPrivateSpace() {
+ PrivateSpaceMaintainer privateSpaceMaintainer =
+ PrivateSpaceMaintainer.getInstance(mContext);
+ privateSpaceMaintainer.createPrivateSpace();
+ assertThat(privateSpaceMaintainer.doesPrivateSpaceExist()).isTrue();
+ assertThat(privateSpaceMaintainer.isPrivateProfileRunning()).isTrue();
+ assertThat(privateSpaceMaintainer.isPrivateSpaceLocked()).isFalse();
+ assertThat(privateSpaceMaintainer.lockPrivateSpace()).isTrue();
+ assertThat(privateSpaceMaintainer.isPrivateSpaceLocked()).isTrue();
+ }
+
+ /**
+ * Tests that {@link PrivateSpaceMaintainer#lockPrivateSpace()} when PS exist and private
+ * profile not running returns false.
+ */
+ @Test
+ public void lockPrivateSpace_psExistsAndPrivateProfileNotRunning_returnsFalse() {
+ PrivateSpaceMaintainer privateSpaceMaintainer =
+ PrivateSpaceMaintainer.getInstance(mContext);
+ privateSpaceMaintainer.createPrivateSpace();
+ assertThat(privateSpaceMaintainer.doesPrivateSpaceExist()).isTrue();
+ assertThat(privateSpaceMaintainer.isPrivateProfileRunning()).isTrue();
+ IActivityManager am = ActivityManager.getService();
+ try {
+ am.stopProfile(privateSpaceMaintainer.getPrivateProfileHandle().getIdentifier());
+ } catch (RemoteException e) {
+ Assert.fail("Stop profile failed with exception " + e.getMessage());
+ }
+ assertThat(privateSpaceMaintainer.isPrivateProfileRunning()).isFalse();
+ assertThat(privateSpaceMaintainer.lockPrivateSpace()).isFalse();
+ }
+
+ /**
+ * Tests that {@link PrivateSpaceMaintainer#lockPrivateSpace()} when no PS exists returns false.
+ */
+ @Test
+ public void lockPrivateSpace_psDoesNotExist_returnsFalse() {
+ PrivateSpaceMaintainer privateSpaceMaintainer =
+ PrivateSpaceMaintainer.getInstance(mContext);
+ assertThat(privateSpaceMaintainer.doesPrivateSpaceExist()).isFalse();
+ assertThat(privateSpaceMaintainer.lockPrivateSpace()).isFalse();
+ }
}