Snap for 11869550 from a24c7183d2 to 24Q3-release

Change-Id: I8c19ffeb13b2e3936e041c985887daf626eb39c5
This commit is contained in:
Android Build Coastguard Worker
2024-05-21 23:21:58 +00:00
71 changed files with 3605 additions and 310 deletions

View File

@@ -758,4 +758,8 @@
<string name="audio_streams_dialog_no_le_device_subtitle" product="default">To listen to an audio stream, first connect headphones that support LE Audio to this phone.</string>
<string name="audio_streams_dialog_no_le_device_subtitle" product="tablet">To listen to an audio stream, first connect headphones that support LE Audio to this tablet.</string>
<string name="audio_streams_dialog_no_le_device_subtitle" product="device">To listen to an audio stream, first connect headphones that support LE Audio to this device.</string>
<!-- Le audio streams unsupported device subtitle [CHAR LIMIT=NONE] -->
<string name="audio_streams_dialog_unsupported_device_subtitle" product="default">This phone doesn\'t support LE Audio, which is needed to listen to audio streams.</string>
<string name="audio_streams_dialog_unsupported_device_subtitle" product="tablet">This tablet doesn\'t support LE Audio, which is needed to listen to audio streams.</string>
<string name="audio_streams_dialog_unsupported_device_subtitle" product="device">This device doesn\'t support LE Audio, which is needed to listen to audio streams.</string>
</resources>

View File

@@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<FrameLayout
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -26,4 +26,4 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
</FrameLayout>
</ScrollView>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:key="zen_mode_calls_settings_page"
settings:searchable="false"
android:title="@string/zen_mode_calls_title">
<PreferenceCategory
android:key="zen_mode_settings_category_calls"
android:title="@string/zen_mode_calls_header"
settings:allowDividerBelow="true">
</PreferenceCategory>
<!-- Repeat callers -->
<SwitchPreferenceCompat
android:key="zen_mode_repeat_callers"
android:title="@string/zen_mode_repeat_callers_title"
settings:allowDividerAbove="true"/>
</PreferenceScreen>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:key="zen_mode_messages_settings_page"
settings:searchable="false"
android:title="@string/zen_mode_messages_title" >
<PreferenceCategory
android:key="zen_mode_settings_category_messages"
android:title="@string/zen_mode_messages_header">
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
android:title="@string/zen_category_exceptions" >
<!-- Alarms -->
<SwitchPreferenceCompat
android:key="modes_category_alarm"
android:title="@string/zen_mode_alarms"/>
<!-- Media -->
<SwitchPreferenceCompat
android:key="modes_category_media"
android:title="@string/zen_mode_media"
android:summary="@string/zen_mode_media_summary"/>
<!-- System -->
<SwitchPreferenceCompat
android:key="modes_category_system"
android:title="@string/zen_mode_system"
android:summary="@string/zen_mode_system_summary"/>
<!-- Reminders -->
<SwitchPreferenceCompat
android:key="modes_category_reminders"
android:title="@string/zen_mode_reminders"/>
<!-- Events -->
<SwitchPreferenceCompat
android:key="modes_category_events"
android:title="@string/zen_mode_events"/>
</PreferenceScreen>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
android:title="@string/zen_category_people" >
<!-- Calls & Messages -->
<PreferenceCategory
android:key="zen_mode_people_calls_messages_section"
android:title="@string/zen_mode_people_calls_messages_section_title">
<Preference
android:key="zen_mode_people_messages"
android:title="@string/zen_mode_messages_title"/>
<Preference
android:key="zen_mode_people_calls"
android:title="@string/zen_mode_calls_title"/>
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -22,4 +22,12 @@
android:key="header"
android:layout="@layout/settings_entity_header" />
<Preference
android:key="zen_mode_people"
android:title="@string/zen_category_people"/>
<Preference
android:key="zen_other_settings"
android:title="@string/zen_category_exceptions" />
</PreferenceScreen>

View File

@@ -34,7 +34,6 @@ import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.accessibility.MagnificationCapabilities.MagnificationMode;
import com.android.settings.core.TogglePreferenceController;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnPause;
import com.android.settingslib.core.lifecycle.events.OnResume;
@@ -44,8 +43,8 @@ import com.android.settingslib.core.lifecycle.events.OnResume;
* feature, where the magnifier will not deactivate on Activity transitions; it will only zoom out
* to 100%.
*/
public class MagnificationAlwaysOnPreferenceController extends TogglePreferenceController
implements LifecycleObserver, OnResume, OnPause {
public class MagnificationAlwaysOnPreferenceController extends
MagnificationFeaturePreferenceController implements LifecycleObserver, OnResume, OnPause {
private static final String TAG =
MagnificationAlwaysOnPreferenceController.class.getSimpleName();
@@ -89,7 +88,7 @@ public class MagnificationAlwaysOnPreferenceController extends TogglePreferenceC
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
return isInSetupWizard() ? CONDITIONALLY_UNAVAILABLE : AVAILABLE;
}
@Override

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.accessibility;
import android.content.Context;
import com.android.settings.core.TogglePreferenceController;
/**
* A base preference controller of magnification feature with common methods.
*/
public abstract class MagnificationFeaturePreferenceController extends TogglePreferenceController {
private boolean mInSetupWizard;
protected MagnificationFeaturePreferenceController(Context context,
String preferenceKey) {
super(context, preferenceKey);
}
protected final boolean isInSetupWizard() {
return mInSetupWizard;
}
protected final void setInSetupWizard(boolean inSetupWizard) {
mInSetupWizard = inSetupWizard;
}
}

View File

@@ -23,10 +23,10 @@ import android.content.Context;
import android.provider.Settings;
import com.android.settings.R;
import com.android.settings.core.TogglePreferenceController;
/** Controller that accesses and switches the preference status of following typing feature */
public class MagnificationFollowTypingPreferenceController extends TogglePreferenceController {
public class MagnificationFollowTypingPreferenceController extends
MagnificationFeaturePreferenceController {
private static final String TAG =
MagnificationFollowTypingPreferenceController.class.getSimpleName();
@@ -38,7 +38,7 @@ public class MagnificationFollowTypingPreferenceController extends TogglePrefere
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
return isInSetupWizard() ? CONDITIONALLY_UNAVAILABLE : AVAILABLE;
}
@Override

View File

@@ -23,12 +23,12 @@ import android.content.Context;
import android.provider.Settings;
import com.android.settings.R;
import com.android.settings.core.TogglePreferenceController;
/**
* Controller that accesses and switches the preference status of the magnification joystick feature
*/
public class MagnificationJoystickPreferenceController extends TogglePreferenceController {
public class MagnificationJoystickPreferenceController extends
MagnificationFeaturePreferenceController {
private static final String TAG =
MagnificationJoystickPreferenceController.class.getSimpleName();
@@ -40,7 +40,7 @@ public class MagnificationJoystickPreferenceController extends TogglePreferenceC
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
return isInSetupWizard() ? CONDITIONALLY_UNAVAILABLE : AVAILABLE;
}
@Override

View File

@@ -35,13 +35,12 @@ import androidx.preference.TwoStatePreference;
import com.android.settings.R;
import com.android.settings.accessibility.MagnificationCapabilities.MagnificationMode;
import com.android.settings.core.TogglePreferenceController;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnPause;
import com.android.settingslib.core.lifecycle.events.OnResume;
public class MagnificationOneFingerPanningPreferenceController
extends TogglePreferenceController implements LifecycleObserver, OnResume, OnPause {
public class MagnificationOneFingerPanningPreferenceController extends
MagnificationFeaturePreferenceController implements LifecycleObserver, OnResume, OnPause {
static final String PREF_KEY = Settings.Secure.ACCESSIBILITY_SINGLE_FINGER_PANNING_ENABLED;
private TwoStatePreference mSwitchPreference;
@@ -82,7 +81,7 @@ public class MagnificationOneFingerPanningPreferenceController
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
return isInSetupWizard() ? CONDITIONALLY_UNAVAILABLE : AVAILABLE;
}
@Override

View File

@@ -15,5 +15,5 @@ menghanli@google.com #{LAST_RESORT_SUGGESTION}
cipson@google.com #{LAST_RESORT_SUGGESTION}
# Partner-team files
per-file HapticFeedbackIntensityPreferenceController.java = file:platform/frameworks/base:/services/core/java/com/android/server/vibrator/OWNERS
per-file *Vibration* = file:platform/frameworks/base:/services/core/java/com/android/server/vibrator/OWNERS
per-file *Haptic* = file:platform/frameworks/base:/services/core/java/com/android/server/vibrator/OWNERS
per-file *Vibrat* = file:platform/frameworks/base:/services/core/java/com/android/server/vibrator/OWNERS

View File

@@ -82,8 +82,6 @@ public class ToggleScreenMagnificationPreferenceFragment extends
private static final TextUtils.SimpleStringSplitter sStringColonSplitter =
new TextUtils.SimpleStringSplitter(COMPONENT_NAME_SEPARATOR);
protected TwoStatePreference mFollowingTypingSwitchPreference;
// TODO(b/147021230): Move duplicated functions with android/internal/accessibility into util.
private TouchExplorationStateChangeListener mTouchExplorationStateChangeListener;
private CheckBox mSoftwareTypeCheckBox;
@@ -92,10 +90,13 @@ public class ToggleScreenMagnificationPreferenceFragment extends
@Nullable private CheckBox mTwoFingerTripleTapTypeCheckBox;
private DialogCreatable mDialogDelegate;
private boolean mInSetupWizard;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getActivity().setTitle(R.string.accessibility_screen_magnification_title);
mInSetupWizard = WizardManagerHelper.isAnySetupWizard(getIntent());
}
@Override
@@ -169,7 +170,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends
.showAccessibilityGestureTutorialDialog(getPrefContext());
case DialogEnums.MAGNIFICATION_EDIT_SHORTCUT:
final CharSequence dialogTitle = getShortcutTitle();
final int dialogType = WizardManagerHelper.isAnySetupWizard(getIntent())
final int dialogType = mInSetupWizard
? DialogType.EDIT_SHORTCUT_MAGNIFICATION_SUW
: DialogType.EDIT_SHORTCUT_MAGNIFICATION;
mDialog = AccessibilityDialogUtils.showEditShortcutDialog(getPrefContext(),
@@ -240,17 +241,18 @@ public class ToggleScreenMagnificationPreferenceFragment extends
}
private void addFollowTypingSetting(PreferenceCategory generalCategory) {
mFollowingTypingSwitchPreference = new SwitchPreferenceCompat(getPrefContext());
mFollowingTypingSwitchPreference.setTitle(
var followingTypingSwitchPreference = new SwitchPreferenceCompat(getPrefContext());
followingTypingSwitchPreference.setTitle(
R.string.accessibility_screen_magnification_follow_typing_title);
mFollowingTypingSwitchPreference.setSummary(
followingTypingSwitchPreference.setSummary(
R.string.accessibility_screen_magnification_follow_typing_summary);
mFollowingTypingSwitchPreference.setKey(
followingTypingSwitchPreference.setKey(
MagnificationFollowTypingPreferenceController.PREF_KEY);
generalCategory.addPreference(mFollowingTypingSwitchPreference);
generalCategory.addPreference(followingTypingSwitchPreference);
var followTypingPreferenceController = new MagnificationFollowTypingPreferenceController(
getContext(), MagnificationFollowTypingPreferenceController.PREF_KEY);
followTypingPreferenceController.setInSetupWizard(mInSetupWizard);
followTypingPreferenceController.displayPreference(getPreferenceScreen());
addPreferenceController(followTypingPreferenceController);
}
@@ -282,6 +284,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends
var alwaysOnPreferenceController = new MagnificationAlwaysOnPreferenceController(
getContext(), MagnificationAlwaysOnPreferenceController.PREF_KEY);
alwaysOnPreferenceController.setInSetupWizard(mInSetupWizard);
getSettingsLifecycle().addObserver(alwaysOnPreferenceController);
alwaysOnPreferenceController.displayPreference(getPreferenceScreen());
addPreferenceController(alwaysOnPreferenceController);
@@ -301,6 +304,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends
var oneFingerPanningPreferenceController =
new MagnificationOneFingerPanningPreferenceController(getContext());
oneFingerPanningPreferenceController.setInSetupWizard(mInSetupWizard);
getSettingsLifecycle().addObserver(oneFingerPanningPreferenceController);
oneFingerPanningPreferenceController.displayPreference(getPreferenceScreen());
addPreferenceController(oneFingerPanningPreferenceController);
@@ -329,6 +333,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends
getContext(),
MagnificationJoystickPreferenceController.PREF_KEY
);
joystickPreferenceController.setInSetupWizard(mInSetupWizard);
joystickPreferenceController.displayPreference(getPreferenceScreen());
addPreferenceController(joystickPreferenceController);
}

View File

@@ -72,10 +72,6 @@ public class ToggleScreenMagnificationPreferenceFragmentForSetupWizard
if (mSettingsPreference != null) {
mSettingsPreference.setVisible(false);
}
// Setting of following typing
if (mFollowingTypingSwitchPreference != null) {
mFollowingTypingSwitchPreference.setVisible(false);
}
}
@Override

View File

@@ -28,6 +28,8 @@ import static android.app.admin.DevicePolicyResources.Strings.Settings.ONLY_CONN
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
import static android.provider.Settings.ACTION_MANAGE_CROSS_PROFILE_ACCESS;
import static android.provider.Settings.Global.CONNECTED_APPS_ALLOWED_PACKAGES;
import static android.provider.Settings.Global.CONNECTED_APPS_DISALLOWED_PACKAGES;
import android.Manifest;
import android.annotation.UserIdInt;
@@ -35,6 +37,7 @@ import android.app.ActionBar;
import android.app.AppOpsManager;
import android.app.admin.DevicePolicyEventLogger;
import android.app.admin.DevicePolicyManager;
import android.app.admin.flags.Flags;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.DialogInterface;
@@ -49,6 +52,7 @@ import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.stats.devicepolicy.DevicePolicyEnums;
import android.util.IconDrawableFactory;
import android.view.LayoutInflater;
@@ -68,6 +72,10 @@ import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedSwitchPreference;
import com.android.settingslib.widget.LayoutPreference;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
public class InteractAcrossProfilesDetails extends AppInfoBase
implements Preference.OnPreferenceClickListener {
@@ -381,6 +389,7 @@ public class InteractAcrossProfilesDetails extends AppInfoBase
private void enableInteractAcrossProfiles(boolean newState) {
mCrossProfileApps.setInteractAcrossProfilesAppOp(
mPackageName, newState ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED);
setUserPreferenceForPackage(newState, mPackageName);
}
private void handleInstallBannerClick() {
@@ -552,4 +561,40 @@ public class InteractAcrossProfilesDetails extends AppInfoBase
}
return ACTION_MANAGE_CROSS_PROFILE_ACCESS.equals(intent.getAction());
}
private void setUserPreferenceForPackage(boolean enabled, String crossProfilePackage) {
if (!Flags.backupConnectedAppsSettings()) {
return;
}
String allowedPackagesString = Settings.Global.getString(getContentResolver(),
CONNECTED_APPS_ALLOWED_PACKAGES);
String disallowedPackagesString = Settings.Global.getString(getContentResolver(),
CONNECTED_APPS_DISALLOWED_PACKAGES);
Set<String> allowedPackagesSet = getSetFromString(allowedPackagesString);
Set<String> disallowedPackagesSet = getSetFromString(disallowedPackagesString);
if (enabled) {
allowedPackagesSet.add(crossProfilePackage);
disallowedPackagesSet.remove(crossProfilePackage);
} else {
allowedPackagesSet.remove(crossProfilePackage);
disallowedPackagesSet.add(crossProfilePackage);
}
Settings.Global.putString(getContentResolver(),
CONNECTED_APPS_ALLOWED_PACKAGES,
String.join(",", allowedPackagesSet));
Settings.Global.putString(getContentResolver(),
CONNECTED_APPS_DISALLOWED_PACKAGES,
String.join(",", disallowedPackagesSet));
}
private Set<String> getSetFromString(String packages) {
return Optional.ofNullable(packages)
.map(pkg -> Set.of(pkg.split(",")))
.orElse(Collections.emptySet());
}
}

View File

@@ -14,38 +14,38 @@
* limitations under the License.
*/
package com.android.settings.biometrics.fingerprint2.lib.model
package com.android.settings.biometrics.fingerprint2.data.model
/**
* A view model that describes the various stages of UDFPS Enrollment. This stages typically update
* the enrollment UI in a major way, such as changing the lottie animation or changing the location
* of the where the user should press their fingerprint
*/
sealed class StageViewModel {
sealed class EnrollStageModel {
/** Unknown stage */
data object Unknown : StageViewModel()
data object Unknown : EnrollStageModel()
/** This is the stage that moves the fingerprint icon around during enrollment. */
data object Guided : StageViewModel()
data object Guided : EnrollStageModel()
/** The center stage is the initial stage of enrollment. */
data object Center : StageViewModel()
data object Center : EnrollStageModel()
/**
* Fingerprint stage of enrollment. Typically there is some sort of indication that a user should
* be using their finger tip to enroll.
*/
data object Fingertip : StageViewModel()
data object Fingertip : EnrollStageModel()
/**
* Left edge stage of enrollment. Typically there is an indication that a user should be using the
* left edge of their fingerprint.
*/
data object LeftEdge : StageViewModel()
data object LeftEdge : EnrollStageModel()
/**
* Right edge stage of enrollment. Typically there is an indication that a user should be using
* the right edge of their fingerprint.
*/
data object RightEdge : StageViewModel()
data object RightEdge : EnrollStageModel()
}

View File

@@ -16,11 +16,11 @@
package com.android.settings.biometrics.fingerprint2.domain.interactor
import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
typealias EnrollStageThresholds = Map<Float, StageViewModel>
typealias EnrollStageThresholds = Map<Float, EnrollStageModel>
/** Interactor that provides enroll stages for enrollment. */
interface EnrollStageInteractor {
@@ -33,11 +33,11 @@ class EnrollStageInteractorImpl() : EnrollStageInteractor {
override val enrollStageThresholds: Flow<EnrollStageThresholds> =
flowOf(
mapOf(
0.0f to StageViewModel.Center,
0.25f to StageViewModel.Guided,
0.5f to StageViewModel.Fingertip,
0.75f to StageViewModel.LeftEdge,
0.875f to StageViewModel.RightEdge,
0.0f to EnrollStageModel.Center,
0.25f to EnrollStageModel.Guided,
0.5f to EnrollStageModel.Fingertip,
0.75f to EnrollStageModel.LeftEdge,
0.875f to EnrollStageModel.RightEdge,
)
)
}

View File

@@ -42,6 +42,7 @@ interface OrientationInteractor {
* A flow that contains the rotation info matched against the def [config_reverseDefaultRotation]
*/
val rotationFromDefault: Flow<Int>
/**
* A Helper function that computes rotation if device is in
* [R.bool.config_reverseDefaultConfigRotation]

View File

@@ -0,0 +1,96 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.biometrics.fingerprint2.domain.interactor
import android.graphics.PointF
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
/**
* This interactor provides information about the current offset of the sensor for guided enrollment
* on UDFPS devices.
*/
interface UdfpsEnrollInteractor {
/** Indicates at which step a UDFPS enrollment is in. */
fun onEnrollmentStep(stepsRemaining: Int, totalStep: Int)
/** Indicates if guided enrollment should be enabled or not. */
fun updateGuidedEnrollment(enabled: Boolean)
/**
* A flow indicating how much the sensor image drawable should be offset for guided enrollment. A
* null point indicates that the icon should be in its default position.
*/
val guidedEnrollmentOffset: Flow<PointF>
}
/** Keeps track of which guided enrollment point we should be using */
class UdfpsEnrollInteractorImpl(
pixelsPerMillimeter: Float,
accessibilityInteractor: AccessibilityInteractor,
) : UdfpsEnrollInteractor {
private var isGuidedEnrollment = MutableStateFlow(false)
// Number of pixels per mm
val px = pixelsPerMillimeter
private val guidedEnrollmentPoints: MutableList<PointF> =
mutableListOf(
PointF(2.00f * px, 0.00f * px),
PointF(0.87f * px, -2.70f * px),
PointF(-1.80f * px, -1.31f * px),
PointF(-1.80f * px, 1.31f * px),
PointF(0.88f * px, 2.70f * px),
PointF(3.94f * px, -1.06f * px),
PointF(2.90f * px, -4.14f * px),
PointF(-0.52f * px, -5.95f * px),
PointF(-3.33f * px, -3.33f * px),
PointF(-3.99f * px, -0.35f * px),
PointF(-3.62f * px, 2.54f * px),
PointF(-1.49f * px, 5.57f * px),
PointF(2.29f * px, 4.92f * px),
PointF(3.82f * px, 1.78f * px),
)
override fun onEnrollmentStep(stepsRemaining: Int, totalStep: Int) {
val index = (totalStep - stepsRemaining) % guidedEnrollmentPoints.size
_guidedEnrollment.update { guidedEnrollmentPoints[index] }
}
override fun updateGuidedEnrollment(enabled: Boolean) {
isGuidedEnrollment.update { enabled }
}
private val _guidedEnrollment = MutableStateFlow(PointF(0f, 0f))
override val guidedEnrollmentOffset: Flow<PointF> =
combine(
_guidedEnrollment,
accessibilityInteractor.isAccessibilityEnabled,
isGuidedEnrollment,
) { point, accessibilityEnabled, guidedEnrollmentEnabled ->
if (accessibilityEnabled || !guidedEnrollmentEnabled) {
return@combine PointF(0f, 0f)
} else {
return@combine PointF(point.x * SCALE, point.y * SCALE)
}
}
companion object {
private const val SCALE = 0.5f
}
}

View File

@@ -24,6 +24,7 @@ import android.hardware.fingerprint.FingerprintManager
import android.os.Bundle
import android.os.Vibrator
import android.util.Log
import android.util.TypedValue
import android.view.accessibility.AccessibilityManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
@@ -54,6 +55,8 @@ import com.android.settings.biometrics.fingerprint2.domain.interactor.FoldStateI
import com.android.settings.biometrics.fingerprint2.domain.interactor.FoldStateInteractorImpl
import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractor
import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractorImpl
import com.android.settings.biometrics.fingerprint2.domain.interactor.UdfpsEnrollInteractor
import com.android.settings.biometrics.fingerprint2.domain.interactor.UdfpsEnrollInteractorImpl
import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractor
import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractorImpl
import com.android.settings.biometrics.fingerprint2.lib.model.Default
@@ -89,6 +92,7 @@ import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Fing
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintScrollViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.GatekeeperInfo
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Transition
import com.android.settings.flags.Flags
import com.android.settings.password.ChooseLockGeneric
import com.android.settings.password.ChooseLockSettingsHelper
@@ -116,6 +120,7 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
private lateinit var foldStateInteractor: FoldStateInteractor
private lateinit var orientationInteractor: OrientationInteractor
private lateinit var displayDensityInteractor: DisplayDensityInteractor
private lateinit var udfpsEnrollInteractor: UdfpsEnrollInteractor
private lateinit var fingerprintScrollViewModel: FingerprintScrollViewModel
private lateinit var backgroundViewModel: BackgroundViewModel
private lateinit var fingerprintFlowViewModel: FingerprintFlowViewModel
@@ -256,6 +261,15 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
fingerprintManager,
Settings,
)
val accessibilityInteractor =
AccessibilityInteractorImpl(
getSystemService(AccessibilityManager::class.java)!!,
lifecycleScope,
)
val pixelsPerMillimeter =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1f, context.resources.displayMetrics)
udfpsEnrollInteractor = UdfpsEnrollInteractorImpl(pixelsPerMillimeter, accessibilityInteractor)
val fingerprintManagerInteractor =
FingerprintManagerInteractorImpl(
@@ -273,12 +287,6 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
val hasConfirmedDeviceCredential = gatekeeperInfo is GatekeeperInfo.GatekeeperPasswordInfo
val accessibilityInteractor =
AccessibilityInteractorImpl(
getSystemService(AccessibilityManager::class.java)!!,
lifecycleScope,
)
navigationViewModel =
ViewModelProvider(
this,
@@ -384,6 +392,7 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
orientationInteractor,
backgroundViewModel,
fingerprintSensorRepo,
udfpsEnrollInteractor,
),
)[UdfpsViewModel::class.java]
@@ -435,17 +444,17 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
else -> FingerprintEnrollEnrollingV2Fragment()
}
}
Introduction -> FingerprintEnrollIntroV2Fragment()
is Introduction -> FingerprintEnrollIntroV2Fragment()
else -> null
}
if (theClass != null) {
supportFragmentManager.fragments.onEach { fragment ->
supportFragmentManager.beginTransaction().remove(fragment).commit()
}
supportFragmentManager
.beginTransaction()
.setCustomAnimations(
step.enterTransition.toAnimation(),
step.exitTransition.toAnimation(),
)
.setReorderingAllowed(true)
.add(R.id.fragment_container_view, theClass::class.java, null)
.commit()
@@ -512,3 +521,12 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
}
}
}
private fun Transition.toAnimation(): Int {
return when (this) {
Transition.EnterFromLeft -> com.google.android.setupdesign.R.anim.sud_slide_back_in
Transition.EnterFromRight -> com.google.android.setupdesign.R.anim.sud_slide_next_in
Transition.ExitToLeft -> com.google.android.setupdesign.R.anim.sud_slide_next_out
Transition.ExitToRight -> com.google.android.setupdesign.R.anim.sud_slide_back_out
}
}

View File

@@ -32,12 +32,12 @@ import androidx.lifecycle.repeatOnLifecycle
import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieCompositionFactory
import com.android.settings.R
import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel
import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.common.widget.FingerprintErrorDialog
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.DescriptionText
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.DescriptionText
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.HeaderText
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.EducationAnimationModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.HeaderText
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.UdfpsViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.widget.UdfpsEnrollViewV2
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep
@@ -83,6 +83,8 @@ class UdfpsEnrollFragment() : Fragment(R.layout.fingerprint_v2_udfps_enroll_enro
window.statusBarColor = color
view.setBackgroundColor(color)
udfpsEnrollView.setFinishAnimationCompleted { viewModel.finishedSuccessfully() }
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
launch {
@@ -159,7 +161,14 @@ class UdfpsEnrollFragment() : Fragment(R.layout.fingerprint_v2_udfps_enroll_enro
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.enrollStage.collect { udfpsEnrollView.updateStage(it) }
viewModel.guidedEnrollment.collect {
glifLayout.post { udfpsEnrollView.updateGuidedEnrollment(it) }
}
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.guidedEnrollmentSaved.collect {
glifLayout.post { udfpsEnrollView.onGuidedPointSaved(it) }
}
}
}
}
@@ -175,35 +184,35 @@ class UdfpsEnrollFragment() : Fragment(R.layout.fingerprint_v2_udfps_enroll_enro
}
private fun HeaderText.toResource(): Int {
return when (this.stageViewModel) {
StageViewModel.Center,
StageViewModel.Guided,
StageViewModel.Fingertip,
StageViewModel.Unknown -> R.string.security_settings_udfps_enroll_fingertip_title
StageViewModel.LeftEdge -> R.string.security_settings_udfps_enroll_left_edge_title
StageViewModel.RightEdge -> R.string.security_settings_udfps_enroll_right_edge_title
return when (this.enrollStageModel) {
EnrollStageModel.Center,
EnrollStageModel.Guided,
EnrollStageModel.Fingertip,
EnrollStageModel.Unknown -> R.string.security_settings_udfps_enroll_fingertip_title
EnrollStageModel.LeftEdge -> R.string.security_settings_udfps_enroll_left_edge_title
EnrollStageModel.RightEdge -> R.string.security_settings_udfps_enroll_right_edge_title
}
}
private fun DescriptionText.toResource(): Int? {
return when (this.stageViewModel) {
StageViewModel.Center,
StageViewModel.Guided,
StageViewModel.Fingertip,
StageViewModel.LeftEdge,
StageViewModel.RightEdge -> null
StageViewModel.Unknown -> R.string.security_settings_udfps_enroll_start_message
return when (this.enrollStageModel) {
EnrollStageModel.Center,
EnrollStageModel.Guided,
EnrollStageModel.Fingertip,
EnrollStageModel.LeftEdge,
EnrollStageModel.RightEdge -> null
EnrollStageModel.Unknown -> R.string.security_settings_udfps_enroll_start_message
}
}
private fun EducationAnimationModel.toResource(): Int? {
return when (this.stageViewModel) {
StageViewModel.Center,
StageViewModel.Guided -> R.raw.udfps_center_hint_lottie
StageViewModel.Fingertip -> R.raw.udfps_tip_hint_lottie
StageViewModel.LeftEdge -> R.raw.udfps_left_edge_hint_lottie
StageViewModel.RightEdge -> R.raw.udfps_right_edge_hint_lottie
StageViewModel.Unknown -> null
return when (this.enrollStageModel) {
EnrollStageModel.Center,
EnrollStageModel.Guided -> R.raw.udfps_center_hint_lottie
EnrollStageModel.Fingertip -> R.raw.udfps_tip_hint_lottie
EnrollStageModel.LeftEdge -> R.raw.udfps_left_edge_hint_lottie
EnrollStageModel.RightEdge -> R.raw.udfps_right_edge_hint_lottie
EnrollStageModel.Unknown -> null
}
}

View File

@@ -14,13 +14,13 @@
* limitations under the License.
*/
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model
import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel
/** Represents the description text for UDFPS enrollment */
data class DescriptionText(
val isSuw: Boolean,
val isAccessibility: Boolean,
val stageViewModel: StageViewModel,
val enrollStageModel: EnrollStageModel,
)

View File

@@ -14,13 +14,13 @@
* limitations under the License.
*/
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model
import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel
/** Represents the header text for UDFPS enrollment */
data class HeaderText(
val isSuw: Boolean,
val isAccessibility: Boolean,
val stageViewModel: StageViewModel,
val enrollStageModel: EnrollStageModel,
)

View File

@@ -16,11 +16,11 @@
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel
import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel
/** Represents the lottie for UDFPS enrollment */
data class EducationAnimationModel(
val isSuw: Boolean,
val isAccessibility: Boolean,
val stageViewModel: StageViewModel,
val enrollStageModel: EnrollStageModel,
)

View File

@@ -17,20 +17,24 @@
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel
import android.graphics.Point
import android.graphics.PointF
import android.view.Surface
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepository
import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel
import com.android.settings.biometrics.fingerprint2.data.repository.SimulatedTouchEventsRepository
import com.android.settings.biometrics.fingerprint2.domain.interactor.DebuggingInteractor
import com.android.settings.biometrics.fingerprint2.domain.interactor.DisplayDensityInteractor
import com.android.settings.biometrics.fingerprint2.domain.interactor.EnrollStageInteractor
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintVibrationEffects
import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractor
import com.android.settings.biometrics.fingerprint2.domain.interactor.UdfpsEnrollInteractor
import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractor
import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.DescriptionText
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.HeaderText
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.BackgroundViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintAction
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollEnrollingViewModel
@@ -61,9 +65,11 @@ class UdfpsViewModel(
orientationInteractor: OrientationInteractor,
backgroundViewModel: BackgroundViewModel,
sensorRepository: FingerprintSensorRepository,
udfpsEnrollInteractor: UdfpsEnrollInteractor,
) : ViewModel() {
private val isSetupWizard = flowOf(false)
private var shouldResetErollment = false
private var _enrollState: Flow<FingerEnrollState?> =
fingerprintEnrollEnrollingViewModel.enrollFlow
@@ -112,6 +118,17 @@ class UdfpsViewModel(
}
}
/**
* This indicates at which point the UI should offset the fingerprint sensor icon for guided
* enrollment.
*/
val guidedEnrollment: Flow<PointF> =
udfpsEnrollInteractor.guidedEnrollmentOffset.distinctUntilChanged()
/** The saved version of [guidedEnrollment] */
val guidedEnrollmentSaved: Flow<PointF> =
guidedEnrollment.shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
/**
* This is the saved progress, this is for when views are recreated and need saved state for the
* first time.
@@ -132,13 +149,13 @@ class UdfpsViewModel(
}
}
/** Determines the current [StageViewModel] enrollment is in */
val enrollStage: Flow<StageViewModel> =
/** Determines the current [EnrollStageModel] enrollment is in */
private val enrollStage: Flow<EnrollStageModel> =
combine(enrollStageInteractor.enrollStageThresholds, enrollState) { thresholds, event ->
if (event is FingerEnrollState.EnrollProgress) {
val progress =
(event.totalStepsRequired - event.remainingSteps).toFloat() / event.totalStepsRequired
var stageToReturn: StageViewModel = StageViewModel.Center
var stageToReturn: EnrollStageModel = EnrollStageModel.Center
thresholds.forEach { (threshold, stage) ->
if (progress < threshold) {
return@forEach
@@ -153,6 +170,40 @@ class UdfpsViewModel(
.filterNotNull()
.shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
init {
viewModelScope.launch {
enrollState
.combine(accessibilityEnabled) { event, isEnabled -> Pair(event, isEnabled) }
.collect {
if (
when (it.first) {
is FingerEnrollState.EnrollError -> true
is FingerEnrollState.EnrollHelp -> it.second
is FingerEnrollState.EnrollProgress -> true
else -> false
}
) {
vibrate(it.first)
}
}
}
viewModelScope.launch {
enrollStage.collect {
udfpsEnrollInteractor.updateGuidedEnrollment(it is EnrollStageModel.Guided)
}
}
viewModelScope.launch {
enrollState.filterIsInstance<FingerEnrollState.EnrollProgress>().collect {
udfpsEnrollInteractor.onEnrollmentStep(it.remainingSteps, it.totalStepsRequired)
}
}
viewModelScope.launch {
backgroundViewModel.background.filter { true }.collect { didGoToBackground() }
}
}
/** Indicates if we should show the lottie. */
val shouldShowLottie: Flow<Boolean> =
combine(
@@ -183,7 +234,7 @@ class UdfpsViewModel(
}
.shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
private val shouldClearDescriptionText = enrollStage.map { it is StageViewModel.Unknown }
private val shouldClearDescriptionText = enrollStage.map { it is EnrollStageModel.Unknown }
/** The description text for UDFPS enrollment */
val descriptionText: Flow<DescriptionText?> =
@@ -202,6 +253,10 @@ class UdfpsViewModel(
/** Indicates if the consumer is ready for enrollment */
fun readyForEnrollment() {
if (shouldResetErollment) {
shouldResetErollment = false
_enrollState = fingerprintEnrollEnrollingViewModel.enrollFlow
}
fingerprintEnrollEnrollingViewModel.canEnroll()
}
@@ -237,8 +292,12 @@ class UdfpsViewModel(
}
private fun doReset() {
/** Indicates if the icon should be animating or not */
_enrollState = fingerprintEnrollEnrollingViewModel.enrollFlow
progressSaved =
enrollState
.filterIsInstance<FingerEnrollState.EnrollProgress>()
.filterNotNull()
.shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
}
/** The lottie that should be shown for UDFPS Enrollment */
@@ -272,6 +331,7 @@ class UdfpsViewModel(
private val orientationInteractor: OrientationInteractor,
private val backgroundViewModel: BackgroundViewModel,
private val sensorRepository: FingerprintSensorRepository,
private val udfpsEnrollInteractor: UdfpsEnrollInteractor,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@@ -287,6 +347,7 @@ class UdfpsViewModel(
orientationInteractor,
backgroundViewModel,
sensorRepository,
udfpsEnrollInteractor,
)
as T
}

View File

@@ -1,89 +0,0 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.widget
import android.content.Context
import android.graphics.PointF
import android.util.TypedValue
import android.view.accessibility.AccessibilityManager
import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
/** Keeps track of which guided enrollment point we should be using */
class UdfpsEnrollHelperV2(private val mContext: Context) {
private var isGuidedEnrollment: Boolean = false
private val accessibilityEnabled: Boolean
private val guidedEnrollmentPoints: MutableList<PointF>
/** The current index of [guidedEnrollmentPoints] for the guided enrollment. */
private var index = 0
init {
val am = mContext.getSystemService(AccessibilityManager::class.java)
accessibilityEnabled = am!!.isEnabled
guidedEnrollmentPoints = ArrayList()
// Number of pixels per mm
val px =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1f, mContext.resources.displayMetrics)
guidedEnrollmentPoints.add(PointF(2.00f * px, 0.00f * px))
guidedEnrollmentPoints.add(PointF(0.87f * px, -2.70f * px))
guidedEnrollmentPoints.add(PointF(-1.80f * px, -1.31f * px))
guidedEnrollmentPoints.add(PointF(-1.80f * px, 1.31f * px))
guidedEnrollmentPoints.add(PointF(0.88f * px, 2.70f * px))
guidedEnrollmentPoints.add(PointF(3.94f * px, -1.06f * px))
guidedEnrollmentPoints.add(PointF(2.90f * px, -4.14f * px))
guidedEnrollmentPoints.add(PointF(-0.52f * px, -5.95f * px))
guidedEnrollmentPoints.add(PointF(-3.33f * px, -3.33f * px))
guidedEnrollmentPoints.add(PointF(-3.99f * px, -0.35f * px))
guidedEnrollmentPoints.add(PointF(-3.62f * px, 2.54f * px))
guidedEnrollmentPoints.add(PointF(-1.49f * px, 5.57f * px))
guidedEnrollmentPoints.add(PointF(2.29f * px, 4.92f * px))
guidedEnrollmentPoints.add(PointF(3.82f * px, 1.78f * px))
}
/**
* This indicates whether we should be offsetting the enrollment icon based on
* [guidedEnrollmentPoints]
*/
fun onUpdateStage(stage: StageViewModel) {
this.isGuidedEnrollment = stage is StageViewModel.Guided
}
/** Updates [index] to be used by [guidedEnrollmentPoints] */
fun onEnrollmentProgress(remaining: Int, totalSteps: Int) {
index = totalSteps - remaining
}
/**
* Returns the current guided enrollment point, or (0,0) if we are not in guided enrollment or are
* in accessibility.
*/
val guidedEnrollmentLocation: PointF?
get() {
if (accessibilityEnabled || !isGuidedEnrollment) {
return null
}
val scale = SCALE
val originalPoint = guidedEnrollmentPoints[index % guidedEnrollmentPoints.size]
return PointF(originalPoint.x * scale, originalPoint.y * scale)
}
companion object {
private const val TAG = "UdfpsEnrollHelperV2"
private const val SCALE = 0.5f
}
}

View File

@@ -24,6 +24,7 @@ import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.PointF
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.Drawable
@@ -37,7 +38,6 @@ import androidx.core.animation.addListener
import androidx.core.graphics.toRect
import androidx.core.graphics.toRectF
import com.android.settings.R
import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
import kotlin.math.sin
/**
@@ -51,11 +51,11 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS
private val fingerprintDrawable: ShapeDrawable
private val sensorOutlinePaint: Paint
private val blueFill: Paint
private val helper = UdfpsEnrollHelperV2(context)
@ColorInt private var enrollIconColor = 0
@ColorInt private var movingTargetFill = 0
private var currentScale = 1.0f
private var alpha = 0
private var guidedEnrollmentOffset: PointF? = null
/**
* This is the physical location of the sensor. This rect will be updated by [drawSensorRectAt]
@@ -143,45 +143,6 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS
invalidateSelf()
}
/** Update the progress of the icon */
fun onEnrollmentProgress(remaining: Int, totalSteps: Int, isRecreating: Boolean = false) {
restoreAnimationTime()
// If we are restoring this view from a saved state, set animation duration to 0 to avoid
// animating progress that has already occurred.
if (isRecreating) {
setAnimationTimeToZero()
} else {
restoreAnimationTime()
}
helper.onEnrollmentProgress(remaining, totalSteps)
val offset = helper.guidedEnrollmentLocation
val currentBounds = getCurrLocation().toRect()
if (offset != null) {
// This is the desired location of the sensor rect, the [EnrollHelper]
// offsets the initial sensor rect by a bit to get the user to move their finger a bit more.
val targetRect = Rect(sensorRectBounds).toRectF()
targetRect.offset(offset.x, offset.y)
val shouldAnimateMovement =
!currentBounds.equals(targetRect) && offset.x != 0f && offset.y != 0f
if (shouldAnimateMovement) {
targetAnimatorSet?.cancel()
animateMovement(currentBounds, targetRect, true)
}
} else {
// If we are not offsetting the sensor, move it back to its original place
animateMovement(currentBounds, sensorRectBounds.toRectF(), false)
}
invalidateSelf()
}
/** Update the stage of the icon */
fun updateStage(it: StageViewModel) {
helper.onUpdateStage(it)
invalidateSelf()
}
/** Stop drawing the fingerprint icon. */
fun stopDrawing() {
alpha = 0
@@ -211,6 +172,7 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS
if (currentBounds.equals(offsetRect)) {
return
}
val xAnimator = ValueAnimator.ofFloat(currentBounds.left.toFloat(), offsetRect.left)
xAnimator.addUpdateListener {
currX = it.animatedValue as Float
@@ -260,6 +222,40 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS
targetAnimationDuration = TARGET_ANIM_DURATION_LONG
}
/**
* Indicates a change to guided enrollment has occurred. Also indicates if we are recreating the
* view, in which case their is no need to animate the icon to whatever position it was in.
*/
fun updateGuidedEnrollment(point: PointF, isRecreating: Boolean) {
guidedEnrollmentOffset = point
if (isRecreating) {
setAnimationTimeToZero()
} else {
restoreAnimationTime()
}
val currentBounds = getCurrLocation().toRect()
val offset = guidedEnrollmentOffset
if (offset?.x != 0f && offset?.y != 0f) {
val targetRect = Rect(sensorRectBounds).toRectF()
// This is the desired location of the sensor rect, the [EnrollHelper]
// offsets the initial sensor rect by a bit to get the user to move their finger a bit more.
targetRect.offset(offset!!.x, offset!!.y)
val shouldAnimateMovement = !currentBounds.equals(targetRect)
if (shouldAnimateMovement) {
targetAnimatorSet?.cancel()
animateMovement(currentBounds, targetRect, true)
} else {
// If we are not offsetting the sensor, move it back to its original place
animateMovement(currentBounds, sensorRectBounds.toRectF(), false)
}
} else {
// If we are not offsetting the sensor, move it back to its original place
animateMovement(currentBounds, sensorRectBounds.toRectF(), false)
}
invalidateSelf()
}
companion object {
private const val TAG = "UdfpsEnrollDrawableV2"
private const val DEFAULT_STROKE_WIDTH = 3f

View File

@@ -27,10 +27,12 @@ import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.util.Log
import android.view.animation.DecelerateInterpolator
import android.view.animation.Interpolator
import android.view.animation.OvershootInterpolator
import androidx.annotation.ColorInt
import androidx.core.animation.addListener
import androidx.core.animation.doOnEnd
import androidx.core.graphics.toRectF
import com.android.internal.annotations.VisibleForTesting
@@ -46,6 +48,7 @@ import kotlin.math.sin
class UdfpsEnrollProgressBarDrawableV2(private val context: Context, attrs: AttributeSet?) :
Drawable() {
private val sensorRect: Rect = Rect()
private var onFinishedCompletionAnimation: (() -> Unit)? = null
private var rotation: Int = 0
private val strokeWidthPx: Float
@@ -287,6 +290,12 @@ class UdfpsEnrollProgressBarDrawableV2(private val context: Context, attrs: Attr
checkMarkDrawable.bounds = newBounds
checkMarkDrawable.setVisible(true, false)
}
doOnEnd {
onFinishedCompletionAnimation?.let{
it()
}
}
start()
}
}
@@ -380,6 +389,13 @@ class UdfpsEnrollProgressBarDrawableV2(private val context: Context, attrs: Attr
checkmarkAnimationDuration = CHECKMARK_ANIMATION_DURATION_MS
}
/**
* Indicates that the finish animation has completed, and enrollment can proceed to the next stage
*/
fun setFinishAnimationCompleted(onFinishedAnimation: () -> Unit) {
this.onFinishedCompletionAnimation = onFinishedAnimation
}
companion object {
private const val TAG = "UdfpsProgressBar"
private const val FILL_COLOR_ANIMATION_DURATION_MS = 350L

View File

@@ -18,6 +18,7 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrol
import android.content.Context
import android.graphics.Point
import android.graphics.PointF
import android.graphics.Rect
import android.util.AttributeSet
import android.util.Log
@@ -31,7 +32,6 @@ import android.widget.FrameLayout
import android.widget.ImageView
import com.android.settings.R
import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
import com.android.systemui.biometrics.UdfpsUtils
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
@@ -53,6 +53,13 @@ class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(co
private val udfpsUtils: UdfpsUtils = UdfpsUtils()
private lateinit var touchExplorationAnnouncer: TouchExplorationAnnouncer
private var isRecreating = false
private var onFinishedCompletionAnimation: (() -> Unit)? = null
init {
fingerprintProgressDrawable.setFinishAnimationCompleted {
onFinishedCompletionAnimation?.let { it() }
}
}
/**
* This function computes the center (x,y) location with respect to the parent [FrameLayout] for
@@ -112,11 +119,6 @@ class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(co
touchExplorationAnnouncer = TouchExplorationAnnouncer(context, this, overlayParams, udfpsUtils)
}
/** Updates the current enrollment stage. */
fun updateStage(it: StageViewModel) {
fingerprintIcon.updateStage(it)
}
/** Receive enroll progress event */
fun onUdfpsEvent(event: FingerEnrollState) {
when (event) {
@@ -174,7 +176,6 @@ class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(co
/** Receive enroll progress event */
private fun onEnrollmentProgress(remaining: Int, totalSteps: Int) {
fingerprintIcon.onEnrollmentProgress(remaining, totalSteps)
fingerprintProgressDrawable.onEnrollmentProgress(remaining, totalSteps)
}
@@ -241,10 +242,25 @@ class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(co
/** Indicates we should should restore the views saved state. */
fun onEnrollProgressSaved(it: FingerEnrollState.EnrollProgress) {
fingerprintIcon.onEnrollmentProgress(it.remainingSteps, it.totalStepsRequired, true)
fingerprintProgressDrawable.onEnrollmentProgress(it.remainingSteps, it.totalStepsRequired, true)
}
/** Indicates we are recreating the UI from a saved state. */
fun onGuidedPointSaved(it: PointF) {
fingerprintIcon.updateGuidedEnrollment(it, true)
}
/**
* Indicates that the finish animation has completed, and enrollment can proceed to the next stage
*/
fun setFinishAnimationCompleted(onFinishedAnimation: () -> Unit) {
this.onFinishedCompletionAnimation = onFinishedAnimation
}
fun updateGuidedEnrollment(point: PointF) {
fingerprintIcon.updateGuidedEnrollment(point, false)
}
companion object {
private const val TAG = "UdfpsEnrollView"
}

View File

@@ -88,7 +88,10 @@ sealed interface FingerprintNavigationStep {
}
/** UiSteps should have a 1 to 1 mapping between each screen of FingerprintEnrollment */
sealed class UiStep : FingerprintNavigationStep
sealed class UiStep(
val enterTransition: Transition = Transition.EnterFromRight,
val exitTransition: Transition = Transition.ExitToLeft,
) : FingerprintNavigationStep
/** This is the landing page for enrollment, where no content is shown. */
data object Init : UiStep() {
@@ -103,7 +106,7 @@ sealed interface FingerprintNavigationStep {
} else if (state.flowType is FastEnroll) {
TransitionStep(Enrollment(state.fingerprintSensor!!))
} else {
TransitionStep(Introduction)
TransitionStep(Introduction())
}
}
else -> null
@@ -118,7 +121,7 @@ sealed interface FingerprintNavigationStep {
action: FingerprintAction,
): FingerprintNavigationStep? {
return when (action) {
FingerprintAction.CONFIRM_DEVICE_SUCCESS -> TransitionStep(Introduction)
FingerprintAction.CONFIRM_DEVICE_SUCCESS -> TransitionStep(Introduction())
FingerprintAction.CONFIRM_DEVICE_FAIL -> Finish(null)
else -> null
}
@@ -126,7 +129,10 @@ sealed interface FingerprintNavigationStep {
}
/** Indicates the FingerprintIntroduction screen is being presented to the user */
data object Introduction : UiStep() {
class Introduction(
enterTransition: Transition = Transition.EnterFromRight,
exitTransition: Transition = Transition.ExitToLeft,
) : UiStep(enterTransition, exitTransition) {
override fun update(
state: NavigationState,
action: FingerprintAction,
@@ -141,7 +147,11 @@ sealed interface FingerprintNavigationStep {
}
/** Indicates the FingerprintEducation screen is being presented to the user */
data class Education(val sensor: FingerprintSensor) : UiStep() {
class Education(
val sensor: FingerprintSensor,
enterTransition: Transition = Transition.EnterFromRight,
exitTransition: Transition = Transition.ExitToLeft,
) : UiStep(enterTransition, exitTransition) {
override fun update(
state: NavigationState,
action: FingerprintAction,
@@ -149,7 +159,8 @@ sealed interface FingerprintNavigationStep {
return when (action) {
FingerprintAction.NEXT -> TransitionStep(Enrollment(state.fingerprintSensor!!))
FingerprintAction.NEGATIVE_BUTTON_PRESSED,
FingerprintAction.PREV -> TransitionStep(Introduction)
FingerprintAction.PREV ->
TransitionStep(Introduction(Transition.EnterFromLeft, Transition.ExitToRight))
else -> null
}
}
@@ -179,7 +190,10 @@ sealed interface FingerprintNavigationStep {
): FingerprintNavigationStep? {
return when (action) {
FingerprintAction.NEXT -> Finish(null)
FingerprintAction.PREV -> TransitionStep(Education(state.fingerprintSensor!!))
FingerprintAction.PREV ->
TransitionStep(
Education(state.fingerprintSensor!!, Transition.EnterFromLeft, Transition.ExitToRight)
)
FingerprintAction.ADD_ANOTHER -> TransitionStep(Enrollment(state.fingerprintSensor!!))
else -> null
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel
/** Indicates the type of transitions that can occur between fragments */
sealed class Transition {
/**
* Indicates the new fragment should slide in from the left side
*/
data object EnterFromLeft : Transition()
/**
* Indicates the new fragment should slide in from the right side
*/
data object EnterFromRight : Transition()
/**
* Indicates the old fragment should slide out to the left side
*/
data object ExitToLeft : Transition()
/**
* Indicates the old fragment should slide out to the right side
*/
data object ExitToRight : Transition()
}

View File

@@ -54,9 +54,6 @@ public class AudioStreamConfirmDialog extends InstrumentedDialogFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!AudioSharingUtils.isFeatureEnabled()) {
return;
}
setShowsDialog(true);
mActivity = getActivity();
if (mActivity == null) {
@@ -84,6 +81,9 @@ public class AudioStreamConfirmDialog extends InstrumentedDialogFragment {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
if (!AudioSharingUtils.isFeatureEnabled()) {
return getUnsupporteDialog();
}
if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
CachedBluetoothDevice connectedLeDevice =
AudioStreamsHelper.getCachedBluetoothDeviceInSharingOrLeConnected(
@@ -137,6 +137,21 @@ public class AudioStreamConfirmDialog extends InstrumentedDialogFragment {
.build();
}
private Dialog getUnsupporteDialog() {
return new AudioStreamsDialogFragment.DialogBuilder(getActivity())
.setTitle(getString(R.string.audio_streams_dialog_cannot_listen))
.setSubTitle2(getString(R.string.audio_streams_dialog_unsupported_device_subtitle))
.setRightButtonText(getString(R.string.audio_streams_dialog_close))
.setRightButtonOnClickListener(
unused -> {
dismiss();
if (mActivity != null) {
mActivity.finish();
}
})
.build();
}
private Dialog getErrorDialog(String name) {
return new AudioStreamsDialogFragment.DialogBuilder(getActivity())
.setTitle(getString(R.string.audio_streams_dialog_cannot_listen))

View File

@@ -19,16 +19,12 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import android.os.Bundle;
import com.android.settings.SettingsActivity;
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
public class AudioStreamConfirmDialogActivity extends SettingsActivity {
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
if (!AudioSharingUtils.isFeatureEnabled()) {
finish();
}
}
@Override

View File

@@ -44,6 +44,8 @@ public class PowerGaugePreference extends AppPreference {
private static final float UNSELECTABLE_ALPHA_LIGHT_MODE = 0.65f;
private static final float UNSELECTABLE_ALPHA_DARK_MODE = 0.65f;
private final int mTitleColorNormal;
private BatteryEntry mInfo;
private BatteryDiffEntry mBatteryDiffEntry;
private CharSequence mContentDescription;
@@ -78,6 +80,8 @@ public class PowerGaugePreference extends AppPreference {
mInfo = info;
mContentDescription = contentDescription;
mShowAnomalyIcon = false;
mTitleColorNormal =
Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary);
}
/** Sets the content description. */
@@ -155,6 +159,13 @@ public class PowerGaugePreference extends AppPreference {
final TextView titleView = (TextView) view.findViewById(android.R.id.title);
titleView.setContentDescription(mContentDescription);
}
if (!isSelectable()) {
// Set colors consistently to meet contrast requirements for non-selectable items
((TextView) view.findViewById(android.R.id.title)).setTextColor(mTitleColorNormal);
((TextView) view.findViewById(android.R.id.summary)).setTextColor(mTitleColorNormal);
subtitle.setTextColor(mTitleColorNormal);
}
}
private static void setViewAlpha(View view, float alpha) {

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import android.app.settings.SettingsEnums;
import android.content.Context;
import com.android.settings.R;
import com.android.settingslib.core.AbstractPreferenceController;
import java.util.ArrayList;
import java.util.List;
/**
* DND Calls Settings page to determine which priority senders can bypass DND when this mode is
* activated.
*/
public class ZenModeCallsFragment extends ZenModeFragmentBase {
@Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
List<AbstractPreferenceController> controllers = new ArrayList<>();
controllers.add(new ZenModePrioritySendersPreferenceController(context,
"zen_mode_settings_category_calls", false, mBackend));
controllers.add(new ZenModeRepeatCallersPreferenceController(context,
"zen_mode_repeat_callers", mBackend,
context.getResources().getInteger(com.android.internal.R.integer
.config_zen_repeat_callers_threshold)));
return controllers;
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.modes_calls_settings;
}
@Override
public int getMetricsCategory() {
// TODO: b/332937635 - make this the correct metrics category
return SettingsEnums.DND_CALLS;
}
@Override
public void onResume() {
super.onResume();
use(ZenModePrioritySendersPreferenceController.class).onResume();
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
import android.content.Context;
import android.os.Bundle;
import androidx.preference.Preference;
import com.android.settings.core.SubSettingLauncher;
public class ZenModeCallsLinkPreferenceController extends AbstractZenModePreferenceController {
private ZenModeSummaryHelper mSummaryHelper;
public ZenModeCallsLinkPreferenceController(Context context, String key,
ZenModesBackend backend) {
super(context, key, backend);
mSummaryHelper = new ZenModeSummaryHelper(context, backend);
}
@Override
public void updateState(Preference preference) {
super.updateState(preference);
Bundle bundle = new Bundle();
bundle.putString(MODE_ID, getMode().getId());
// TODO(b/332937635): Update metrics category
preference.setIntent(new SubSettingLauncher(mContext)
.setDestination(ZenModeCallsFragment.class.getName())
.setSourceMetricsCategory(0)
.setArguments(bundle)
.toIntent());
preference.setSummary(mSummaryHelper.getCallsSettingSummary(getMode()));
}
}

View File

@@ -20,9 +20,6 @@ import android.app.AutomaticZenRule;
import android.app.settings.SettingsEnums;
import android.content.Context;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settingslib.core.AbstractPreferenceController;
@@ -42,6 +39,10 @@ public class ZenModeFragment extends ZenModeFragmentBase {
// {@link AbstractZenModePreferenceController}.
List<AbstractPreferenceController> prefControllers = new ArrayList<>();
prefControllers.add(new ZenModeHeaderController(context, "header", this, mBackend));
prefControllers.add(new ZenModePeopleLinkPreferenceController(
context, "zen_mode_people", mBackend));
prefControllers.add(new ZenModeOtherLinkPreferenceController(
context, "zen_other_settings", mBackend));
return prefControllers;
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import android.app.settings.SettingsEnums;
import android.content.Context;
import com.android.settings.R;
import com.android.settingslib.core.AbstractPreferenceController;
import java.util.ArrayList;
import java.util.List;
/**
* DND Messages Settings page to determine which priority senders can bypass DND.
* "Messages" include SMS, MMS, and messaging apps.
*/
public class ZenModeMessagesFragment extends ZenModeFragmentBase {
@Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
List<AbstractPreferenceController> controllers = new ArrayList<>();
controllers.add(new ZenModePrioritySendersPreferenceController(context,
"zen_mode_settings_category_messages", true, mBackend));
return controllers;
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.modes_messages_settings;
}
@Override
public int getMetricsCategory() {
// TODO: b/332937635 - make this the correct metrics category
return SettingsEnums.DND_MESSAGES;
}
@Override
public void onResume() {
super.onResume();
use(ZenModePrioritySendersPreferenceController.class).onResume();
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
import android.content.Context;
import android.os.Bundle;
import androidx.preference.Preference;
import com.android.settings.core.SubSettingLauncher;
public class ZenModeMessagesLinkPreferenceController extends AbstractZenModePreferenceController {
private final ZenModeSummaryHelper mSummaryHelper;
public ZenModeMessagesLinkPreferenceController(Context context, String key,
ZenModesBackend backend) {
super(context, key, backend);
mSummaryHelper = new ZenModeSummaryHelper(context, backend);
}
@Override
public void updateState(Preference preference) {
super.updateState(preference);
Bundle bundle = new Bundle();
bundle.putString(MODE_ID, getMode().getId());
// TODO(b/332937635): Update metrics category
preference.setIntent(new SubSettingLauncher(mContext)
.setDestination(ZenModeMessagesFragment.class.getName())
.setSourceMetricsCategory(0)
.setArguments(bundle)
.toIntent());
preference.setEnabled(true);
preference.setSummary(mSummaryHelper.getMessagesSettingSummary(getMode().getPolicy()));
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_ALARMS;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_EVENTS;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_MEDIA;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REMINDERS;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_SYSTEM;
import android.app.settings.SettingsEnums;
import android.content.Context;
import com.android.settings.R;
import com.android.settingslib.core.AbstractPreferenceController;
import java.util.ArrayList;
import java.util.List;
/**
* Mode > Alarms & Other Interruptions
*/
public class ZenModeOtherFragment extends ZenModeFragmentBase {
@Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
List<AbstractPreferenceController> controllers = new ArrayList<>();
controllers.add(new ZenModeOtherPreferenceController(
context, "modes_category_alarm", mBackend));
controllers.add(new ZenModeOtherPreferenceController(
context, "modes_category_media", mBackend));
controllers.add(new ZenModeOtherPreferenceController(
context, "modes_category_system", mBackend));
controllers.add(new ZenModeOtherPreferenceController(
context, "modes_category_reminders", mBackend));
controllers.add(new ZenModeOtherPreferenceController(
context, "modes_category_events", mBackend));
return controllers;
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.modes_other_settings;
}
@Override
public int getMetricsCategory() {
// TODO: b/332937635 - make this the correct metrics category
return SettingsEnums.NOTIFICATION_ZEN_MODE_PRIORITY;
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
import android.content.Context;
import android.os.Bundle;
import androidx.preference.Preference;
import com.android.settings.core.SubSettingLauncher;
/**
* Preference with a link and summary about what other sounds can break through the mode
*/
public class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceController {
ZenModeSummaryHelper mSummaryHelper;
public ZenModeOtherLinkPreferenceController(Context context, String key,
ZenModesBackend backend) {
super(context, key, backend);
mSummaryHelper = new ZenModeSummaryHelper(mContext, mBackend);
}
@Override
public void updateState(Preference preference) {
super.updateState(preference);
Bundle bundle = new Bundle();
bundle.putString(MODE_ID, getMode().getId());
preference.setIntent(new SubSettingLauncher(mContext)
.setDestination(ZenModeOtherFragment.class.getName())
.setSourceMetricsCategory(0)
.setArguments(bundle)
.toIntent());
preference.setSummary(mSummaryHelper.getOtherSoundCategoriesSummary(getMode()));
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_ALARMS;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_EVENTS;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_MEDIA;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REMINDERS;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_SYSTEM;
import android.content.Context;
import android.service.notification.ZenPolicy;
import androidx.preference.Preference;
import androidx.preference.TwoStatePreference;
public class ZenModeOtherPreferenceController extends AbstractZenModePreferenceController
implements Preference.OnPreferenceChangeListener {
public ZenModeOtherPreferenceController(Context context, String key,
ZenModesBackend backend) {
super(context, key, backend);
}
@Override
public void updateState(Preference preference) {
super.updateState(preference);
TwoStatePreference pref = (TwoStatePreference) preference;
pref.setChecked(getMode().getPolicy().isCategoryAllowed(getCategory(), true));
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final boolean allow = (Boolean) newValue;
ZenPolicy diffPolicy = new ZenPolicy.Builder()
.allowCategory(getCategory(), allow)
.build();
getMode().setPolicy(diffPolicy);
mBackend.updateMode(getMode());
return true;
}
private int getCategory() {
switch (getPreferenceKey()) {
case "modes_category_alarm":
return PRIORITY_CATEGORY_ALARMS;
case "modes_category_media":
return PRIORITY_CATEGORY_MEDIA;
case "modes_category_system":
return PRIORITY_CATEGORY_SYSTEM;
case "modes_category_reminders":
return PRIORITY_CATEGORY_REMINDERS;
case "modes_category_events":
return PRIORITY_CATEGORY_EVENTS;
}
return -1;
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import android.app.settings.SettingsEnums;
import android.content.Context;
import com.android.settings.R;
import com.android.settingslib.core.AbstractPreferenceController;
import java.util.ArrayList;
import java.util.List;
/**
* Settings page that shows what calls and messages will break through the mode and links to the
* configuration pages for both.
*/
public class ZenModePeopleFragment extends ZenModeFragmentBase {
@Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
List<AbstractPreferenceController> prefControllers = new ArrayList<>();
prefControllers.add(new ZenModeCallsLinkPreferenceController(
context, "zen_mode_people_calls", mBackend));
prefControllers.add(new ZenModeMessagesLinkPreferenceController(
context, "zen_mode_people_messages", mBackend));
return prefControllers;
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.modes_people_settings;
}
@Override
public int getMetricsCategory() {
// TODO: b/332937635 - make this the correct metrics category
return SettingsEnums.DND_PEOPLE;
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
import android.content.Context;
import android.os.Bundle;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.core.SubSettingLauncher;
/**
* Preference with a link and summary about what calls and messages can break through the mode
*/
public class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceController {
ZenModeSummaryHelper mSummaryHelper;
public ZenModePeopleLinkPreferenceController(Context context, String key,
ZenModesBackend backend) {
super(context, key, backend);
mSummaryHelper = new ZenModeSummaryHelper(mContext, mBackend);
}
@Override
public void updateState(Preference preference) {
super.updateState(preference);
Bundle bundle = new Bundle();
bundle.putString(MODE_ID, getMode().getId());
// TODO(b/332937635): Update metrics category
preference.setIntent(new SubSettingLauncher(mContext)
.setDestination(ZenModePeopleFragment.class.getName())
.setSourceMetricsCategory(0)
.setArguments(bundle)
.toIntent());
preference.setSummary(mSummaryHelper.getPeopleSummary(getMode()));
}
}

View File

@@ -0,0 +1,447 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE;
import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE;
import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_UNSET;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_UNSET;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.icu.text.MessageFormat;
import android.provider.Contacts;
import android.service.notification.ConversationChannelWrapper;
import android.service.notification.ZenPolicy;
import android.view.View;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.notification.app.ConversationListSettings;
import com.android.settingslib.widget.SelectorWithWidgetPreference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* Common preference controller functionality for zen mode priority senders preferences for both
* messages and calls.
*
* These controllers handle the settings regarding which priority senders that are allowed to
* bypass DND for calls or messages, which may be one of the following values: starred contacts, all
* contacts, priority conversations (for messages only), anyone, or no one.
*/
public class ZenModePrioritySendersPreferenceController
extends AbstractZenModePreferenceController {
private final boolean mIsMessages; // if this is false, then this preference is for calls
static final String KEY_ANY = "senders_anyone";
static final String KEY_CONTACTS = "senders_contacts";
static final String KEY_STARRED = "senders_starred_contacts";
static final String KEY_IMPORTANT = "conversations_important";
static final String KEY_NONE = "senders_none";
private int mNumImportantConversations = CONVERSATION_SENDERS_UNSET;
private static final Intent ALL_CONTACTS_INTENT =
new Intent(Contacts.Intents.UI.LIST_DEFAULT)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
private static final Intent STARRED_CONTACTS_INTENT =
new Intent(Contacts.Intents.UI.LIST_STARRED_ACTION)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
private static final Intent FALLBACK_INTENT = new Intent(Intent.ACTION_MAIN)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
private final PackageManager mPackageManager;
private PreferenceCategory mPreferenceCategory;
private List<SelectorWithWidgetPreference> mSelectorPreferences = new ArrayList<>();
private final ZenModeSummaryHelper mZenModeSummaryHelper;
public ZenModePrioritySendersPreferenceController(Context context, String key,
boolean isMessages, ZenModesBackend backend) {
super(context, key, backend);
mIsMessages = isMessages;
String contactsPackage = context.getString(R.string.config_contacts_package_name);
ALL_CONTACTS_INTENT.setPackage(contactsPackage);
STARRED_CONTACTS_INTENT.setPackage(contactsPackage);
FALLBACK_INTENT.setPackage(contactsPackage);
mPackageManager = mContext.getPackageManager();
if (!FALLBACK_INTENT.hasCategory(Intent.CATEGORY_APP_CONTACTS)) {
FALLBACK_INTENT.addCategory(Intent.CATEGORY_APP_CONTACTS);
}
mZenModeSummaryHelper = new ZenModeSummaryHelper(mContext, mBackend);
}
@Override
public void displayPreference(PreferenceScreen screen) {
mPreferenceCategory = screen.findPreference(getPreferenceKey());
if (mPreferenceCategory.getPreferenceCount() == 0) {
makeSelectorPreference(KEY_STARRED,
com.android.settings.R.string.zen_mode_from_starred, mIsMessages);
makeSelectorPreference(KEY_CONTACTS,
com.android.settings.R.string.zen_mode_from_contacts, mIsMessages);
if (mIsMessages) {
makeSelectorPreference(KEY_IMPORTANT,
com.android.settings.R.string.zen_mode_from_important_conversations, true);
}
makeSelectorPreference(KEY_ANY,
com.android.settings.R.string.zen_mode_from_anyone, mIsMessages);
makeSelectorPreference(KEY_NONE,
com.android.settings.R.string.zen_mode_none_messages, mIsMessages);
}
super.displayPreference(screen);
}
@Override
public void updateState(Preference preference) {
if (mIsMessages) {
updateChannelCounts();
}
final int currContactsSetting = getPrioritySenders();
final int currConversationsSetting = getPriorityConversationSenders();
for (SelectorWithWidgetPreference pref : mSelectorPreferences) {
// for each preference, check whether the current state matches what this state
// would look like if the button were checked.
final int[] checkedState = keyToSettingEndState(pref.getKey(), true);
final int checkedContactsSetting = checkedState[0];
final int checkedConversationsSetting = checkedState[1];
boolean match = checkedContactsSetting == currContactsSetting;
if (mIsMessages && checkedConversationsSetting != CONVERSATION_SENDERS_UNSET) {
// "CONVERSATION_SENDERS_UNSET" in checkedContactsSetting means this preference
// doesn't govern the priority senders setting, so the full match happens when
// either the priority senders setting matches or if it's CONVERSATION_SENDERS_UNSET
// so only the conversation setting needs to match.
match = (match || checkedContactsSetting == PEOPLE_TYPE_UNSET)
&& (checkedConversationsSetting == currConversationsSetting);
}
pref.setChecked(match);
}
updateSummaries();
}
public void onResume() {
if (mIsMessages) {
updateChannelCounts();
}
updateSummaries();
}
private void updateChannelCounts() {
ParceledListSlice<ConversationChannelWrapper> impConversations =
mBackend.getConversations(true);
int numImportantConversations = 0;
if (impConversations != null) {
for (ConversationChannelWrapper conversation : impConversations.getList()) {
if (!conversation.getNotificationChannel().isDemoted()) {
numImportantConversations++;
}
}
}
mNumImportantConversations = numImportantConversations;
}
private int getPrioritySenders() {
if (mIsMessages) {
return getMode().getPolicy().getPriorityMessageSenders();
} else {
return getMode().getPolicy().getPriorityCallSenders();
}
}
private int getPriorityConversationSenders() {
if (mIsMessages) {
return getMode().getPolicy().getPriorityConversationSenders();
}
return CONVERSATION_SENDERS_UNSET;
}
private SelectorWithWidgetPreference makeSelectorPreference(String key, int titleId,
boolean isCheckbox) {
final SelectorWithWidgetPreference pref =
new SelectorWithWidgetPreference(mPreferenceCategory.getContext(), isCheckbox);
pref.setKey(key);
pref.setTitle(titleId);
pref.setOnClickListener(mSelectorClickListener);
View.OnClickListener widgetClickListener = getWidgetClickListener(key);
if (widgetClickListener != null) {
pref.setExtraWidgetOnClickListener(widgetClickListener);
}
mPreferenceCategory.addPreference(pref);
mSelectorPreferences.add(pref);
return pref;
}
private View.OnClickListener getWidgetClickListener(String key) {
if (!KEY_CONTACTS.equals(key) && !KEY_STARRED.equals(key) && !KEY_IMPORTANT.equals(key)) {
return null;
}
if (KEY_STARRED.equals(key) && !isStarredIntentValid()) {
return null;
}
if (KEY_CONTACTS.equals(key) && !isContactsIntentValid()) {
return null;
}
return v -> {
if (KEY_STARRED.equals(key)
&& STARRED_CONTACTS_INTENT.resolveActivity(mPackageManager) != null) {
mContext.startActivity(STARRED_CONTACTS_INTENT);
} else if (KEY_CONTACTS.equals(key)
&& ALL_CONTACTS_INTENT.resolveActivity(mPackageManager) != null) {
mContext.startActivity(ALL_CONTACTS_INTENT);
} else if (KEY_IMPORTANT.equals(key)) {
new SubSettingLauncher(mContext)
.setDestination(ConversationListSettings.class.getName())
.setSourceMetricsCategory(SettingsEnums.DND_CONVERSATIONS)
.launch();
} else {
mContext.startActivity(FALLBACK_INTENT);
}
};
}
private boolean isStarredIntentValid() {
return STARRED_CONTACTS_INTENT.resolveActivity(mPackageManager) != null
|| FALLBACK_INTENT.resolveActivity(mPackageManager) != null;
}
private boolean isContactsIntentValid() {
return ALL_CONTACTS_INTENT.resolveActivity(mPackageManager) != null
|| FALLBACK_INTENT.resolveActivity(mPackageManager) != null;
}
void updateSummaries() {
for (SelectorWithWidgetPreference pref : mSelectorPreferences) {
pref.setSummary(getSummary(pref.getKey()));
}
}
// Gets the desired end state of the priority senders and conversations for the given key
// and whether it is being checked or unchecked. [type]_UNSET indicates no change in state.
//
// Returns an integer array with 2 entries. The first entry is the setting for priority senders
// and the second entry is for priority conversation senders; if isMessages is false, then
// no changes will ever be prescribed for conversation senders.
int[] keyToSettingEndState(String key, boolean checked) {
int[] endState = new int[]{ PEOPLE_TYPE_UNSET, CONVERSATION_SENDERS_UNSET };
if (!checked) {
// Unchecking any priority-senders-based state should reset the state to NONE.
// "Unchecking" the NONE state doesn't do anything, in practice.
switch (key) {
case KEY_STARRED:
case KEY_CONTACTS:
case KEY_ANY:
case KEY_NONE:
endState[0] = PEOPLE_TYPE_NONE;
}
// For messages, unchecking "priority conversations" and "any" should reset conversation
// state to "NONE" as well.
if (mIsMessages) {
switch (key) {
case KEY_IMPORTANT:
case KEY_ANY:
case KEY_NONE:
endState[1] = CONVERSATION_SENDERS_NONE;
}
}
} else {
// All below is for the enabling (checked) state.
switch (key) {
case KEY_STARRED:
endState[0] = PEOPLE_TYPE_STARRED;
break;
case KEY_CONTACTS:
endState[0] = PEOPLE_TYPE_CONTACTS;
break;
case KEY_ANY:
endState[0] = PEOPLE_TYPE_ANYONE;
break;
case KEY_NONE:
endState[0] = PEOPLE_TYPE_NONE;
}
// In the messages case *only*, also handle changing of conversation settings.
if (mIsMessages) {
switch (key) {
case KEY_IMPORTANT:
endState[1] = CONVERSATION_SENDERS_IMPORTANT;
break;
case KEY_ANY:
endState[1] = CONVERSATION_SENDERS_ANYONE;
break;
case KEY_NONE:
endState[1] = CONVERSATION_SENDERS_NONE;
}
}
}
// Error case check: if somehow, after all of that, endState is still
// {PEOPLE_TYPE_UNSET, CONVERSATION_SENDERS_UNSET}, something has gone wrong.
if (endState[0] == PEOPLE_TYPE_UNSET && endState[1] == CONVERSATION_SENDERS_UNSET) {
throw new IllegalArgumentException("invalid key " + key);
}
return endState;
}
// Returns the preferences, if any, that should be newly saved for the specified setting and
// checked state in an array where index 0 is the new senders setting and 1 the new
// conversations setting. A return value of [type]_UNSET indicates that nothing should
// change.
//
// The returned conversations setting will always be CONVERSATION_SENDERS_UNSET (not to change)
// in the calls case.
//
// Checking and unchecking is mostly an operation of setting or unsetting the relevant
// preference, except for some special handling where the conversation setting overlaps:
// - setting or unsetting "priority contacts" or "contacts" has no effect on the
// priority conversation setting, and vice versa
// - if "priority conversations" is selected, and the user checks "anyone", the conversation
// setting is also set to any conversations
// - if "anyone" is previously selected, and the user clicks "priority conversations", then
// the contacts setting is additionally reset to "none".
// - if "anyone" is previously selected, and the user clicks one of the contacts values,
// then the conversations setting is additionally reset to "none".
int[] settingsToSaveOnClick(SelectorWithWidgetPreference preference,
int currSendersSetting, int currConvosSetting) {
int[] savedSettings = new int[]{ PEOPLE_TYPE_UNSET, CONVERSATION_SENDERS_UNSET };
// If the preference isn't a checkbox, always consider this to be "checking" the setting.
// Otherwise, toggle.
final int[] endState = keyToSettingEndState(preference.getKey(),
preference.isCheckBox() ? preference.isChecked() : true);
final int prioritySendersSetting = endState[0];
final int priorityConvosSetting = endState[1];
if (prioritySendersSetting != PEOPLE_TYPE_UNSET
&& prioritySendersSetting != currSendersSetting) {
savedSettings[0] = prioritySendersSetting;
}
// Only handle conversation settings for the messages case. If not messages, there should
// never be any change to the conversation senders setting.
if (mIsMessages) {
if (priorityConvosSetting != CONVERSATION_SENDERS_UNSET
&& priorityConvosSetting != currConvosSetting) {
savedSettings[1] = priorityConvosSetting;
}
// Special-case handling for the "priority conversations" checkbox:
// If a specific selection exists for priority senders (starred, contacts), we leave
// it untouched. Otherwise (when the senders is set to "any"), set it to NONE.
if (preference.getKey() == KEY_IMPORTANT
&& currSendersSetting == PEOPLE_TYPE_ANYONE) {
savedSettings[0] = PEOPLE_TYPE_NONE;
}
// Flip-side special case for clicking either "contacts" option: if a specific selection
// exists for priority conversations, leave it untouched; otherwise, set to none.
if ((preference.getKey() == KEY_STARRED || preference.getKey() == KEY_CONTACTS)
&& currConvosSetting == CONVERSATION_SENDERS_ANYONE) {
savedSettings[1] = CONVERSATION_SENDERS_NONE;
}
}
return savedSettings;
}
private String getSummary(String key) {
switch (key) {
case KEY_STARRED:
return mZenModeSummaryHelper.getStarredContactsSummary();
case KEY_CONTACTS:
return mZenModeSummaryHelper.getContactsNumberSummary();
case KEY_IMPORTANT:
return getConversationSummary();
case KEY_ANY:
return mContext.getResources().getString(mIsMessages
? R.string.zen_mode_all_messages_summary
: R.string.zen_mode_all_calls_summary);
case KEY_NONE:
default:
return null;
}
}
private String getConversationSummary() {
final int numConversations = mNumImportantConversations;
if (numConversations == CONVERSATION_SENDERS_UNSET) {
return null;
} else {
MessageFormat msgFormat = new MessageFormat(
mContext.getString(R.string.zen_mode_conversations_count),
Locale.getDefault());
Map<String, Object> args = new HashMap<>();
args.put("count", numConversations);
return msgFormat.format(args);
}
}
@VisibleForTesting
SelectorWithWidgetPreference.OnClickListener mSelectorClickListener =
new SelectorWithWidgetPreference.OnClickListener() {
@Override
public void onRadioButtonClicked(SelectorWithWidgetPreference preference) {
// The settingsToSaveOnClick function takes whether the preference is a
// checkbox into account to determine whether this selection is checked or unchecked.
final int[] settingsToSave = settingsToSaveOnClick(preference,
getPrioritySenders(), getPriorityConversationSenders());
final int prioritySendersSetting = settingsToSave[0];
final int priorityConvosSetting = settingsToSave[1];
ZenPolicy.Builder diffPolicy = new ZenPolicy.Builder();
if (prioritySendersSetting != PEOPLE_TYPE_UNSET) {
if (mIsMessages) {
diffPolicy.allowMessages(prioritySendersSetting);
} else {
diffPolicy.allowCalls(prioritySendersSetting);
}
}
if (mIsMessages && priorityConvosSetting != CONVERSATION_SENDERS_UNSET) {
diffPolicy.allowConversations(priorityConvosSetting);
}
getMode().setPolicy(diffPolicy.build());
mBackend.updateMode(getMode());
}
};
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE;
import static android.service.notification.ZenPolicy.STATE_ALLOW;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.provider.Settings;
import android.service.notification.ZenPolicy;
import android.util.Log;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import androidx.preference.TwoStatePreference;
import com.android.settings.R;
public class ZenModeRepeatCallersPreferenceController extends AbstractZenModePreferenceController
implements Preference.OnPreferenceChangeListener {
private final int mRepeatCallersThreshold;
public ZenModeRepeatCallersPreferenceController(Context context,
String key, ZenModesBackend backend, int repeatCallersThreshold) {
super(context, key, backend);
mRepeatCallersThreshold = repeatCallersThreshold;
}
@Override
public void updateState(Preference preference) {
super.updateState(preference);
TwoStatePreference pref = (TwoStatePreference) preference;
boolean anyCallersCanBypassDnd =
getMode().getPolicy().getPriorityCategoryCalls() == STATE_ALLOW
&& getMode().getPolicy().getPriorityCallSenders() == PEOPLE_TYPE_ANYONE;
// if any caller can bypass dnd then repeat callers preference is disabled
if (anyCallersCanBypassDnd) {
pref.setEnabled(false);
pref.setChecked(true);
} else {
pref.setEnabled(true);
pref.setChecked(
getMode().getPolicy().getPriorityCategoryRepeatCallers() == STATE_ALLOW);
}
setRepeatCallerSummary(preference);
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final boolean allowRepeatCallers = (Boolean) newValue;
ZenPolicy diffPolicy = new ZenPolicy.Builder()
.allowRepeatCallers(allowRepeatCallers)
.build();
getMode().setPolicy(diffPolicy);
mBackend.updateMode(getMode());
return true;
}
private void setRepeatCallerSummary(Preference preference) {
preference.setSummary(mContext.getString(R.string.zen_mode_repeat_callers_summary,
mRepeatCallersThreshold));
}
}

View File

@@ -0,0 +1,295 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE;
import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_ALARMS;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_CALLS;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_CONVERSATIONS;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_EVENTS;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_MEDIA;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_MESSAGES;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REMINDERS;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REPEAT_CALLERS;
import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_SYSTEM;
import android.content.Context;
import android.icu.text.MessageFormat;
import android.service.notification.ZenPolicy;
import com.android.settings.R;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Predicate;
public class ZenModeSummaryHelper {
private Context mContext;
private ZenModesBackend mBackend;
public ZenModeSummaryHelper(Context context, ZenModesBackend backend) {
mContext = context;
mBackend = backend;
}
private static final int[] ALL_PRIORITY_CATEGORIES = {
PRIORITY_CATEGORY_ALARMS,
PRIORITY_CATEGORY_MEDIA,
PRIORITY_CATEGORY_SYSTEM,
PRIORITY_CATEGORY_MESSAGES,
PRIORITY_CATEGORY_CONVERSATIONS,
PRIORITY_CATEGORY_EVENTS,
PRIORITY_CATEGORY_REMINDERS,
PRIORITY_CATEGORY_CALLS,
PRIORITY_CATEGORY_REPEAT_CALLERS,
};
String getOtherSoundCategoriesSummary(ZenMode zenMode) {
List<String> enabledCategories = getEnabledCategories(
zenMode.getPolicy(),
category -> PRIORITY_CATEGORY_ALARMS == category
|| PRIORITY_CATEGORY_MEDIA == category
|| PRIORITY_CATEGORY_SYSTEM == category
|| PRIORITY_CATEGORY_REMINDERS == category
|| PRIORITY_CATEGORY_EVENTS == category,
true);
int numCategories = enabledCategories.size();
MessageFormat msgFormat = new MessageFormat(
mContext.getString(R.string.zen_mode_other_sounds_summary),
Locale.getDefault());
Map<String, Object> args = new HashMap<>();
args.put("count", numCategories);
if (numCategories >= 1) {
args.put("sound_category_1", enabledCategories.get(0));
if (numCategories >= 2) {
args.put("sound_category_2", enabledCategories.get(1));
if (numCategories == 3) {
args.put("sound_category_3", enabledCategories.get(2));
}
}
}
return msgFormat.format(args);
}
String getCallsSettingSummary(ZenMode zenMode) {
List<String> enabledCategories = getEnabledCategories(zenMode.getPolicy(),
category -> PRIORITY_CATEGORY_CALLS == category
|| PRIORITY_CATEGORY_REPEAT_CALLERS == category, true);
int numCategories = enabledCategories.size();
if (numCategories == 0) {
return mContext.getString(R.string.zen_mode_none_calls);
} else if (numCategories == 1) {
return mContext.getString(R.string.zen_mode_calls_summary_one,
enabledCategories.get(0));
} else {
return mContext.getString(R.string.zen_mode_calls_summary_two,
enabledCategories.get(0),
enabledCategories.get(1));
}
}
String getMessagesSettingSummary(ZenPolicy policy) {
List<String> enabledCategories = getEnabledCategories(policy,
category -> PRIORITY_CATEGORY_MESSAGES == category
|| PRIORITY_CATEGORY_CONVERSATIONS == category, true);
int numCategories = enabledCategories.size();
if (numCategories == 0) {
return mContext.getString(R.string.zen_mode_none_messages);
} else if (numCategories == 1) {
return enabledCategories.get(0);
} else {
// While this string name seems like a slight misnomer: it's borrowing the analogous
// calls-summary functionality to combine two permissions.
return mContext.getString(R.string.zen_mode_calls_summary_two,
enabledCategories.get(0),
enabledCategories.get(1));
}
}
String getBlockedEffectsSummary(ZenMode zenMode) {
if (zenMode.getPolicy().shouldShowAllVisualEffects()) {
return mContext.getResources().getString(
R.string.zen_mode_restrict_notifications_summary_muted);
} else if (zenMode.getPolicy().shouldHideAllVisualEffects()) {
return mContext.getResources().getString(
R.string.zen_mode_restrict_notifications_summary_hidden);
} else {
return mContext.getResources().getString(
R.string.zen_mode_restrict_notifications_summary_custom);
}
}
private List<String> getEnabledCategories(ZenPolicy policy,
Predicate<Integer> filteredCategories, boolean capitalizeFirstInList) {
List<String> enabledCategories = new ArrayList<>();
for (int category : ALL_PRIORITY_CATEGORIES) {
boolean isFirst = capitalizeFirstInList && enabledCategories.isEmpty();
if (filteredCategories.test(category) && policy.isCategoryAllowed(category, false)) {
if (category == PRIORITY_CATEGORY_REPEAT_CALLERS
&& policy.isCategoryAllowed(PRIORITY_CATEGORY_CALLS, false)
&& policy.getPriorityCallSenders() == PEOPLE_TYPE_ANYONE) {
continue;
}
// For conversations, only the "priority conversations" setting is relevant; any
// other setting is subsumed by the messages-specific messaging.
if (category == PRIORITY_CATEGORY_CONVERSATIONS
&& policy.isCategoryAllowed(PRIORITY_CATEGORY_CONVERSATIONS, false)
&& policy.getPriorityConversationSenders()
!= CONVERSATION_SENDERS_IMPORTANT) {
continue;
}
enabledCategories.add(getCategory(category, policy, isFirst));
}
}
return enabledCategories;
}
private String getCategory(int category, ZenPolicy policy, boolean isFirst) {
if (category == PRIORITY_CATEGORY_ALARMS) {
if (isFirst) {
return mContext.getString(R.string.zen_mode_alarms_list_first);
} else {
return mContext.getString(R.string.zen_mode_alarms_list);
}
} else if (category == PRIORITY_CATEGORY_MEDIA) {
if (isFirst) {
return mContext.getString(R.string.zen_mode_media_list_first);
} else {
return mContext.getString(R.string.zen_mode_media_list);
}
} else if (category == PRIORITY_CATEGORY_SYSTEM) {
if (isFirst) {
return mContext.getString(R.string.zen_mode_system_list_first);
} else {
return mContext.getString(R.string.zen_mode_system_list);
}
} else if (category == PRIORITY_CATEGORY_MESSAGES) {
if (policy.getPriorityMessageSenders() == PEOPLE_TYPE_ANYONE) {
return mContext.getString(R.string.zen_mode_from_anyone);
} else if (policy.getPriorityMessageSenders() == PEOPLE_TYPE_CONTACTS) {
return mContext.getString(R.string.zen_mode_from_contacts);
} else {
return mContext.getString(R.string.zen_mode_from_starred);
}
} else if (category == PRIORITY_CATEGORY_CONVERSATIONS
&& policy.getPriorityConversationSenders() == CONVERSATION_SENDERS_IMPORTANT) {
if (isFirst) {
return mContext.getString(R.string.zen_mode_from_important_conversations);
} else {
return mContext.getString(
R.string.zen_mode_from_important_conversations_second);
}
} else if (category == PRIORITY_CATEGORY_EVENTS) {
if (isFirst) {
return mContext.getString(R.string.zen_mode_events_list_first);
} else {
return mContext.getString(R.string.zen_mode_events_list);
}
} else if (category == PRIORITY_CATEGORY_REMINDERS) {
if (isFirst) {
return mContext.getString(R.string.zen_mode_reminders_list_first);
} else {
return mContext.getString(R.string.zen_mode_reminders_list);
}
} else if (category == PRIORITY_CATEGORY_CALLS) {
if (policy.getPriorityCallSenders() == PEOPLE_TYPE_ANYONE) {
if (isFirst) {
return mContext.getString(R.string.zen_mode_from_anyone);
}
return mContext.getString(R.string.zen_mode_all_callers);
} else if (policy.getPriorityCallSenders() == PEOPLE_TYPE_CONTACTS) {
if (isFirst) {
return mContext.getString(R.string.zen_mode_from_contacts);
}
return mContext.getString(R.string.zen_mode_contacts_callers);
} else {
if (isFirst) {
return mContext.getString(R.string.zen_mode_from_starred);
}
return mContext.getString(R.string.zen_mode_starred_callers);
}
} else if (category == PRIORITY_CATEGORY_REPEAT_CALLERS) {
if (isFirst) {
return mContext.getString(R.string.zen_mode_repeat_callers);
} else {
return mContext.getString(R.string.zen_mode_repeat_callers_list);
}
}
return "";
}
public String getStarredContactsSummary() {
List<String> starredContacts = mBackend.getStarredContacts();
int numStarredContacts = starredContacts.size();
MessageFormat msgFormat = new MessageFormat(
mContext.getString(R.string.zen_mode_starred_contacts_summary_contacts),
Locale.getDefault());
Map<String, Object> args = new HashMap<>();
args.put("count", numStarredContacts);
if (numStarredContacts >= 1) {
args.put("contact_1", starredContacts.get(0));
if (numStarredContacts >= 2) {
args.put("contact_2", starredContacts.get(1));
if (numStarredContacts == 3) {
args.put("contact_3", starredContacts.get(2));
}
}
}
return msgFormat.format(args);
}
public String getContactsNumberSummary() {
MessageFormat msgFormat = new MessageFormat(
mContext.getString(R.string.zen_mode_contacts_count),
Locale.getDefault());
Map<String, Object> args = new HashMap<>();
args.put("count", mBackend.queryAllContactsData().getCount());
return msgFormat.format(args);
}
public String getPeopleSummary(ZenMode zenMode) {
final int callersAllowed = zenMode.getPolicy().getPriorityCallSenders();
final int messagesAllowed = zenMode.getPolicy().getPriorityMessageSenders();
final int conversationsAllowed = zenMode.getPolicy().getPriorityConversationSenders();
final boolean areRepeatCallersAllowed =
zenMode.getPolicy().isCategoryAllowed(PRIORITY_CATEGORY_REPEAT_CALLERS, false);
if (callersAllowed == PEOPLE_TYPE_ANYONE
&& messagesAllowed == PEOPLE_TYPE_ANYONE
&& conversationsAllowed == CONVERSATION_SENDERS_ANYONE) {
return mContext.getResources().getString(R.string.zen_mode_people_all);
} else if (callersAllowed == PEOPLE_TYPE_NONE
&& messagesAllowed == PEOPLE_TYPE_NONE
&& conversationsAllowed == CONVERSATION_SENDERS_NONE
&& !areRepeatCallersAllowed) {
return mContext.getResources().getString(R.string.zen_mode_people_none);
} else {
return mContext.getResources().getString(R.string.zen_mode_people_some);
}
}
}

View File

@@ -21,14 +21,22 @@ import static java.util.Objects.requireNonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.AutomaticZenRule;
import android.app.INotificationManager;
import android.app.NotificationManager;
import android.content.Context;
import android.content.pm.ParceledListSlice;
import android.database.Cursor;
import android.net.Uri;
import android.os.ServiceManager;
import android.provider.ContactsContract;
import android.provider.Settings;
import android.service.notification.Condition;
import android.service.notification.ConversationChannelWrapper;
import android.service.notification.ZenAdapters;
import android.service.notification.ZenModeConfig;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import java.time.Duration;
@@ -51,6 +59,8 @@ class ZenModesBackend {
private static ZenModesBackend sInstance;
private final NotificationManager mNotificationManager;
static INotificationManager sINM = INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE));
private final Context mContext;
@@ -105,6 +115,54 @@ class ZenModesBackend {
}
}
public ParceledListSlice<ConversationChannelWrapper> getConversations(boolean onlyImportant) {
try {
return sINM.getConversations(onlyImportant);
} catch (Exception e) {
Log.w(TAG, "Error calling NoMan", e);
return ParceledListSlice.emptyList();
}
}
public List<String> getStarredContacts() {
Cursor cursor = null;
try {
cursor = queryStarredContactsData();
return getStarredContacts(cursor);
} finally {
if (cursor != null) {
cursor.close();
}
}
}
@VisibleForTesting
List<String> getStarredContacts(Cursor cursor) {
List<String> starredContacts = new ArrayList<>();
if (cursor != null && cursor.moveToFirst()) {
do {
String contact = cursor.getString(0);
starredContacts.add(contact != null ? contact :
mContext.getString(R.string.zen_mode_starred_contacts_empty_name));
} while (cursor.moveToNext());
}
return starredContacts;
}
private Cursor queryStarredContactsData() {
return mContext.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI,
new String[]{ContactsContract.Contacts.DISPLAY_NAME_PRIMARY},
ContactsContract.Data.STARRED + "=1", null,
ContactsContract.Data.TIMES_CONTACTED);
}
Cursor queryAllContactsData() {
return mContext.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI,
new String[]{ContactsContract.Contacts.DISPLAY_NAME_PRIMARY},
null, null, null);
}
private ZenMode getManualDndMode(ZenModeConfig config) {
// TODO: b/333530553 - Read ZenDeviceEffects of manual DND.
// TODO: b/333682392 - Replace with final strings for name & trigger description

View File

@@ -57,9 +57,9 @@ abstract class ZenModesFragmentBase extends RestrictedDashboardFragment {
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
mContext = context;
mBackend = ZenModesBackend.getInstance(context);
super.onAttach(context);
}
@Override
@@ -77,6 +77,12 @@ abstract class ZenModesFragmentBase extends RestrictedDashboardFragment {
}
}
@Override
public void onResume() {
super.onResume();
updateZenModeState();
}
@Override
public void onStop() {
super.onStop();

View File

@@ -29,7 +29,7 @@ import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
import com.android.settingslib.R
import com.android.settingslib.spa.lifecycle.collectAsCallbackWithLifecycle
import com.android.settingslib.spaprivileged.model.app.AppOps
import com.android.settingslib.spaprivileged.model.app.AppOpsController
import com.android.settingslib.spaprivileged.model.app.AppOpsPermissionController
import com.android.settingslib.spaprivileged.model.app.AppRecord
import com.android.settingslib.spaprivileged.model.app.IPackageManagers
import com.android.settingslib.spaprivileged.model.app.PackageManagers
@@ -49,7 +49,7 @@ data class AlarmsAndRemindersAppRecord(
override val app: ApplicationInfo,
val isTrumped: Boolean,
val isChangeable: Boolean,
var controller: AppOpsController,
val controller: AppOpsPermissionController,
) : AppRecord
class AlarmsAndRemindersAppListModel(
@@ -84,7 +84,7 @@ class AlarmsAndRemindersAppListModel(
@Composable
override fun isAllowed(record: AlarmsAndRemindersAppRecord): () -> Boolean? = when {
record.isTrumped -> ({ true })
else -> record.controller.isAllowed.collectAsCallbackWithLifecycle()
else -> record.controller.isAllowedFlow.collectAsCallbackWithLifecycle()
}
override fun isChangeable(record: AlarmsAndRemindersAppRecord) = record.isChangeable
@@ -114,10 +114,11 @@ class AlarmsAndRemindersAppListModel(
app = app,
isTrumped = isTrumped,
isChangeable = hasRequestPermission && !isTrumped,
controller = AppOpsController(
controller = AppOpsPermissionController(
context = context,
app = app,
appOps = APP_OPS,
permission = PERMISSION,
),
)
}

View File

@@ -90,7 +90,8 @@ public class FactoryResetPreferenceController extends BasePreferenceController {
String packageName = resolution.activityInfo.packageName;
PackageInfo factoryResetWizardPackageInfo;
try {
factoryResetWizardPackageInfo = pm.getPackageInfo(packageName, 0);
factoryResetWizardPackageInfo = pm.getPackageInfo(packageName,
PackageManager.GET_PERMISSIONS);
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Unable to resolve a Factory Reset Handler Application");
return null;

View File

@@ -18,6 +18,8 @@ package com.android.settings.accessibility;
import static com.android.settings.accessibility.AccessibilityUtil.State.OFF;
import static com.android.settings.accessibility.MagnificationCapabilities.MagnificationMode;
import static com.android.settings.core.BasePreferenceController.AVAILABLE;
import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE;
import static com.google.common.truth.Truth.assertThat;
@@ -148,4 +150,15 @@ public class MagnificationAlwaysOnPreferenceControllerTest {
mController.updateState(mSwitchPreference);
assertThat(mSwitchPreference.isEnabled()).isTrue();
}
@Test
public void getAvailableStatus_notInSetupWizard_returnAvailable() {
assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
}
@Test
public void getAvailableStatus_inSetupWizard_returnConditionallyUnavailable() {
mController.setInSetupWizard(true);
assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE);
}
}

View File

@@ -17,6 +17,8 @@
package com.android.settings.accessibility;
import static com.android.settings.accessibility.AccessibilityUtil.State.OFF;
import static com.android.settings.core.BasePreferenceController.AVAILABLE;
import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE;
import static com.google.common.truth.Truth.assertThat;
@@ -61,6 +63,17 @@ public class MagnificationFollowTypingPreferenceControllerTest {
reset(mSwitchPreference);
}
@Test
public void getAvailableStatus_notInSetupWizard_returnAvailable() {
assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
}
@Test
public void getAvailableStatus_inSetupWizard_returnConditionallyUnavailable() {
mController.setInSetupWizard(true);
assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE);
}
@Test
public void performClick_switchDefaultStateForFollowTyping_shouldReturnFalse() {
mSwitchPreference.performClick();

View File

@@ -17,6 +17,8 @@
package com.android.settings.accessibility;
import static com.android.settings.accessibility.AccessibilityUtil.State.OFF;
import static com.android.settings.core.BasePreferenceController.AVAILABLE;
import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE;
import static com.google.common.truth.Truth.assertThat;
@@ -80,4 +82,15 @@ public class MagnificationJoystickPreferenceControllerTest {
assertThat(mController.isChecked()).isFalse();
assertThat(mSwitchPreference.isChecked()).isFalse();
}
@Test
public void getAvailableStatus_notInSetupWizard_returnAvailable() {
assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
}
@Test
public void getAvailableStatus_inSetupWizard_returnConditionallyUnavailable() {
mController.setInSetupWizard(true);
assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE);
}
}

View File

@@ -19,6 +19,8 @@ package com.android.settings.accessibility;
import static com.android.settings.accessibility.AccessibilityUtil.State.OFF;
import static com.android.settings.accessibility.AccessibilityUtil.State.ON;
import static com.android.settings.accessibility.MagnificationCapabilities.MagnificationMode;
import static com.android.settings.core.BasePreferenceController.AVAILABLE;
import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE;
import static com.google.common.truth.Truth.assertThat;
@@ -186,6 +188,17 @@ public class MagnificationOneFingerPanningPreferenceControllerTest {
assertThat(mSwitchPreference.isChecked()).isFalse();
}
@Test
public void getAvailableStatus_notInSetupWizard_returnAvailable() {
assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
}
@Test
public void getAvailableStatus_inSetupWizard_returnConditionallyUnavailable() {
mController.setInSetupWizard(true);
assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE);
}
private String enabledSummary() {
return mContext.getString(
R.string.accessibility_magnification_one_finger_panning_summary_on);

View File

@@ -32,7 +32,6 @@ import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreferenceCompat;
import androidx.test.core.app.ApplicationProvider;
import com.android.settings.R;
@@ -97,7 +96,6 @@ public class ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest {
verify(mFooterBarMixin).setPrimaryButton(any());
assertThat(mFragment.mTopIntroPreference.isVisible()).isFalse();
assertThat(mFragment.mSettingsPreference.isVisible()).isFalse();
assertThat(mFragment.mFollowingTypingSwitchPreference.isVisible()).isFalse();
}
@Test
@@ -124,7 +122,6 @@ public class ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest {
mPreferenceManager.setPreferences(mPreferenceManager.createPreferenceScreen(context));
mTopIntroPreference = new TopIntroPreference(context);
mSettingsPreference = new Preference(context);
mFollowingTypingSwitchPreference = new SwitchPreferenceCompat(context);
}
@Override

View File

@@ -90,7 +90,7 @@ class FingerprintEnrollIntroFragmentTest {
private val navigationViewModel =
FingerprintNavigationViewModel(
Introduction,
Introduction(),
false,
flowViewModel,
interactor

View File

@@ -0,0 +1,80 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.app.AutomaticZenRule;
import android.app.Flags;
import android.content.Context;
import android.net.Uri;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.ZenPolicy;
import androidx.preference.Preference;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.shadows.ShadowApplication;
import org.robolectric.util.ReflectionHelpers;
@RunWith(RobolectricTestRunner.class)
public final class ZenModeCallsLinkPreferenceControllerTest {
private ZenModeCallsLinkPreferenceController mController;
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private Context mContext;
@Mock
private ZenModesBackend mBackend;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
mController = new ZenModeCallsLinkPreferenceController(
mContext, "something", mBackend);
}
@Test
@EnableFlags(Flags.FLAG_MODES_UI)
public void testHasSummary() {
Preference pref = mock(Preference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
.build(), true);
mController.updateZenMode(pref, zenMode);
verify(pref).setSummary(any());
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.app.AutomaticZenRule;
import android.app.Flags;
import android.content.Context;
import android.net.Uri;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.ZenPolicy;
import androidx.preference.Preference;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
@RunWith(RobolectricTestRunner.class)
public final class ZenModeMessagesLinkPreferenceControllerTest {
private ZenModeMessagesLinkPreferenceController mController;
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private Context mContext;
@Mock
private ZenModesBackend mBackend;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
mController = new ZenModeMessagesLinkPreferenceController(
mContext, "something", mBackend);
}
@Test
@EnableFlags(Flags.FLAG_MODES_UI)
public void testHasSummary() {
Preference pref = mock(Preference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
.build(), true);
mController.updateZenMode(pref, zenMode);
verify(pref).setSummary(any());
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.app.AutomaticZenRule;
import android.app.Flags;
import android.content.Context;
import android.net.Uri;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.ZenPolicy;
import androidx.preference.Preference;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
@RunWith(RobolectricTestRunner.class)
@EnableFlags(Flags.FLAG_MODES_UI)
public final class ZenModeOtherLinkPreferenceControllerTest {
private ZenModeOtherLinkPreferenceController mController;
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private Context mContext;
@Mock
private ZenModesBackend mBackend;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
mController = new ZenModeOtherLinkPreferenceController(
mContext, "something", mBackend);
}
@Test
@EnableFlags(Flags.FLAG_MODES_UI)
public void testHasSummary() {
Preference pref = mock(Preference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
.build(), true);
mController.updateZenMode(pref, zenMode);
verify(pref).setSummary(any());
}
}

View File

@@ -0,0 +1,277 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
import static android.service.notification.ZenPolicy.STATE_ALLOW;
import static android.service.notification.ZenPolicy.STATE_UNSET;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.app.AutomaticZenRule;
import android.app.Flags;
import android.content.Context;
import android.net.Uri;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.ZenPolicy;
import androidx.preference.TwoStatePreference;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
@RunWith(RobolectricTestRunner.class)
@EnableFlags(Flags.FLAG_MODES_UI)
public final class ZenModeOtherPreferenceControllerTest {
private Context mContext;
@Mock
private ZenModesBackend mBackend;
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
}
@Test
public void testUpdateState_alarms() {
TwoStatePreference preference = mock(TwoStatePreference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).build())
.build(), true);
ZenModeOtherPreferenceController controller =
new ZenModeOtherPreferenceController(mContext, "modes_category_alarm", mBackend);
controller.updateZenMode(preference, zenMode);
verify(preference).setChecked(true);
}
@Test
public void testOnPreferenceChange_alarms() {
TwoStatePreference preference = mock(TwoStatePreference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder().allowAlarms(false).build())
.build(), true);
ZenModeOtherPreferenceController controller =
new ZenModeOtherPreferenceController(mContext, "modes_category_alarm", mBackend);
controller.updateZenMode(preference, zenMode);
controller.onPreferenceChange(preference, true);
ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
verify(mBackend).updateMode(captor.capture());
assertThat(captor.getValue().getPolicy().getPriorityCategoryAlarms())
.isEqualTo(STATE_ALLOW);
assertThat(captor.getValue().getPolicy().getPriorityCategoryEvents())
.isEqualTo(STATE_UNSET);
}
@Test
public void testUpdateState_media() {
TwoStatePreference preference = mock(TwoStatePreference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder().allowMedia(true).build())
.build(), true);
ZenModeOtherPreferenceController controller =
new ZenModeOtherPreferenceController(mContext, "modes_category_media", mBackend);
controller.updateZenMode(preference, zenMode);
verify(preference).setChecked(true);
}
@Test
public void testOnPreferenceChange_media() {
TwoStatePreference preference = mock(TwoStatePreference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder().allowMedia(false).build())
.build(), true);
ZenModeOtherPreferenceController controller =
new ZenModeOtherPreferenceController(mContext, "modes_category_media", mBackend);
controller.updateZenMode(preference, zenMode);
controller.onPreferenceChange(preference, true);
ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
verify(mBackend).updateMode(captor.capture());
assertThat(captor.getValue().getPolicy().getPriorityCategoryMedia())
.isEqualTo(STATE_ALLOW);
assertThat(captor.getValue().getPolicy().getPriorityCategoryEvents())
.isEqualTo(STATE_UNSET);
}
@Test
public void testUpdateState_system() {
TwoStatePreference preference = mock(TwoStatePreference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder().allowSystem(true).build())
.build(), true);
ZenModeOtherPreferenceController controller =
new ZenModeOtherPreferenceController(mContext, "modes_category_system", mBackend);
controller.updateZenMode(preference, zenMode);
verify(preference).setChecked(true);
}
@Test
public void testOnPreferenceChange_system() {
TwoStatePreference preference = mock(TwoStatePreference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder().allowSystem(false).build())
.build(), true);
ZenModeOtherPreferenceController controller =
new ZenModeOtherPreferenceController(mContext, "modes_category_system", mBackend);
controller.updateZenMode(preference, zenMode);
controller.onPreferenceChange(preference, true);
ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
verify(mBackend).updateMode(captor.capture());
assertThat(captor.getValue().getPolicy().getPriorityCategorySystem())
.isEqualTo(STATE_ALLOW);
assertThat(captor.getValue().getPolicy().getPriorityCategoryEvents())
.isEqualTo(STATE_UNSET);
}
@Test
public void testUpdateState_reminders() {
TwoStatePreference preference = mock(TwoStatePreference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder().allowReminders(true).build())
.build(), true);
ZenModeOtherPreferenceController controller =
new ZenModeOtherPreferenceController(mContext, "modes_category_reminders",
mBackend);
controller.updateZenMode(preference, zenMode);
verify(preference).setChecked(true);
}
@Test
public void testOnPreferenceChange_reminders() {
TwoStatePreference preference = mock(TwoStatePreference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder().allowReminders(false).build())
.build(), true);
ZenModeOtherPreferenceController controller =
new ZenModeOtherPreferenceController(mContext, "modes_category_reminders",
mBackend);
controller.updateZenMode(preference, zenMode);
controller.onPreferenceChange(preference, true);
ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
verify(mBackend).updateMode(captor.capture());
assertThat(captor.getValue().getPolicy().getPriorityCategoryReminders())
.isEqualTo(STATE_ALLOW);
assertThat(captor.getValue().getPolicy().getPriorityCategoryEvents())
.isEqualTo(STATE_UNSET);
}
@Test
public void testUpdateState_events() {
TwoStatePreference preference = mock(TwoStatePreference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder().allowEvents(true).build())
.build(), true);
ZenModeOtherPreferenceController controller =
new ZenModeOtherPreferenceController(mContext, "modes_category_events", mBackend);
controller.updateZenMode(preference, zenMode);
verify(preference).setChecked(true);
}
@Test
public void testOnPreferenceChange_events() {
TwoStatePreference preference = mock(TwoStatePreference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder().allowEvents(false).build())
.build(), true);
ZenModeOtherPreferenceController controller =
new ZenModeOtherPreferenceController(mContext, "modes_category_events", mBackend);
controller.updateZenMode(preference, zenMode);
controller.onPreferenceChange(preference, true);
ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
verify(mBackend).updateMode(captor.capture());
assertThat(captor.getValue().getPolicy().getPriorityCategoryEvents())
.isEqualTo(STATE_ALLOW);
assertThat(captor.getValue().getPolicy().getPriorityCategoryAlarms())
.isEqualTo(STATE_UNSET);
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.app.AutomaticZenRule;
import android.app.Flags;
import android.content.Context;
import android.net.Uri;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.ZenPolicy;
import androidx.preference.Preference;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
@RunWith(RobolectricTestRunner.class)
public final class ZenModePeopleLinkPreferenceControllerTest {
private ZenModePeopleLinkPreferenceController mController;
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private Context mContext;
@Mock
private ZenModesBackend mBackend;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
mController = new ZenModePeopleLinkPreferenceController(
mContext, "something", mBackend);
}
@Test
@EnableFlags(Flags.FLAG_MODES_UI)
public void testHasSummary() {
Preference pref = mock(Preference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
.build(), true);
mController.updateZenMode(pref, zenMode);
verify(pref).setSummary(any());
}
}

View File

@@ -0,0 +1,509 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE;
import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE;
import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_UNSET;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_UNSET;
import static android.service.notification.ZenPolicy.STATE_ALLOW;
import static android.service.notification.ZenPolicy.STATE_DISALLOW;
import static android.service.notification.ZenPolicy.STATE_UNSET;
import static com.android.settings.notification.modes.ZenModePrioritySendersPreferenceController.KEY_ANY;
import static com.android.settings.notification.modes.ZenModePrioritySendersPreferenceController.KEY_CONTACTS;
import static com.android.settings.notification.modes.ZenModePrioritySendersPreferenceController.KEY_IMPORTANT;
import static com.android.settings.notification.modes.ZenModePrioritySendersPreferenceController.KEY_NONE;
import static com.android.settings.notification.modes.ZenModePrioritySendersPreferenceController.KEY_STARRED;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.AutomaticZenRule;
import android.app.Flags;
import android.content.Context;
import android.net.Uri;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.ZenPolicy;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import com.android.settingslib.widget.SelectorWithWidgetPreference;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
@RunWith(RobolectricTestRunner.class)
@EnableFlags(Flags.FLAG_MODES_UI)
public final class ZenModePrioritySendersPreferenceControllerTest {
private ZenModePrioritySendersPreferenceController mCallsController;
private ZenModePrioritySendersPreferenceController mMessagesController;
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private Context mContext;
@Mock
private ZenModesBackend mBackend;
@Mock
private PreferenceCategory mMockMessagesPrefCategory, mMockCallsPrefCategory;
@Mock
private PreferenceScreen mPreferenceScreen;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
mMessagesController = new ZenModePrioritySendersPreferenceController(
mContext, "messages", true, mBackend);
mCallsController = new ZenModePrioritySendersPreferenceController(
mContext, "calls", false, mBackend);
when(mMockMessagesPrefCategory.getContext()).thenReturn(mContext);
when(mMockCallsPrefCategory.getContext()).thenReturn(mContext);
when(mPreferenceScreen.findPreference(mMessagesController.getPreferenceKey()))
.thenReturn(mMockMessagesPrefCategory);
when(mPreferenceScreen.findPreference(mCallsController.getPreferenceKey()))
.thenReturn(mMockCallsPrefCategory);
}
// Makes a preference with the provided key and whether it's a checkbox with
// mSelectorClickListener as the onClickListener set.
private SelectorWithWidgetPreference makePreference(
String key, boolean isCheckbox, boolean isMessages) {
final SelectorWithWidgetPreference pref =
new SelectorWithWidgetPreference(mContext, isCheckbox);
pref.setKey(key);
pref.setOnClickListener(
isMessages ? mMessagesController.mSelectorClickListener
: mCallsController.mSelectorClickListener);
return pref;
}
// Extension of ArgumentMatcher to check that a preference argument has the correct preference
// key, but doesn't check any other properties.
private class PrefKeyMatcher implements ArgumentMatcher<SelectorWithWidgetPreference> {
private String mKey;
PrefKeyMatcher(String key) {
mKey = key;
}
public boolean matches(SelectorWithWidgetPreference pref) {
return pref.getKey() != null && pref.getKey().equals(mKey);
}
public String toString() {
return "SelectorWithWidgetPreference matcher for key " + mKey;
}
}
@Test
public void testDisplayPreferences_makeMessagesPrefs() {
ArgumentCaptor<SelectorWithWidgetPreference> prefCaptor =
ArgumentCaptor.forClass(SelectorWithWidgetPreference.class);
when(mMockMessagesPrefCategory.getPreferenceCount()).thenReturn(0); // not yet created
mMessagesController.displayPreference(mPreferenceScreen);
// Starred contacts, Contacts, Priority Conversations, Any, None
verify(mMockMessagesPrefCategory, times(5)).addPreference(prefCaptor.capture());
}
@Test
public void testDisplayPreferences_makeCallsPrefs() {
ArgumentCaptor<SelectorWithWidgetPreference> prefCaptor =
ArgumentCaptor.forClass(SelectorWithWidgetPreference.class);
when(mMockCallsPrefCategory.getPreferenceCount()).thenReturn(0); // not yet created
mCallsController.displayPreference(mPreferenceScreen);
// Starred contacts, Contacts, Any, None
verify(mMockCallsPrefCategory, times(4)).addPreference(prefCaptor.capture());
// Make sure we never have the conversation one
verify(mMockCallsPrefCategory, never())
.addPreference(argThat(new PrefKeyMatcher(KEY_IMPORTANT)));
}
@Test
public void testDisplayPreferences_createdOnlyOnce() {
// Return a nonzero number of child preference when asked.
// Then when displayPreference is called, it should never make any new preferences.
when(mMockCallsPrefCategory.getPreferenceCount()).thenReturn(4); // already created
mCallsController.displayPreference(mPreferenceScreen);
mCallsController.displayPreference(mPreferenceScreen);
mCallsController.displayPreference(mPreferenceScreen);
// Even though we called display 3 times we shouldn't add more preferences here.
verify(mMockCallsPrefCategory, never())
.addPreference(any(SelectorWithWidgetPreference.class));
}
@Test
public void testKeyToSettingEndState_messagesCheck() {
int[] endState;
// For KEY_NONE everything should be none.
endState = mMessagesController.keyToSettingEndState(KEY_NONE, true);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE);
// For KEY_ANY everything should be allowed.
endState = mMessagesController.keyToSettingEndState(KEY_ANY, true);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_ANYONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_ANYONE);
// For [starred] contacts, we should set the priority senders, but not the conversations
endState = mMessagesController.keyToSettingEndState(KEY_STARRED, true);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_STARRED);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
endState = mMessagesController.keyToSettingEndState(KEY_CONTACTS, true);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_CONTACTS);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
// For priority conversations, we should set the conversations but not priority senders
endState = mMessagesController.keyToSettingEndState(KEY_IMPORTANT, true);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_UNSET);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_IMPORTANT);
}
@Test
public void testKeyToSettingEndState_messagesUncheck() {
int[] endState;
// For KEY_NONE, "unchecking" still means "none".
endState = mMessagesController.keyToSettingEndState(KEY_NONE, false);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE);
// For KEY_ANY unchecking resets the state to "none".
endState = mMessagesController.keyToSettingEndState(KEY_ANY, false);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE);
// For [starred] contacts, we should unset the priority senders, but not the conversations
endState = mMessagesController.keyToSettingEndState(KEY_STARRED, false);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
endState = mMessagesController.keyToSettingEndState(KEY_CONTACTS, false);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
// For priority conversations, we should set the conversations but not priority senders
endState = mMessagesController.keyToSettingEndState(KEY_IMPORTANT, false);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_UNSET);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE);
}
@Test
public void testKeyToSettingEndState_callsCheck() {
int[] endState;
// For calls: we should never set conversations, as this is unrelated to calls.
// For KEY_NONE senders should be none.
endState = mCallsController.keyToSettingEndState(KEY_NONE, true);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
// For KEY_ANY senders should be ANY.
endState = mCallsController.keyToSettingEndState(KEY_ANY, true);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_ANYONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
// For [starred] contacts, we should set the priority senders accordingly
endState = mCallsController.keyToSettingEndState(KEY_STARRED, true);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_STARRED);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
endState = mCallsController.keyToSettingEndState(KEY_CONTACTS, true);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_CONTACTS);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
}
@Test
public void testKeyToSettingEndState_callsUncheck() {
int[] endState;
// A calls setup should never set conversations settings.
// For KEY_NONE, "unchecking" still means "none".
endState = mCallsController.keyToSettingEndState(KEY_NONE, false);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
// For KEY_ANY unchecking resets the state to "none".
endState = mCallsController.keyToSettingEndState(KEY_ANY, false);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
// For [starred] contacts, we should unset the priority senders, but not the conversations
endState = mCallsController.keyToSettingEndState(KEY_STARRED, false);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
endState = mCallsController.keyToSettingEndState(KEY_CONTACTS, false);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
}
@Test
public void testSettingsToSaveOnClick_messagesCheck() {
SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true);
SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true);
SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true, true);
SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true, true);
SelectorWithWidgetPreference impPref = makePreference(KEY_IMPORTANT, true, true);
int[] endState;
// For KEY_NONE everything should be none.
nonePref.setChecked(true);
endState = mMessagesController.settingsToSaveOnClick(
nonePref, PEOPLE_TYPE_ANYONE, CONVERSATION_SENDERS_ANYONE);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE);
// For KEY_ANY everything should be allowed.
anyPref.setChecked(true);
endState = mMessagesController.settingsToSaveOnClick(
anyPref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_ANYONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_ANYONE);
// For [starred] contacts, we should set the priority senders, but not the conversations
starredPref.setChecked(true);
endState = mMessagesController.settingsToSaveOnClick(
starredPref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_STARRED);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
contactsPref.setChecked(true);
endState = mMessagesController.settingsToSaveOnClick(
contactsPref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_CONTACTS);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
// For priority conversations, we should set the conversations but not priority senders
impPref.setChecked(true);
endState = mMessagesController.settingsToSaveOnClick(
impPref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_UNSET);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_IMPORTANT);
}
@Test
public void testSettingsToSaveOnClick_messagesUncheck() {
int[] endState;
SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true);
SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true);
SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true, true);
SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true, true);
SelectorWithWidgetPreference impPref = makePreference(KEY_IMPORTANT, true, true);
// For KEY_NONE, "unchecking" still means "none".
nonePref.setChecked(false);
endState = mMessagesController.settingsToSaveOnClick(
nonePref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_UNSET);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
// For KEY_ANY unchecking resets the state to "none".
anyPref.setChecked(false);
endState = mMessagesController.settingsToSaveOnClick(
anyPref, PEOPLE_TYPE_ANYONE, CONVERSATION_SENDERS_ANYONE);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE);
// For [starred] contacts, we should unset the priority senders, but not the conversations
starredPref.setChecked(false);
endState = mMessagesController.settingsToSaveOnClick(
starredPref, PEOPLE_TYPE_STARRED, CONVERSATION_SENDERS_IMPORTANT);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
contactsPref.setChecked(false);
endState = mMessagesController.settingsToSaveOnClick(
contactsPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
// For priority conversations, we should set the conversations but not priority senders
impPref.setChecked(false);
endState = mMessagesController.settingsToSaveOnClick(
impPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_UNSET);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE);
}
@Test
public void testSettingsToSaveOnClick_callsCheck() {
int[] endState;
SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true);
SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true);
SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true, true);
SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true, true);
// For calls: we should never set conversations, as this is unrelated to calls.
// For KEY_NONE senders should be none.
nonePref.setChecked(true);
endState = mCallsController.settingsToSaveOnClick(
nonePref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
// For KEY_ANY senders should be ANY.
anyPref.setChecked(true);
endState = mCallsController.settingsToSaveOnClick(
anyPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_ANYONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
// For [starred] contacts, we should set the priority senders accordingly
starredPref.setChecked(true);
endState = mCallsController.settingsToSaveOnClick(
starredPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_STARRED);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
contactsPref.setChecked(true);
endState = mCallsController.settingsToSaveOnClick(
contactsPref, PEOPLE_TYPE_STARRED, CONVERSATION_SENDERS_IMPORTANT);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_CONTACTS);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
}
@Test
public void testSettingsToSaveOnClick_callsUncheck() {
int[] endState;
SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true);
SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true);
SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true, true);
SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true, true);
// A calls setup should never set conversations settings.
// For KEY_NONE, "unchecking" still means "none".
nonePref.setChecked(false);
endState = mCallsController.settingsToSaveOnClick(
nonePref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE);
assertThat(endState[0]).isEqualTo(STATE_UNSET);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
// For KEY_ANY unchecking resets the state to "none".
anyPref.setChecked(false);
endState = mCallsController.settingsToSaveOnClick(
anyPref, PEOPLE_TYPE_ANYONE, CONVERSATION_SENDERS_ANYONE);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
// For [starred] contacts, we should unset the priority senders, but not the conversations
starredPref.setChecked(false);
endState = mCallsController.settingsToSaveOnClick(
starredPref, PEOPLE_TYPE_STARRED, CONVERSATION_SENDERS_IMPORTANT);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
contactsPref.setChecked(false);
endState = mCallsController.settingsToSaveOnClick(
contactsPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT);
assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE);
assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET);
}
@Test
public void testSettingsToSave_messages_noChange() {
int[] savedSettings;
SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true);
nonePref.setChecked(true);
savedSettings = mMessagesController.settingsToSaveOnClick(
nonePref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE);
assertThat(savedSettings[0]).isEqualTo(STATE_UNSET);
assertThat(savedSettings[1]).isEqualTo(STATE_UNSET);
SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true);
anyPref.setChecked(true);
savedSettings = mMessagesController.settingsToSaveOnClick(
anyPref, PEOPLE_TYPE_ANYONE, CONVERSATION_SENDERS_ANYONE);
assertThat(savedSettings[0]).isEqualTo(STATE_UNSET);
assertThat(savedSettings[1]).isEqualTo(STATE_UNSET);
SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true, true);
SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true, true);
starredPref.setChecked(true);
savedSettings = mMessagesController.settingsToSaveOnClick(
starredPref, PEOPLE_TYPE_STARRED, CONVERSATION_SENDERS_ANYONE);
assertThat(savedSettings[0]).isEqualTo(STATE_UNSET);
contactsPref.setChecked(true);
savedSettings = mMessagesController.settingsToSaveOnClick(
contactsPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_ANYONE);
assertThat(savedSettings[0]).isEqualTo(STATE_UNSET);
SelectorWithWidgetPreference impPref = makePreference(KEY_IMPORTANT, true, true);
impPref.setChecked(true);
savedSettings = mMessagesController.settingsToSaveOnClick(
impPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT);
assertThat(savedSettings[1]).isEqualTo(STATE_UNSET);
}
@Test
public void testSettingsToSave_calls_noChange() {
int[] savedSettings;
SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, false, false);
savedSettings = mMessagesController.settingsToSaveOnClick(
nonePref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE);
assertThat(savedSettings[0]).isEqualTo(STATE_UNSET);
assertThat(savedSettings[1]).isEqualTo(STATE_UNSET);
SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, false, false);
savedSettings = mMessagesController.settingsToSaveOnClick(
anyPref, PEOPLE_TYPE_ANYONE, CONVERSATION_SENDERS_ANYONE);
assertThat(savedSettings[0]).isEqualTo(STATE_UNSET);
assertThat(savedSettings[1]).isEqualTo(STATE_UNSET);
SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, false, false);
SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, false, false);
savedSettings = mMessagesController.settingsToSaveOnClick(
starredPref, PEOPLE_TYPE_STARRED, CONVERSATION_SENDERS_ANYONE);
assertThat(savedSettings[0]).isEqualTo(STATE_UNSET);
savedSettings = mMessagesController.settingsToSaveOnClick(
contactsPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_ANYONE);
assertThat(savedSettings[0]).isEqualTo(STATE_UNSET);
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED;
import static android.service.notification.ZenPolicy.STATE_ALLOW;
import static android.service.notification.ZenPolicy.STATE_DISALLOW;
import static android.service.notification.ZenPolicy.STATE_UNSET;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.app.AutomaticZenRule;
import android.app.Flags;
import android.content.Context;
import android.net.Uri;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.ZenPolicy;
import androidx.preference.TwoStatePreference;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
@RunWith(RobolectricTestRunner.class)
@EnableFlags(Flags.FLAG_MODES_UI)
public final class ZenModeRepeatCallersPreferenceControllerTest {
private Context mContext;
@Mock
private ZenModesBackend mBackend;
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
}
@Test
public void testUpdateState_allCalls() {
TwoStatePreference preference = mock(TwoStatePreference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder()
.allowCalls(PEOPLE_TYPE_ANYONE)
.build())
.build(), true);
ZenModeRepeatCallersPreferenceController controller =
new ZenModeRepeatCallersPreferenceController(mContext, "repeat", mBackend, 1);
controller.updateZenMode(preference, zenMode);
verify(preference).setChecked(true);
verify(preference).setEnabled(false);
}
@Test
public void testUpdateState_someCalls() {
TwoStatePreference preference = mock(TwoStatePreference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder()
.allowCalls(PEOPLE_TYPE_STARRED)
.allowRepeatCallers(true)
.build())
.build(), true);
ZenModeRepeatCallersPreferenceController controller =
new ZenModeRepeatCallersPreferenceController(mContext, "repeat", mBackend, 1);
controller.updateZenMode(preference, zenMode);
verify(preference).setChecked(true);
verify(preference).setEnabled(true);
}
@Test
public void testOnPreferenceChange() {
TwoStatePreference preference = mock(TwoStatePreference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(true).build())
.build(), true);
ZenModeRepeatCallersPreferenceController controller =
new ZenModeRepeatCallersPreferenceController(mContext, "repeat", mBackend, 1);
controller.updateZenMode(preference, zenMode);
controller.onPreferenceChange(preference, false);
ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
verify(mBackend).updateMode(captor.capture());
assertThat(captor.getValue().getPolicy().getPriorityCategoryRepeatCallers())
.isEqualTo(STATE_DISALLOW);
assertThat(captor.getValue().getPolicy().getPriorityCategoryEvents())
.isEqualTo(STATE_UNSET);
assertThat(captor.getValue().getPolicy().getPriorityCallSenders())
.isEqualTo(STATE_UNSET);
}
}

View File

@@ -0,0 +1,168 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.notification.modes;
import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS;
import static com.google.common.truth.Truth.assertThat;
import android.app.AutomaticZenRule;
import android.content.Context;
import android.net.Uri;
import android.service.notification.ZenPolicy;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
@RunWith(RobolectricTestRunner.class)
public class ZenModesSummaryHelperTest {
private Context mContext;
private ZenModesBackend mBackend;
private ZenModeSummaryHelper mSummaryHelper;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
mBackend = new ZenModesBackend(mContext);
mSummaryHelper = new ZenModeSummaryHelper(mContext, mBackend);
}
@Test
public void getPeopleSummary_noOne() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
.setType(AutomaticZenRule.TYPE_BEDTIME)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build())
.build();
ZenMode zenMode = new ZenMode("id", rule, true);
assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("No one can interrupt");
}
@Test
public void getPeopleSummary_some() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
.setType(AutomaticZenRule.TYPE_BEDTIME)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder().allowCalls(PEOPLE_TYPE_CONTACTS).build())
.build();
ZenMode zenMode = new ZenMode("id", rule, true);
assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("Some people can interrupt");
}
@Test
public void getPeopleSummary_all() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
.setType(AutomaticZenRule.TYPE_BEDTIME)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder().allowCalls(PEOPLE_TYPE_ANYONE).
allowConversations(CONVERSATION_SENDERS_ANYONE)
.allowMessages(PEOPLE_TYPE_ANYONE).build())
.build();
ZenMode zenMode = new ZenMode("id", rule, true);
assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("All people can interrupt");
}
@Test
public void getOtherSoundCategoriesSummary_single() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
.setType(AutomaticZenRule.TYPE_BEDTIME)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).build())
.build();
ZenMode zenMode = new ZenMode("id", rule, true);
assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo(
"Alarms can interrupt");
}
@Test
public void getOtherSoundCategoriesSummary_duo() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
.setType(AutomaticZenRule.TYPE_BEDTIME)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).allowMedia(true).build())
.build();
ZenMode zenMode = new ZenMode("id", rule, true);
assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo(
"Alarms and media can interrupt");
}
@Test
public void getOtherSoundCategoriesSummary_trio() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
.setType(AutomaticZenRule.TYPE_BEDTIME)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder()
.allowAlarms(true)
.allowMedia(true)
.allowSystem(true)
.build())
.build();
ZenMode zenMode = new ZenMode("id", rule, true);
assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo(
"Alarms, media, and touch sounds can interrupt");
}
@Test
public void getOtherSoundCategoriesSummary_quad() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
.setType(AutomaticZenRule.TYPE_BEDTIME)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder()
.allowAlarms(true)
.allowMedia(true)
.allowSystem(true)
.allowReminders(true)
.build())
.build();
ZenMode zenMode = new ZenMode("id", rule, true);
assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo(
"Alarms, media, and 2 more can interrupt");
}
@Test
public void getOtherSoundCategoriesSummary_all() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
.setType(AutomaticZenRule.TYPE_BEDTIME)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder()
.allowAlarms(true)
.allowMedia(true)
.allowSystem(true)
.allowReminders(true)
.allowEvents(true)
.build())
.build();
ZenMode zenMode = new ZenMode("id", rule, true);
assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo(
"Alarms, media, and 3 more can interrupt");
}
}

View File

@@ -145,10 +145,13 @@ public class FactoryResetPreferenceControllerTest {
@Test
@RequiresFlagsEnabled(com.android.settings.factory_reset.Flags.FLAG_ENABLE_FACTORY_RESET_WIZARD)
public void handlePreference_factoryResetWizardEnabled() {
public void handlePreference_factoryResetWizardEnabled()
throws PackageManager.NameNotFoundException {
ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
assertThat(mController.handlePreferenceTreeClick(mPreference)).isTrue();
verify(mPackageManager).getPackageInfo(eq(FACTORY_RESET_APP_PACKAGE),
eq(PackageManager.GET_PERMISSIONS));
verify(mFactoryResetLauncher).launch(intentArgumentCaptor.capture());
assertThat(intentArgumentCaptor.getValue()).isNotNull();
assertThat(intentArgumentCaptor.getValue().getAction())

View File

@@ -28,7 +28,7 @@ import platform.test.screenshot.ViewScreenshotTestRule.Mode
@RunWith(AndroidJUnit4::class)
class FingerprintEnrollIntroScreenshotTest {
private val injector: Injector = Injector(FingerprintNavigationStep.Introduction)
private val injector: Injector = Injector(FingerprintNavigationStep.Introduction())
@Rule
@JvmField

View File

@@ -17,14 +17,13 @@
package com.android.settings.spa.app.specialaccess
import android.Manifest
import android.app.AppOpsManager
import android.content.Context
import android.content.pm.ApplicationInfo
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
import com.android.settingslib.spaprivileged.model.app.IAppOpsController
import com.android.settingslib.spaprivileged.model.app.IAppOpsPermissionController
import com.android.settingslib.spaprivileged.model.app.IPackageManagers
import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord
import com.google.common.truth.Truth.assertThat
@@ -117,14 +116,14 @@ class WifiControlAppListModelTest {
app = APP_NOT_REQUEST_PERMISSION,
hasRequestPermission = false,
hasRequestBroaderPermission = false,
appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
appOpsPermissionController = FakeAppOpsPermissionController(false),
)
val appRequestedNetworkSettingsRecord =
AppOpPermissionRecord(
app = APP_REQUESTED_NETWORK_SETTINGS,
hasRequestPermission = true,
hasRequestBroaderPermission = false,
appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT)
appOpsPermissionController = FakeAppOpsPermissionController(false),
)
val recordListFlow =
@@ -144,7 +143,7 @@ class WifiControlAppListModelTest {
app = APP,
hasRequestPermission = false,
hasRequestBroaderPermission = true,
appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
appOpsPermissionController = FakeAppOpsPermissionController(false),
)
val isAllowed = getIsAllowed(record)
@@ -159,7 +158,7 @@ class WifiControlAppListModelTest {
app = APP,
hasRequestPermission = true,
hasRequestBroaderPermission = false,
appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ALLOWED),
appOpsPermissionController = FakeAppOpsPermissionController(true),
)
val isAllowed = getIsAllowed(record)
@@ -174,7 +173,7 @@ class WifiControlAppListModelTest {
app = APP,
hasRequestPermission = true,
hasRequestBroaderPermission = false,
appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_IGNORED),
appOpsPermissionController = FakeAppOpsPermissionController(false),
)
val isAllowed = getIsAllowed(record)
@@ -189,7 +188,7 @@ class WifiControlAppListModelTest {
app = APP,
hasRequestPermission = false,
hasRequestBroaderPermission = false,
appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
appOpsPermissionController = FakeAppOpsPermissionController(false),
)
val isChangeable = listModel.isChangeable(record)
@@ -198,13 +197,13 @@ class WifiControlAppListModelTest {
}
@Test
fun isChangeable_notChangableWhenRequestedNetworkSettingPermissions() {
fun isChangeable_notChangeableWhenRequestedNetworkSettingPermissions() {
val record =
AppOpPermissionRecord(
app = APP,
hasRequestPermission = false,
hasRequestBroaderPermission = true,
appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
appOpsPermissionController = FakeAppOpsPermissionController(false),
)
val isChangeable = listModel.isChangeable(record)
@@ -213,13 +212,13 @@ class WifiControlAppListModelTest {
}
@Test
fun isChangeable_changableWhenRequestedChangeWifiStatePermission() {
fun isChangeable_changeableWhenRequestedChangeWifiStatePermission() {
val record =
AppOpPermissionRecord(
app = APP,
hasRequestPermission = true,
hasRequestBroaderPermission = false,
appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
appOpsPermissionController = FakeAppOpsPermissionController(false),
)
val isChangeable = listModel.isChangeable(record)
@@ -229,18 +228,18 @@ class WifiControlAppListModelTest {
@Test
fun setAllowed_shouldCallController() {
val appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT)
val appOpsPermissionController = FakeAppOpsPermissionController(false)
val record =
AppOpPermissionRecord(
app = APP,
hasRequestPermission = true,
hasRequestBroaderPermission = false,
appOpsController = appOpsController,
appOpsPermissionController = appOpsPermissionController,
)
listModel.setAllowed(record = record, newAllowed = true)
assertThat(appOpsController.setAllowedCalledWith).isTrue()
assertThat(appOpsPermissionController.setAllowedCalledWith).isTrue()
}
private fun getIsAllowed(record: AppOpPermissionRecord): Boolean? {
@@ -266,14 +265,12 @@ class WifiControlAppListModelTest {
}
}
private class FakeAppOpsController(private val fakeMode: Int) : IAppOpsController {
private class FakeAppOpsPermissionController(allowed: Boolean) : IAppOpsPermissionController {
var setAllowedCalledWith: Boolean? = null
override val modeFlow = flowOf(fakeMode)
override val isAllowedFlow = flowOf(allowed)
override fun setAllowed(allowed: Boolean) {
setAllowedCalledWith = allowed
}
override fun getMode() = fakeMode
}

View File

@@ -30,10 +30,10 @@ import com.android.media.flags.Flags
import com.android.settings.R
import com.android.settings.testutils.FakeFeatureFactory
import com.android.settingslib.spaprivileged.model.app.AppOps
import com.android.settingslib.spaprivileged.model.app.IAppOpsController
import com.android.settingslib.spaprivileged.model.app.IAppOpsPermissionController
import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -50,7 +50,7 @@ class MediaRoutingControlTest {
@get:Rule
val mockito: MockitoRule = MockitoJUnit.rule()
@get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule();
@get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule()
@Spy
private val context: Context = ApplicationProvider.getApplicationContext()
@@ -86,29 +86,29 @@ class MediaRoutingControlTest {
@Test
fun setAllowed_callWithNewStatusAsTrue_shouldChangeAppControllerModeToAllowed() {
val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT)
val fakeAppOpsPermissionController = FakeAppOpsPermissionController(false)
val permissionRequestedRecord =
AppOpPermissionRecord(
app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
hasRequestPermission = true,
hasRequestBroaderPermission = false,
appOpsController = fakeAppOpController,
appOpsPermissionController = fakeAppOpsPermissionController,
)
listModel.setAllowed(permissionRequestedRecord, true)
assertThat(fakeAppOpController.getMode()).isEqualTo(AppOpsManager.MODE_ALLOWED)
assertThat(fakeAppOpsPermissionController.setAllowedCalledWith).isTrue()
}
@Test
fun setAllowed_callWithNewStatusAsTrue_shouldLogPermissionToggleActionAsAllowed() {
val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT)
val fakeAppOpsPermissionController = FakeAppOpsPermissionController(false)
val permissionRequestedRecord =
AppOpPermissionRecord(
app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
hasRequestPermission = true,
hasRequestBroaderPermission = false,
appOpsController = fakeAppOpController,
appOpsPermissionController = fakeAppOpsPermissionController,
)
listModel.setAllowed(permissionRequestedRecord, true)
@@ -119,29 +119,29 @@ class MediaRoutingControlTest {
@Test
fun setAllowed_callWithNewStatusAsFalse_shouldChangeAppControllerModeToErrored() {
val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT)
val fakeAppOpsPermissionController = FakeAppOpsPermissionController(false)
val permissionRequestedRecord =
AppOpPermissionRecord(
app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
hasRequestPermission = true,
hasRequestBroaderPermission = false,
appOpsController = fakeAppOpController,
appOpsPermissionController = fakeAppOpsPermissionController,
)
listModel.setAllowed(permissionRequestedRecord, false)
assertThat(fakeAppOpController.getMode()).isEqualTo(AppOpsManager.MODE_ERRORED)
assertThat(fakeAppOpsPermissionController.setAllowedCalledWith).isFalse()
}
@Test
fun setAllowed_callWithNewStatusAsFalse_shouldLogPermissionToggleActionAsDenied() {
val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT)
val fakeAppOpsPermissionController = FakeAppOpsPermissionController(false)
val permissionRequestedRecord =
AppOpPermissionRecord(
app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
hasRequestPermission = true,
hasRequestBroaderPermission = false,
appOpsController = fakeAppOpController,
appOpsPermissionController = fakeAppOpsPermissionController,
)
listModel.setAllowed(permissionRequestedRecord, false)
@@ -158,8 +158,7 @@ class MediaRoutingControlTest {
app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
hasRequestPermission = true,
hasRequestBroaderPermission = false,
appOpsController =
FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
appOpsPermissionController = FakeAppOpsPermissionController(false),
)
whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH))
.thenReturn(listOf(PACKAGE_NAME))
@@ -177,8 +176,7 @@ class MediaRoutingControlTest {
app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
hasRequestPermission = false,
hasRequestBroaderPermission = false,
appOpsController =
FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
appOpsPermissionController = FakeAppOpsPermissionController(false),
)
whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH))
.thenReturn(listOf(PACKAGE_NAME))
@@ -196,8 +194,7 @@ class MediaRoutingControlTest {
app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
hasRequestPermission = true,
hasRequestBroaderPermission = false,
appOpsController =
FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
appOpsPermissionController = FakeAppOpsPermissionController(false),
)
whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH))
.thenReturn(listOf("other.package.name"))
@@ -215,8 +212,7 @@ class MediaRoutingControlTest {
app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
hasRequestPermission = true,
hasRequestBroaderPermission = false,
appOpsController =
FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
appOpsPermissionController = FakeAppOpsPermissionController(false),
)
whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH))
.thenReturn(listOf(PACKAGE_NAME))
@@ -226,15 +222,14 @@ class MediaRoutingControlTest {
assertThat(isSpecialAccessChangeable).isFalse()
}
private class FakeAppOpsController(fakeMode: Int) : IAppOpsController {
private class FakeAppOpsPermissionController(allowed: Boolean) : IAppOpsPermissionController {
var setAllowedCalledWith: Boolean? = null
override val modeFlow = MutableStateFlow(fakeMode)
override val isAllowedFlow = flowOf(allowed)
override fun setAllowed(allowed: Boolean) {
modeFlow.value = if (allowed) AppOpsManager.MODE_ALLOWED else AppOpsManager.MODE_ERRORED
setAllowedCalledWith = allowed
}
override fun getMode(): Int = modeFlow.value
}
companion object {

View File

@@ -41,7 +41,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -754,12 +753,11 @@ public class UiccSlotUtilTest {
}
@Test
@Ignore("b/337417975")
public void onReceiveSimCardStateChangeReceiver_receiveAction_timerCountDown() {
CountDownLatch latch = spy(new CountDownLatch(1));
UiccSlotUtil.SimCardStateChangeReceiver receive =
new UiccSlotUtil.SimCardStateChangeReceiver(latch);
Intent intent = new Intent(TelephonyManager.ACTION_SIM_SLOT_STATUS_CHANGED);
Intent intent = new Intent(TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED);
intent.putExtra(TelephonyManager.EXTRA_SIM_STATE, TelephonyManager.SIM_STATE_PRESENT);
receive.onReceive(mContext, intent);