Merge "Change screenshot test to use RNG" into main
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 2a45624..2b49148 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -4200,8 +4200,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/layout/dialog_audio_sharing.xml b/res/layout/dialog_audio_sharing.xml
index 3b11020..3ea2c01 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" />
+
+ <com.android.internal.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..54dee40 100644
--- a/res/layout/dialog_audio_sharing_disconnect.xml
+++ b/res/layout/dialog_audio_sharing_disconnect.xml
@@ -19,29 +19,31 @@
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" />
+
+ <com.android.internal.widget.RecyclerView
+ android:id="@+id/device_btn_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center" />
+
+ <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/values/strings.xml b/res/values/strings.xml
index 650dcfe..33cae6d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -3268,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 -->
@@ -3894,6 +3896,8 @@
<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, 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) -->
@@ -4004,6 +4008,11 @@
<!-- 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>
+
<!-- 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>
@@ -4637,8 +4646,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] -->
@@ -4651,8 +4660,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] -->
@@ -4695,12 +4704,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] -->
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/AudioSharingDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java
index 5cd86b4..e44939d 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.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;
@@ -33,8 +34,10 @@
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,62 @@
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();
+ }));
+ 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..7eedb9a 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.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;
@@ -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(
@@ -116,7 +118,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/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..6cf69c5 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
@@ -18,8 +18,6 @@
import static java.util.Collections.emptyList;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothLeAudioContentMetadata;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
@@ -35,19 +33,12 @@
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
import com.android.settings.core.BasePreferenceController;
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.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
-import java.util.stream.Stream;
import javax.annotation.Nullable;
@@ -58,17 +49,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 +95,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 +127,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 +166,43 @@
}
});
}
- // 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) {
+ // TODO(chelseahao): impl
+ return true;
+ }
+
+ private void launchPasswordDialog(BluetoothLeBroadcastMetadata source, Preference preference) {
+ // TODO(chelseahao): impl
+ }
}
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/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/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/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/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/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/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..7a127bb 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/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..f6fafd7 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,7 @@
private val appUninstallButton = AppUninstallButton(packageInfoPresenter)
private val appClearButton = AppClearButton(packageInfoPresenter)
private val appForceStopButton = AppForceStopButton(packageInfoPresenter)
+ private val appArchiveButton = AppArchiveButton(packageInfoPresenter)
@Composable
fun getActionButtons() =
@@ -58,7 +62,11 @@
@Composable
private fun getActionButtons(app: ApplicationInfo): List<ActionButton> = listOfNotNull(
- if (featureFlags.archiving()) null else appLaunchButton.getActionButton(app),
+ if (featureFlags.archiving()) {
+ 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/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/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/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt b/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt
index c990927..3e48aa5 100644
--- a/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt
+++ b/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt
@@ -18,6 +18,7 @@
import android.Manifest
import android.app.AlarmManager
+import android.app.AppOpsManager
import android.app.compat.CompatChanges
import android.app.settings.SettingsEnums
import android.content.Context
@@ -56,6 +57,7 @@
override val pageTitleResId = R.string.alarms_and_reminders_title
override val switchTitleResId = R.string.alarms_and_reminders_switch_title
override val footerResId = R.string.alarms_and_reminders_footer_title
+ override val enhancedConfirmationKey: String = AppOpsManager.OPSTR_SCHEDULE_EXACT_ALARM
override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
userIdFlow.map { userId ->
diff --git a/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt b/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt
index 7f63e38..dc98330 100644
--- a/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt
+++ b/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt
@@ -18,6 +18,7 @@
import android.Manifest
import android.app.AppGlobals
+import android.app.AppOpsManager
import android.app.AppOpsManager.MODE_DEFAULT
import android.app.AppOpsManager.OP_REQUEST_INSTALL_PACKAGES
import android.content.Context
@@ -55,6 +56,7 @@
UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES,
UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY,
)
+ override val enhancedConfirmationKey: String = AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES
override fun transformItem(app: ApplicationInfo) =
InstallUnknownAppsRecord(
diff --git a/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt b/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt
index cd615919..fe8f103 100644
--- a/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt
+++ b/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt
@@ -16,6 +16,7 @@
package com.android.settings.spa.app.specialaccess
+import android.app.AppOpsManager
import android.app.AppOpsManager.OP_PICTURE_IN_PICTURE
import android.content.Context
import android.content.pm.ActivityInfo
@@ -53,6 +54,7 @@
override val pageTitleResId = R.string.picture_in_picture_title
override val switchTitleResId = R.string.picture_in_picture_app_detail_switch
override val footerResId = R.string.picture_in_picture_app_detail_summary
+ override val enhancedConfirmationKey: String = AppOpsManager.OPSTR_PICTURE_IN_PICTURE
private val packageManager = context.packageManager
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..86c724c
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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 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.After;
+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() {
+ 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);
+ }
+
+ @After
+ public void tearDown() {
+ ShadowAlertDialogCompat.reset();
+ }
+
+ @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(TEST_DEVICE_NAME1).isEqualTo(subtitle1.getText());
+ 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);
+ assertThat(dialog.isShowing()).isTrue();
+ assertThat(subtitle1.getVisibility()).isEqualTo(View.GONE);
+ assertThat(guidance.getVisibility()).isEqualTo(View.GONE);
+ assertThat(shareBtn.getVisibility()).isEqualTo(View.GONE);
+ }
+
+ @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..976e164
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragmentTest.java
@@ -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.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 com.android.internal.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.After;
+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() {
+ 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();
+ }
+
+ @After
+ public void tearDown() {
+ ShadowAlertDialogCompat.reset();
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void onCreateDialog_flagOff_dialogNotExist() {
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNotNull();
+ }
+
+ @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..5eb0e8a
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragmentTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.After;
+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() {
+ 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);
+ }
+
+ @After
+ public void tearDown() {
+ ShadowAlertDialogCompat.reset();
+ }
+
+ @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..1de7b2d
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragmentTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.After;
+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() {
+ 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);
+ }
+
+ @After
+ public void tearDown() {
+ ShadowAlertDialogCompat.reset();
+ }
+
+ @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..f306693 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
@@ -29,6 +29,7 @@
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@@ -60,6 +61,7 @@
verify(mResolver).notifyChange(URI, null);
}
+ @Ignore("b/315399487")
@Test
public void onActiveDeviceChanged_shouldNotifyChange() {
mBluetoothUpdateWorker.onActiveDeviceChanged(null, 0);
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..50094f2 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,24 @@
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()
+ }
+
+ 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/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 */);