diff --git a/res/values/strings.xml b/res/values/strings.xml index 2e98ec57de3..7018565b942 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -8409,7 +8409,7 @@ Use personal profile sounds - Sounds are the same for work and personal profiles + Use the same sounds as your personal profile Work phone ringtone @@ -8424,13 +8424,13 @@ Same as personal profile - Replace sounds? + Use personal profile sounds? - Replace + Confirm - Your personal profile sounds will be used for your work profile + Your work profile will use the same sounds as your personal profile Add custom sound? diff --git a/res/xml/sound_settings_v2.xml b/res/xml/sound_settings_v2.xml index 22a3581bc19..b45dd9bec99 100644 --- a/res/xml/sound_settings_v2.xml +++ b/res/xml/sound_settings_v2.xml @@ -212,43 +212,10 @@ android:summary="%s" android:order="-10"/> - - - - - - - - - - - - - - - - + android:fragment="com.android.settings.notification.SoundWorkSettings" + android:order="100" + settings:controller="com.android.settings.notification.WorkSoundsPreferenceController"/> diff --git a/res/xml/sound_work_settings.xml b/res/xml/sound_work_settings.xml new file mode 100644 index 00000000000..8e48faca773 --- /dev/null +++ b/res/xml/sound_work_settings.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/com/android/settings/notification/SoundSettings.java b/src/com/android/settings/notification/SoundSettings.java index fc209b8cff3..7c665d11e0c 100644 --- a/src/com/android/settings/notification/SoundSettings.java +++ b/src/com/android/settings/notification/SoundSettings.java @@ -25,6 +25,7 @@ import android.os.Looper; import android.os.Message; import android.os.UserHandle; import android.preference.SeekBarVolumizer; +import android.provider.SearchIndexableResource; import android.text.TextUtils; import android.util.FeatureFlagUtils; @@ -250,8 +251,11 @@ public class SoundSettings extends DashboardFragment implements OnActivityResult controllers.add(new AlarmRingtonePreferenceController(context)); controllers.add(new NotificationRingtonePreferenceController(context)); - // === Work Sound Settings === - controllers.add(new WorkSoundPreferenceController(context, fragment, lifecycle)); + if (!FeatureFlagUtils.isEnabled(context, FeatureFlags.SILKY_HOME)) { + // TODO(b/174964721): This should be removed when the flag is deprecated. + // === Work Sound Settings === + controllers.add(new WorkSoundPreferenceController(context, fragment, lifecycle)); + } // === Other Sound Settings === final DialPadTonePreferenceController dialPadTonePreferenceController = @@ -308,15 +312,27 @@ public class SoundSettings extends DashboardFragment implements OnActivityResult return buildPreferenceControllers(context, null /* fragment */, null /* lifecycle */); } + + @Override + public List getXmlResourcesToIndex( + Context context, boolean enabled) { + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = FeatureFlagUtils.isEnabled(context, FeatureFlags.SILKY_HOME) + ? R.xml.sound_settings_v2 : R.xml.sound_settings; + return Arrays.asList(sir); + } }; // === Work Sound Settings === void enableWorkSync() { - final WorkSoundPreferenceController workSoundController = - use(WorkSoundPreferenceController.class); - if (workSoundController != null) { - workSoundController.enableWorkSync(); + // TODO(b/174964721): This should be refined when the flag is deprecated. + if (!FeatureFlagUtils.isEnabled(getContext(), FeatureFlags.SILKY_HOME)) { + final WorkSoundPreferenceController workSoundController = + use(WorkSoundPreferenceController.class); + if (workSoundController != null) { + workSoundController.enableWorkSync(); + } } } diff --git a/src/com/android/settings/notification/SoundWorkSettings.java b/src/com/android/settings/notification/SoundWorkSettings.java new file mode 100644 index 00000000000..9fae72d3175 --- /dev/null +++ b/src/com/android/settings/notification/SoundWorkSettings.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.text.TextUtils; +import android.util.FeatureFlagUtils; + +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.RingtonePreference; +import com.android.settings.core.FeatureFlags; +import com.android.settings.core.OnActivityResultListener; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.List; + +/** Sounds settings for work profile. */ +@SearchIndexable +public class SoundWorkSettings extends DashboardFragment implements OnActivityResultListener { + + private static final String TAG = "SoundWorkSettings"; + private static final int REQUEST_CODE = 200; + private static final String SELECTED_PREFERENCE_KEY = "selected_preference"; + + private RingtonePreference mRequestPreference; + + @Override + public int getMetricsCategory() { + return SettingsEnums.WORK_PROFILE_SOUNDS; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState != null) { + String selectedPreference = savedInstanceState.getString( + SELECTED_PREFERENCE_KEY, /* defaultValue= */ null); + if (!TextUtils.isEmpty(selectedPreference)) { + mRequestPreference = findPreference(selectedPreference); + } + } + } + + @Override + public boolean onPreferenceTreeClick(Preference preference) { + if (preference instanceof RingtonePreference) { + writePreferenceClickMetric(preference); + mRequestPreference = (RingtonePreference) preference; + mRequestPreference.onPrepareRingtonePickerIntent(mRequestPreference.getIntent()); + getActivity().startActivityForResultAsUser( + mRequestPreference.getIntent(), + REQUEST_CODE, + /* options= */ null, + UserHandle.of(mRequestPreference.getUserId())); + return true; + } + return super.onPreferenceTreeClick(preference); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (mRequestPreference != null) { + mRequestPreference.onActivityResult(requestCode, resultCode, data); + mRequestPreference = null; + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (mRequestPreference != null) { + outState.putString(SELECTED_PREFERENCE_KEY, mRequestPreference.getKey()); + } + } + + @Override + protected List createPreferenceControllers(Context context) { + return buildPreferenceControllers(context, /* fragment= */ this, getSettingsLifecycle()); + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.sound_work_settings; + } + + private static List buildPreferenceControllers(Context context, + SoundWorkSettings fragment, Lifecycle lifecycle) { + final List controllers = new ArrayList<>(); + controllers.add(new SoundWorkSettingsController(context, fragment, lifecycle)); + return controllers; + } + + static final boolean isSupportWorkProfileSound(Context context) { + // TODO(b/174964721): Feature flag should be removed when silky home launched. + final boolean isSilkyEnabled = FeatureFlagUtils.isEnabled(context, + FeatureFlags.SILKY_HOME); + + final AudioHelper audioHelper = new AudioHelper(context); + final boolean hasWorkProfile = audioHelper.getManagedProfileId( + UserManager.get(context)) != UserHandle.USER_NULL; + final boolean shouldShowRingtoneSettings = !audioHelper.isSingleVolume(); + + return isSilkyEnabled && hasWorkProfile && shouldShowRingtoneSettings; + } + + void enableWorkSync() { + final SoundWorkSettingsController soundWorkSettingsController = + use(SoundWorkSettingsController.class); + if (soundWorkSettingsController != null) { + soundWorkSettingsController.enableWorkSync(); + } + } + + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider(R.xml.sound_work_settings) { + @Override + protected boolean isPageSearchEnabled(Context context) { + return isSupportWorkProfileSound(context); + } + }; +} diff --git a/src/com/android/settings/notification/SoundWorkSettingsController.java b/src/com/android/settings/notification/SoundWorkSettingsController.java new file mode 100644 index 00000000000..d156795b8d3 --- /dev/null +++ b/src/com/android/settings/notification/SoundWorkSettingsController.java @@ -0,0 +1,358 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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; + +import android.annotation.UserIdInt; +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.Ringtone; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; + +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentManager; +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; +import androidx.preference.TwoStatePreference; + +import com.android.settings.DefaultRingtonePreference; +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnPause; +import com.android.settingslib.core.lifecycle.events.OnResume; + +/** Controller that manages the Sounds settings relevant preferences for work profile. */ +public class SoundWorkSettingsController extends AbstractPreferenceController + implements Preference.OnPreferenceChangeListener, LifecycleObserver, OnResume, OnPause { + + private static final String TAG = "SoundWorkSettingsController"; + private static final String KEY_WORK_USE_PERSONAL_SOUNDS = "work_use_personal_sounds"; + private static final String KEY_WORK_PHONE_RINGTONE = "work_ringtone"; + private static final String KEY_WORK_NOTIFICATION_RINGTONE = "work_notification_ringtone"; + private static final String KEY_WORK_ALARM_RINGTONE = "work_alarm_ringtone"; + + private final boolean mVoiceCapable; + private final UserManager mUserManager; + private final SoundWorkSettings mParent; + private final AudioHelper mHelper; + + private TwoStatePreference mWorkUsePersonalSounds; + private Preference mWorkPhoneRingtonePreference; + private Preference mWorkNotificationRingtonePreference; + private Preference mWorkAlarmRingtonePreference; + private PreferenceScreen mScreen; + + @UserIdInt + private int mManagedProfileId; + private final BroadcastReceiver mManagedProfileReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final int userId = ((UserHandle) intent.getExtra(Intent.EXTRA_USER)).getIdentifier(); + switch (intent.getAction()) { + case Intent.ACTION_MANAGED_PROFILE_ADDED: { + onManagedProfileAdded(userId); + return; + } + case Intent.ACTION_MANAGED_PROFILE_REMOVED: { + onManagedProfileRemoved(userId); + return; + } + } + } + }; + + public SoundWorkSettingsController(Context context, SoundWorkSettings parent, + Lifecycle lifecycle) { + this(context, parent, lifecycle, new AudioHelper(context)); + } + + @VisibleForTesting + SoundWorkSettingsController(Context context, SoundWorkSettings parent, Lifecycle lifecycle, + AudioHelper helper) { + super(context); + mUserManager = UserManager.get(context); + mVoiceCapable = Utils.isVoiceCapable(mContext); + mParent = parent; + mHelper = helper; + if (lifecycle != null) { + lifecycle.addObserver(this); + } + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mScreen = screen; + } + + @Override + public void onResume() { + IntentFilter managedProfileFilter = new IntentFilter(); + managedProfileFilter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED); + managedProfileFilter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED); + mContext.registerReceiver(mManagedProfileReceiver, managedProfileFilter); + mManagedProfileId = mHelper.getManagedProfileId(mUserManager); + updateWorkPreferences(); + } + + @Override + public void onPause() { + mContext.unregisterReceiver(mManagedProfileReceiver); + } + + @Override + public boolean isAvailable() { + return mHelper.getManagedProfileId(mUserManager) != UserHandle.USER_NULL + && shouldShowRingtoneSettings(); + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + return false; + } + + @Override + public String getPreferenceKey() { + return null; + } + + /** + * Updates the summary of work preferences + * + * This controller listens to changes on the work ringtone preferences, identified by keys + * "work_ringtone", "work_notification_ringtone" and "work_alarm_ringtone". + */ + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + int ringtoneType; + if (KEY_WORK_PHONE_RINGTONE.equals(preference.getKey())) { + ringtoneType = RingtoneManager.TYPE_RINGTONE; + } else if (KEY_WORK_NOTIFICATION_RINGTONE.equals(preference.getKey())) { + ringtoneType = RingtoneManager.TYPE_NOTIFICATION; + } else if (KEY_WORK_ALARM_RINGTONE.equals(preference.getKey())) { + ringtoneType = RingtoneManager.TYPE_ALARM; + } else { + return true; + } + + preference.setSummary(updateRingtoneName(getManagedProfileContext(), ringtoneType)); + return true; + } + + private boolean shouldShowRingtoneSettings() { + return !mHelper.isSingleVolume(); + } + + private CharSequence updateRingtoneName(Context context, int type) { + if (context == null || !mHelper.isUserUnlocked(mUserManager, context.getUserId())) { + return mContext.getString(R.string.managed_profile_not_available_label); + } + Uri ringtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context, type); + return Ringtone.getTitle(context, ringtoneUri, false /* followSettingsUri */, + /* allowRemote= */ true); + } + + private Context getManagedProfileContext() { + if (mManagedProfileId == UserHandle.USER_NULL) { + return null; + } + return mHelper.createPackageContextAsUser(mManagedProfileId); + } + + private DefaultRingtonePreference initWorkPreference(PreferenceGroup root, String key) { + final DefaultRingtonePreference pref = root.findPreference(key); + pref.setOnPreferenceChangeListener(this); + + // Required so that RingtonePickerActivity lists the work profile ringtones + pref.setUserId(mManagedProfileId); + return pref; + } + + private void updateWorkPreferences() { + if (!isAvailable()) { + return; + } + + if (mWorkUsePersonalSounds == null) { + mWorkUsePersonalSounds = mScreen.findPreference(KEY_WORK_USE_PERSONAL_SOUNDS); + mWorkUsePersonalSounds.setOnPreferenceChangeListener((Preference p, Object value) -> { + if ((boolean) value) { + SoundWorkSettingsController.UnifyWorkDialogFragment.show(mParent); + return false; + } else { + disableWorkSync(); + return true; + } + }); + } + + if (mWorkPhoneRingtonePreference == null) { + mWorkPhoneRingtonePreference = initWorkPreference(mScreen, + KEY_WORK_PHONE_RINGTONE); + } + + if (mWorkNotificationRingtonePreference == null) { + mWorkNotificationRingtonePreference = initWorkPreference(mScreen, + KEY_WORK_NOTIFICATION_RINGTONE); + } + + if (mWorkAlarmRingtonePreference == null) { + mWorkAlarmRingtonePreference = initWorkPreference(mScreen, + KEY_WORK_ALARM_RINGTONE); + } + + if (!mVoiceCapable) { + mWorkPhoneRingtonePreference.setVisible(false); + mWorkPhoneRingtonePreference = null; + } + + final Context managedProfileContext = getManagedProfileContext(); + if (Settings.Secure.getIntForUser(managedProfileContext.getContentResolver(), + Settings.Secure.SYNC_PARENT_SOUNDS, /* def= */ 0, mManagedProfileId) == 1) { + enableWorkSyncSettings(); + } else { + disableWorkSyncSettings(); + } + } + + void enableWorkSync() { + RingtoneManager.enableSyncFromParent(getManagedProfileContext()); + enableWorkSyncSettings(); + } + + private void enableWorkSyncSettings() { + mWorkUsePersonalSounds.setChecked(true); + + if (mWorkPhoneRingtonePreference != null) { + mWorkPhoneRingtonePreference.setSummary(R.string.work_sound_same_as_personal); + } + mWorkNotificationRingtonePreference.setSummary(R.string.work_sound_same_as_personal); + mWorkAlarmRingtonePreference.setSummary(R.string.work_sound_same_as_personal); + } + + private void disableWorkSync() { + RingtoneManager.disableSyncFromParent(getManagedProfileContext()); + disableWorkSyncSettings(); + } + + private void disableWorkSyncSettings() { + if (mWorkPhoneRingtonePreference != null) { + mWorkPhoneRingtonePreference.setEnabled(true); + } + mWorkNotificationRingtonePreference.setEnabled(true); + mWorkAlarmRingtonePreference.setEnabled(true); + + updateWorkRingtoneSummaries(); + } + + private void updateWorkRingtoneSummaries() { + Context managedProfileContext = getManagedProfileContext(); + + if (mWorkPhoneRingtonePreference != null) { + mWorkPhoneRingtonePreference.setSummary( + updateRingtoneName(managedProfileContext, RingtoneManager.TYPE_RINGTONE)); + } + mWorkNotificationRingtonePreference.setSummary( + updateRingtoneName(managedProfileContext, RingtoneManager.TYPE_NOTIFICATION)); + mWorkAlarmRingtonePreference.setSummary( + updateRingtoneName(managedProfileContext, RingtoneManager.TYPE_ALARM)); + } + + /** + * Update work preferences if work profile added. + * @param profileId the profile identifier. + */ + public void onManagedProfileAdded(@UserIdInt int profileId) { + if (mManagedProfileId == UserHandle.USER_NULL) { + mManagedProfileId = profileId; + updateWorkPreferences(); + } + } + + /** + * Update work preferences if work profile removed. + * @param profileId the profile identifier. + */ + public void onManagedProfileRemoved(@UserIdInt int profileId) { + if (mManagedProfileId == profileId) { + mManagedProfileId = mHelper.getManagedProfileId(mUserManager); + updateWorkPreferences(); + } + } + + /** + * Dialog to confirm with the user if it's ok to use the personal profile sounds as the work + * profile sounds. + */ + public static class UnifyWorkDialogFragment extends InstrumentedDialogFragment + implements DialogInterface.OnClickListener { + private static final String TAG = "UnifyWorkDialogFragment"; + private static final int REQUEST_CODE = 200; + + /** + * Show dialog that allows to use the personal profile sounds as the work profile sounds. + * @param parent SoundWorkSettings fragment. + */ + public static void show(SoundWorkSettings parent) { + FragmentManager fm = parent.getFragmentManager(); + if (fm.findFragmentByTag(TAG) == null) { + UnifyWorkDialogFragment fragment = new UnifyWorkDialogFragment(); + fragment.setTargetFragment(parent, REQUEST_CODE); + fragment.show(fm, TAG); + } + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_UNIFY_SOUND_SETTINGS; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return new AlertDialog.Builder(getActivity()) + .setTitle(R.string.work_sync_dialog_title) + .setMessage(R.string.work_sync_dialog_message) + .setPositiveButton(R.string.work_sync_dialog_yes, + SoundWorkSettingsController.UnifyWorkDialogFragment.this) + .setNegativeButton(android.R.string.no, /* listener= */ null) + .create(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + SoundWorkSettings soundWorkSettings = (SoundWorkSettings) getTargetFragment(); + if (soundWorkSettings.isAdded()) { + soundWorkSettings.enableWorkSync(); + } + } + } +} diff --git a/src/com/android/settings/notification/WorkSoundPreferenceController.java b/src/com/android/settings/notification/WorkSoundPreferenceController.java index e23d9ea5b9c..e257feab2ca 100644 --- a/src/com/android/settings/notification/WorkSoundPreferenceController.java +++ b/src/com/android/settings/notification/WorkSoundPreferenceController.java @@ -54,6 +54,9 @@ import com.android.settingslib.core.lifecycle.events.OnResume; import java.util.List; +/** + * TODO(b/183670633): Remove this file when silky flag deprecated. + */ public class WorkSoundPreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin, OnPreferenceChangeListener, LifecycleObserver, OnResume, OnPause { diff --git a/src/com/android/settings/notification/WorkSoundsPreferenceController.java b/src/com/android/settings/notification/WorkSoundsPreferenceController.java new file mode 100644 index 00000000000..f76f8c31cae --- /dev/null +++ b/src/com/android/settings/notification/WorkSoundsPreferenceController.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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; + +import android.content.Context; + +import com.android.settings.core.BasePreferenceController; + +/** This controller manages the work profile sounds preference. */ +public class WorkSoundsPreferenceController extends BasePreferenceController { + + public WorkSoundsPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return SoundWorkSettings.isSupportWorkProfileSound(mContext) ? AVAILABLE + : DISABLED_FOR_USER; + } +} diff --git a/tests/robotests/src/com/android/settings/notification/SoundWorkSettingsControllerTest.java b/tests/robotests/src/com/android/settings/notification/SoundWorkSettingsControllerTest.java new file mode 100644 index 00000000000..ee0a2ec0ba8 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/SoundWorkSettingsControllerTest.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.os.UserHandle; +import android.os.UserManager; +import android.telephony.TelephonyManager; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.TwoStatePreference; + +import com.android.settings.DefaultRingtonePreference; +import com.android.settings.R; +import com.android.settings.RingtonePreference; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class SoundWorkSettingsControllerTest { + + private static final String KEY_WORK_USE_PERSONAL_SOUNDS = "work_use_personal_sounds"; + private static final String KEY_WORK_PHONE_RINGTONE = "work_ringtone"; + private static final String KEY_WORK_NOTIFICATION_RINGTONE = "work_notification_ringtone"; + private static final String KEY_WORK_ALARM_RINGTONE = "work_alarm_ringtone"; + + @Mock + private Context mContext; + @Mock + private PreferenceScreen mScreen; + @Mock + private TelephonyManager mTelephonyManager; + @Mock + private AudioHelper mAudioHelper; + @Mock + private SoundWorkSettings mFragment; + + private SoundWorkSettingsController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephonyManager); + when(mTelephonyManager.isVoiceCapable()).thenReturn(true); + when(mFragment.getPreferenceScreen()).thenReturn(mScreen); + when(mScreen.findPreference(KEY_WORK_USE_PERSONAL_SOUNDS)) + .thenReturn(mock(TwoStatePreference.class)); + when(mScreen.findPreference(KEY_WORK_PHONE_RINGTONE)) + .thenReturn(mock(DefaultRingtonePreference.class)); + when(mScreen.findPreference(KEY_WORK_NOTIFICATION_RINGTONE)) + .thenReturn(mock(DefaultRingtonePreference.class)); + when(mScreen.findPreference(KEY_WORK_ALARM_RINGTONE)) + .thenReturn(mock(DefaultRingtonePreference.class)); + + mController = new SoundWorkSettingsController(mContext, mFragment, null, mAudioHelper); + } + + @Test + public void isAvailable_managedProfileAndNotSingleVolume_shouldReturnTrue() { + when(mAudioHelper.getManagedProfileId(nullable(UserManager.class))) + .thenReturn(UserHandle.myUserId()); + when(mAudioHelper.isSingleVolume()).thenReturn(false); + + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_noManagedProfile_shouldReturnFalse() { + when(mAudioHelper.getManagedProfileId(nullable(UserManager.class))) + .thenReturn(UserHandle.USER_NULL); + when(mAudioHelper.isSingleVolume()).thenReturn(false); + + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void isAvailable_singleVolume_shouldReturnFalse() { + when(mAudioHelper.getManagedProfileId(nullable(UserManager.class))) + .thenReturn(UserHandle.myUserId()); + when(mAudioHelper.isSingleVolume()).thenReturn(true); + + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void onPreferenceChange_shouldUpdateSummary() { + final Preference preference = mock(Preference.class); + when(preference.getKey()).thenReturn(KEY_WORK_PHONE_RINGTONE); + + mController.onPreferenceChange(preference, "hello"); + + verify(preference).setSummary(nullable(String.class)); + } + + @Test + public void onResume_noVoiceCapability_shouldHidePhoneRingtone() { + when(mTelephonyManager.isVoiceCapable()).thenReturn(false); + mController = new SoundWorkSettingsController(mContext, mFragment, null, mAudioHelper); + + when(mAudioHelper.getManagedProfileId(nullable(UserManager.class))) + .thenReturn(UserHandle.myUserId()); + when(mAudioHelper.isUserUnlocked(nullable(UserManager.class), anyInt())).thenReturn(true); + when(mAudioHelper.isSingleVolume()).thenReturn(false); + when(mAudioHelper.createPackageContextAsUser(anyInt())).thenReturn(mContext); + + // Precondition: work profile is available. + assertThat(mController.isAvailable()).isTrue(); + + mController.displayPreference(mScreen); + mController.onResume(); + + verify((Preference) mScreen.findPreference(KEY_WORK_PHONE_RINGTONE)).setVisible(false); + } + + @Test + public void onResume_availableButLocked_shouldRedactPreferences() { + final String notAvailable = "(not available)"; + when(mContext.getString(R.string.managed_profile_not_available_label)) + .thenReturn(notAvailable); + + // Given a device with a managed profile: + when(mAudioHelper.isSingleVolume()).thenReturn(false); + when(mAudioHelper.createPackageContextAsUser(anyInt())).thenReturn(mContext); + when(mAudioHelper.getManagedProfileId(nullable(UserManager.class))) + .thenReturn(UserHandle.myUserId()); + when(mAudioHelper.isUserUnlocked(nullable(UserManager.class), anyInt())).thenReturn(false); + + // When resumed: + mController.displayPreference(mScreen); + mController.onResume(); + + // Sound preferences should explain that the profile isn't available yet. + verify((Preference) mScreen.findPreference(KEY_WORK_PHONE_RINGTONE)) + .setSummary(eq(notAvailable)); + verify((Preference) mScreen.findPreference(KEY_WORK_NOTIFICATION_RINGTONE)) + .setSummary(eq(notAvailable)); + verify((Preference) mScreen.findPreference(KEY_WORK_ALARM_RINGTONE)) + .setSummary(eq(notAvailable)); + } + + @Test + public void onResume_shouldSetUserIdToPreference() { + final int managedProfileUserId = 10; + when(mAudioHelper.getManagedProfileId(nullable(UserManager.class))) + .thenReturn(managedProfileUserId); + when(mAudioHelper.isUserUnlocked(nullable(UserManager.class), anyInt())).thenReturn(true); + when(mAudioHelper.isSingleVolume()).thenReturn(false); + when(mAudioHelper.createPackageContextAsUser(anyInt())).thenReturn(mContext); + + mController.displayPreference(mScreen); + mController.onResume(); + + verify((RingtonePreference) mScreen.findPreference(KEY_WORK_PHONE_RINGTONE)) + .setUserId(managedProfileUserId); + verify((RingtonePreference) mScreen.findPreference(KEY_WORK_NOTIFICATION_RINGTONE)) + .setUserId(managedProfileUserId); + verify((RingtonePreference) mScreen.findPreference(KEY_WORK_ALARM_RINGTONE)) + .setUserId(managedProfileUserId); + } +} diff --git a/tests/robotests/src/com/android/settings/notification/SoundWorkSettingsTest.java b/tests/robotests/src/com/android/settings/notification/SoundWorkSettingsTest.java new file mode 100644 index 00000000000..6f8b784cb3f --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/SoundWorkSettingsTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.UserHandle; + +import androidx.fragment.app.FragmentActivity; +import androidx.preference.Preference; + +import com.android.settings.DefaultRingtonePreference; +import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.util.ReflectionHelpers; + +@RunWith(RobolectricTestRunner.class) +public class SoundWorkSettingsTest { + + @Mock + private FragmentActivity mActivity; + + @Mock + private MetricsFeatureProvider mMetricsFeatureProvider; + + private FakeFeatureFactory mFeatureFactory; + private SoundWorkSettings mFragment; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mFragment = spy(new SoundWorkSettings()); + when(mFragment.getActivity()).thenReturn(mActivity); + mFeatureFactory = FakeFeatureFactory.setupForTest(); + mMetricsFeatureProvider = mFeatureFactory.getMetricsFeatureProvider(); + ReflectionHelpers.setField(mFragment, "mMetricsFeatureProvider", mMetricsFeatureProvider); + } + + @Test + public void onPreferenceTreeClick_isRingtonePreference_shouldStartActivity() { + final DefaultRingtonePreference ringtonePreference = mock(DefaultRingtonePreference.class); + when(mMetricsFeatureProvider.logClickedPreference(any(Preference.class), + anyInt())).thenReturn(true); + + mFragment.onPreferenceTreeClick(ringtonePreference); + + verify(mActivity).startActivityForResultAsUser(any(), anyInt(), any(), + any(UserHandle.class)); + } +} diff --git a/tests/robotests/src/com/android/settings/notification/WorkSoundsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/WorkSoundsPreferenceControllerTest.java new file mode 100644 index 00000000000..2b380311439 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/WorkSoundsPreferenceControllerTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.DISABLED_FOR_USER; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.os.UserHandle; +import android.util.FeatureFlagUtils; + +import com.android.settings.core.FeatureFlags; +import com.android.settings.testutils.shadow.ShadowAudioHelper; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@Config(shadows = ShadowAudioHelper.class) +@RunWith(RobolectricTestRunner.class) +public class WorkSoundsPreferenceControllerTest { + + private Context mContext; + private WorkSoundsPreferenceController mController; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mController = new WorkSoundsPreferenceController(mContext, "test_key"); + } + + @After + public void tearDown() { + ShadowAudioHelper.reset(); + } + + @Test + public void getAvailabilityStatus_supportWorkProfileSound_shouldReturnAvailable() { + FeatureFlagUtils.setEnabled(mContext, FeatureFlags.SILKY_HOME, true); + ShadowAudioHelper.setIsSingleVolume(false); + ShadowAudioHelper.setManagedProfileId(UserHandle.USER_CURRENT); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void getAvailabilityStatus_notSupportWorkProfileSound_shouldReturnDisabled() { + ShadowAudioHelper.setIsSingleVolume(true); + ShadowAudioHelper.setManagedProfileId(UserHandle.USER_NULL); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(DISABLED_FOR_USER); + } +} diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowAudioHelper.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowAudioHelper.java index 4a519aaa0f6..0899aebb3ff 100644 --- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowAudioHelper.java +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowAudioHelper.java @@ -23,17 +23,35 @@ import com.android.settings.notification.AudioHelper; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.annotation.Resetter; @Implements(AudioHelper.class) public class ShadowAudioHelper { + private static boolean sIsSingleVolume = true; + private static int sManagedProfileId = UserHandle.USER_CURRENT; + + @Resetter + public static void reset() { + sIsSingleVolume = true; + sManagedProfileId = UserHandle.USER_CURRENT; + } + + public static void setIsSingleVolume(boolean isSingleVolume) { + sIsSingleVolume = isSingleVolume; + } + + public static void setManagedProfileId(int managedProfileId) { + sManagedProfileId = managedProfileId; + } + @Implementation protected boolean isSingleVolume() { - return true; + return sIsSingleVolume; } @Implementation protected int getManagedProfileId(UserManager um) { - return UserHandle.USER_CURRENT; + return sManagedProfileId; } }