diff --git a/AndroidManifest.xml b/AndroidManifest.xml index b589fd9fdf1..8cfd9b597a9 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2891,17 +2891,20 @@ @@ -2911,16 +2914,19 @@ @@ -2966,24 +2972,29 @@ + + + + + + + + + + + + diff --git a/res/drawable/external_display_mirror_portrait.xml b/res/drawable/external_display_mirror_portrait.xml new file mode 100644 index 00000000000..0fe7f93cdbc --- /dev/null +++ b/res/drawable/external_display_mirror_portrait.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + diff --git a/res/drawable/ic_external_display_32dp.xml b/res/drawable/ic_external_display_32dp.xml new file mode 100644 index 00000000000..3e1828255d2 --- /dev/null +++ b/res/drawable/ic_external_display_32dp.xml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/res/values/dimens.xml b/res/values/dimens.xml index d972e138eec..d34647449df 100755 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -179,6 +179,9 @@ 8dp 1dp 3dp + 8dp + 1.0 + 2.5 40dp diff --git a/res/values/integers.xml b/res/values/integers.xml index f62ccae576e..5427cdd0d35 100644 --- a/res/values/integers.xml +++ b/res/values/integers.xml @@ -36,4 +36,8 @@ 0 0 + + + 0 + 3 diff --git a/res/values/strings.xml b/res/values/strings.xml index 50556de6e0b..a8101e4db6f 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1893,6 +1893,37 @@ Restart + + mirror, external display, connected display, usb display, resolution, rotation + + On + + Off + + External Display + + Use external display + + Display resolution + + External display is disconnected + + Rotation + + Standard + + 90° + + 180° + + 270° + + Changing rotation or resolution may stop any apps that are currently running + + Your device must be connected to an external display to mirror your screen + + More options + Cast @@ -4571,6 +4602,12 @@ Pointer speed + + Pointer scale + + Decrease pointer scale + + Increase pointer scale Game Controller @@ -7268,6 +7305,8 @@ + + @@ -7946,11 +7985,11 @@ %1$s%2$s - - Tap to set up + + Not set - Paused + Disabled Limit interruptions @@ -7967,9 +8006,15 @@ Delete schedules - + Delete + + Delete mode + + + Delete \"%1$s\" mode? + Edit @@ -12655,7 +12700,7 @@ Use print service - Allow multiple users + Allow user switch allow, multiple, user, permit, many diff --git a/res/xml/external_display_resolution_settings.xml b/res/xml/external_display_resolution_settings.xml new file mode 100644 index 00000000000..6ac6b1ad52e --- /dev/null +++ b/res/xml/external_display_resolution_settings.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/res/xml/external_display_settings.xml b/res/xml/external_display_settings.xml new file mode 100644 index 00000000000..00472115e0c --- /dev/null +++ b/res/xml/external_display_settings.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/res/xml/trackpad_settings.xml b/res/xml/trackpad_settings.xml index 1eb16b73156..04422dd5df9 100644 --- a/res/xml/trackpad_settings.xml +++ b/res/xml/trackpad_settings.xml @@ -66,9 +66,19 @@ android:key="pointer_fill_style" android:title="@string/pointer_fill_style" android:order="50" - android:dialogTitle="@string/pointer_fill_style" settings:controller="com.android.settings.inputmethod.PointerFillStylePreferenceController"/> + + displaysToShow, int displayId) { + final Activity activity = getCurrentActivity(); + if (activity == null) { + return; + } + if (displaysToShow.size() == 1 && displaysToShow.get(0).getDisplayId() == displayId) { + var displayName = displaysToShow.get(0).getName(); + if (!displayName.isEmpty()) { + activity.setTitle(displayName.substring(0, Math.min(displayName.length(), 40))); + return; + } + } + activity.setTitle(EXTERANAL_DISPLAY_TITLE_RESOURCE); + } + + private void showTextWhenNoDisplaysToShow(@NonNull final PreferenceScreen screen, + @NonNull Context context) { + if (isUseDisplaySettingEnabled(mInjector)) { + screen.addPreference(updateUseDisplayPreferenceNoDisplaysFound(context)); + } + screen.addPreference(updateFooterPreference(context, + EXTERNAL_DISPLAY_NOT_FOUND_FOOTER_RESOURCE)); + } + + private void showDisplaySettings(@NonNull Display display, @NonNull PreferenceScreen screen, + @NonNull Context context) { + final var isEnabled = mInjector != null && mInjector.isDisplayEnabled(display); + if (isUseDisplaySettingEnabled(mInjector)) { + screen.addPreference(updateUseDisplayPreference(context, display, isEnabled)); + } + if (!isEnabled) { + // Skip all other settings + return; + } + final var displayRotation = getDisplayRotation(display.getDisplayId()); + screen.addPreference(updateIllustrationImage(context, displayRotation)); + screen.addPreference(updateResolutionPreference(context, display)); + screen.addPreference(updateRotationPreference(context, display, displayRotation)); + if (isResolutionSettingEnabled(mInjector)) { + screen.addPreference(updateFooterPreference(context, + EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE)); + } + } + + private void showDisplaysList(@NonNull List displaysToShow, + @NonNull PreferenceScreen screen, @NonNull Context context) { + var pref = getDisplaysListPreference(context); + pref.setKey(DISPLAYS_LIST_PREFERENCE_KEY); + pref.removeAll(); + if (!displaysToShow.isEmpty()) { + screen.addPreference(pref); + } + for (var display : displaysToShow) { + pref.addPreference(new DisplayPreference(context, display)); + } + } + + private List getDisplaysToShow(int displayIdToShow) { + if (mInjector == null) { + return List.of(); + } + if (displayIdToShow != INVALID_DISPLAY) { + var display = mInjector.getDisplay(displayIdToShow); + if (display != null && isDisplayAllowed(display, mInjector)) { + return List.of(display); + } + } + var displaysToShow = new ArrayList(); + for (var display : mInjector.getAllDisplays()) { + if (display != null && isDisplayAllowed(display, mInjector)) { + displaysToShow.add(display); + } + } + return displaysToShow; + } + + private Preference updateUseDisplayPreferenceNoDisplaysFound(@NonNull Context context) { + final var pref = getUseDisplayPreference(context); + pref.setKey(EXTERNAL_DISPLAY_USE_PREFERENCE_KEY); + pref.setTitle(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE); + pref.setChecked(false); + pref.setEnabled(false); + pref.setOnPreferenceChangeListener(null); + return pref; + } + + private Preference updateUseDisplayPreference(@NonNull final Context context, + @NonNull final Display display, boolean isEnabled) { + final var pref = getUseDisplayPreference(context); + pref.setKey(EXTERNAL_DISPLAY_USE_PREFERENCE_KEY); + pref.setTitle(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE); + pref.setChecked(isEnabled); + pref.setEnabled(true); + pref.setOnPreferenceChangeListener((p, newValue) -> { + writePreferenceClickMetric(p); + final boolean result; + if (mInjector == null) { + return false; + } + if ((Boolean) newValue) { + result = mInjector.enableConnectedDisplay(display.getDisplayId()); + } else { + result = mInjector.disableConnectedDisplay(display.getDisplayId()); + } + if (result) { + pref.setChecked((Boolean) newValue); + } + return result; + }); + return pref; + } + + private Preference updateIllustrationImage(@NonNull final Context context, + final int displayRotation) { + var pref = getIllustrationPreference(context); + if (displayRotation % 2 == 0) { + pref.setLottieAnimationResId(EXTERNAL_DISPLAY_PORTRAIT_DRAWABLE); + } else { + pref.setLottieAnimationResId(EXTERNAL_DISPLAY_LANDSCAPE_DRAWABLE); + } + return pref; + } + + private Preference updateFooterPreference(@NonNull final Context context, final int title) { + var pref = getFooterPreference(context); + pref.setTitle(title); + return pref; + } + + private Preference updateRotationPreference(@NonNull final Context context, + @NonNull final Display display, final int displayRotation) { + var pref = getRotationPreference(context); + pref.setKey(EXTERNAL_DISPLAY_ROTATION_KEY); + pref.setTitle(EXTERNAL_DISPLAY_ROTATION_TITLE_RESOURCE); + if (mRotationEntries == null || mRotationEntriesValues == null) { + mRotationEntries = new String[] { + context.getString(R.string.external_display_standard_rotation), + context.getString(R.string.external_display_rotation_90), + context.getString(R.string.external_display_rotation_180), + context.getString(R.string.external_display_rotation_270)}; + mRotationEntriesValues = new String[] {"0", "1", "2", "3"}; + } + pref.setEntries(mRotationEntries); + pref.setEntryValues(mRotationEntriesValues); + pref.setValueIndex(displayRotation); + pref.setSummary(mRotationEntries[displayRotation]); + pref.setOnPreferenceChangeListener((p, newValue) -> { + writePreferenceClickMetric(p); + var rotation = Integer.parseInt((String) newValue); + var displayId = display.getDisplayId(); + if (mInjector == null || !mInjector.freezeDisplayRotation(displayId, rotation)) { + return false; + } + pref.setValueIndex(rotation); + return true; + }); + pref.setEnabled(isRotationSettingEnabled(mInjector)); + return pref; + } + + private Preference updateResolutionPreference(@NonNull final Context context, + @NonNull final Display display) { + var pref = getResolutionPreference(context); + pref.setKey(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY); + pref.setTitle(EXTERNAL_DISPLAY_RESOLUTION_TITLE_RESOURCE); + pref.setSummary(display.getMode().getPhysicalWidth() + " x " + + display.getMode().getPhysicalHeight()); + pref.setOnPreferenceClickListener((Preference p) -> { + writePreferenceClickMetric(p); + launchResolutionSelector(context, display.getDisplayId()); + return true; + }); + pref.setEnabled(isResolutionSettingEnabled(mInjector)); + return pref; + } + + private int getDisplayRotation(int displayId) { + if (mInjector == null) { + return 0; + } + return Math.min(3, Math.max(0, mInjector.getDisplayUserRotation(displayId))); + } + + private void scheduleUpdate() { + if (mInjector == null || !mStarted) { + return; + } + unscheduleUpdate(); + mInjector.getHandler().post(mUpdateRunnable); + } + + private void unscheduleUpdate() { + if (mInjector == null || !mStarted) { + return; + } + mInjector.getHandler().removeCallbacks(mUpdateRunnable); + } + + @VisibleForTesting + class DisplayPreference extends TwoTargetPreference + implements Preference.OnPreferenceClickListener { + private final int mDisplayId; + + DisplayPreference(@NonNull final Context context, @NonNull final Display display) { + super(context); + mDisplayId = display.getDisplayId(); + setPersistent(false); + setKey("display_id_" + mDisplayId); + setTitle(display.getName()); + setSummary(display.getMode().getPhysicalWidth() + " x " + + display.getMode().getPhysicalHeight()); + setOnPreferenceClickListener(this); + } + + @Override + public boolean onPreferenceClick(@NonNull Preference preference) { + launchDisplaySettings(mDisplayId); + writePreferenceClickMetric(preference); + return true; + } + } +} diff --git a/src/com/android/settings/connecteddevice/display/ExternalDisplaySettingsConfiguration.java b/src/com/android/settings/connecteddevice/display/ExternalDisplaySettingsConfiguration.java new file mode 100644 index 00000000000..89d464c9a4e --- /dev/null +++ b/src/com/android/settings/connecteddevice/display/ExternalDisplaySettingsConfiguration.java @@ -0,0 +1,341 @@ +/* + * 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.connecteddevice.display; + +import static android.content.Context.DISPLAY_SERVICE; +import static android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED; +import static android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_ADDED; +import static android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED; +import static android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED; +import static android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_REMOVED; +import static android.view.Display.INVALID_DISPLAY; + +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayManagerGlobal; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.view.Display; +import android.view.Display.Mode; +import android.view.IWindowManager; +import android.view.WindowManagerGlobal; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settings.R; +import com.android.settings.flags.FeatureFlags; +import com.android.settings.flags.FeatureFlagsImpl; + +public class ExternalDisplaySettingsConfiguration { + static final String VIRTUAL_DISPLAY_PACKAGE_NAME_SYSTEM_PROPERTY = + "persist.demo.userrotation.package_name"; + static final String DISPLAY_ID_ARG = "display_id"; + static final int EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE = R.string.external_display_not_found; + static final int EXTERNAL_DISPLAY_HELP_URL = R.string.help_url_external_display; + + public static class SystemServicesProvider { + @Nullable + private IWindowManager mWindowManager; + @Nullable + private DisplayManager mDisplayManager; + @Nullable + protected Context mContext; + /** + * @param name of a system property. + * @return the value of the system property. + */ + @NonNull + public String getSystemProperty(@NonNull String name) { + return SystemProperties.get(name); + } + + /** + * @return return public Display manager. + */ + @Nullable + public DisplayManager getDisplayManager() { + if (mDisplayManager == null && getContext() != null) { + mDisplayManager = (DisplayManager) getContext().getSystemService(DISPLAY_SERVICE); + } + return mDisplayManager; + } + + /** + * @return internal IWindowManager + */ + @Nullable + public IWindowManager getWindowManager() { + if (mWindowManager == null) { + mWindowManager = WindowManagerGlobal.getWindowManagerService(); + } + return mWindowManager; + } + + /** + * @return context. + */ + @Nullable + public Context getContext() { + return mContext; + } + } + + public static class Injector extends SystemServicesProvider { + @NonNull + private final FeatureFlags mFlags; + @NonNull + private final Handler mHandler; + + Injector(@Nullable Context context) { + this(context, new FeatureFlagsImpl(), new Handler(Looper.getMainLooper())); + } + + Injector(@Nullable Context context, @NonNull FeatureFlags flags, @NonNull Handler handler) { + mContext = context; + mFlags = flags; + mHandler = handler; + } + + /** + * @return all displays including disabled. + */ + @NonNull + public Display[] getAllDisplays() { + var dm = getDisplayManager(); + if (dm == null) { + return new Display[0]; + } + return dm.getDisplays(DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED); + } + + /** + * @return enabled displays only. + */ + @NonNull + public Display[] getEnabledDisplays() { + var dm = getDisplayManager(); + if (dm == null) { + return new Display[0]; + } + return dm.getDisplays(); + } + + /** + * @return true if the display is enabled + */ + public boolean isDisplayEnabled(@NonNull Display display) { + for (var enabledDisplay : getEnabledDisplays()) { + if (enabledDisplay.getDisplayId() == display.getDisplayId()) { + return true; + } + } + return false; + } + + /** + * Register display listener. + */ + public void registerDisplayListener(@NonNull DisplayManager.DisplayListener listener) { + var dm = getDisplayManager(); + if (dm == null) { + return; + } + dm.registerDisplayListener(listener, mHandler, EVENT_FLAG_DISPLAY_ADDED + | EVENT_FLAG_DISPLAY_CHANGED | EVENT_FLAG_DISPLAY_REMOVED + | EVENT_FLAG_DISPLAY_CONNECTION_CHANGED); + } + + /** + * Unregister display listener. + */ + public void unregisterDisplayListener(@NonNull DisplayManager.DisplayListener listener) { + var dm = getDisplayManager(); + if (dm == null) { + return; + } + dm.unregisterDisplayListener(listener); + } + + /** + * @return feature flags. + */ + @NonNull + public FeatureFlags getFlags() { + return mFlags; + } + + /** + * Enable connected display. + */ + public boolean enableConnectedDisplay(int displayId) { + var dm = getDisplayManager(); + if (dm == null) { + return false; + } + dm.enableConnectedDisplay(displayId); + return true; + } + + /** + * Disable connected display. + */ + public boolean disableConnectedDisplay(int displayId) { + var dm = getDisplayManager(); + if (dm == null) { + return false; + } + dm.disableConnectedDisplay(displayId); + return true; + } + + /** + * @param displayId which must be returned + * @return display object for the displayId + */ + @Nullable + public Display getDisplay(int displayId) { + if (displayId == INVALID_DISPLAY) { + return null; + } + var dm = getDisplayManager(); + if (dm == null) { + return null; + } + return dm.getDisplay(displayId); + } + + /** + * @return handler + */ + @NonNull + public Handler getHandler() { + return mHandler; + } + + /** + * Get display rotation + * @param displayId display identifier + * @return rotation + */ + public int getDisplayUserRotation(int displayId) { + var wm = getWindowManager(); + if (wm == null) { + return 0; + } + try { + return wm.getDisplayUserRotation(displayId); + } catch (RemoteException e) { + return 0; + } + } + + /** + * Freeze rotation of the display in the specified rotation. + * @param displayId display identifier + * @param rotation [0, 1, 2, 3] + * @return true if successful + */ + public boolean freezeDisplayRotation(int displayId, int rotation) { + var wm = getWindowManager(); + if (wm == null) { + return false; + } + try { + wm.freezeDisplayRotation(displayId, rotation, + "ExternalDisplayPreferenceFragment"); + return true; + } catch (RemoteException e) { + return false; + } + } + + /** + * Enforce display mode on the given display. + */ + public void setUserPreferredDisplayMode(int displayId, @NonNull Mode mode) { + DisplayManagerGlobal.getInstance().setUserPreferredDisplayMode(displayId, mode); + } + } + + public abstract static class DisplayListener implements DisplayManager.DisplayListener { + @Override + public void onDisplayAdded(int displayId) { + update(displayId); + } + + @Override + public void onDisplayRemoved(int displayId) { + update(displayId); + } + + @Override + public void onDisplayChanged(int displayId) { + update(displayId); + } + + @Override + public void onDisplayConnected(int displayId) { + update(displayId); + } + + @Override + public void onDisplayDisconnected(int displayId) { + update(displayId); + } + + /** + * Called from other listener methods to trigger update of the settings page. + */ + public abstract void update(int displayId); + } + + /** + * @return whether the settings page is enabled or not. + */ + public static boolean isExternalDisplaySettingsPageEnabled(@NonNull FeatureFlags flags) { + return flags.rotationConnectedDisplaySetting() + || flags.resolutionAndEnableConnectedDisplaySetting(); + } + + static boolean isDisplayAllowed(@NonNull Display display, + @NonNull SystemServicesProvider props) { + return display.getType() == Display.TYPE_EXTERNAL + || display.getType() == Display.TYPE_OVERLAY + || isVirtualDisplayAllowed(display, props); + } + + static boolean isVirtualDisplayAllowed(@NonNull Display display, + @NonNull SystemServicesProvider properties) { + var sysProp = properties.getSystemProperty(VIRTUAL_DISPLAY_PACKAGE_NAME_SYSTEM_PROPERTY); + return !sysProp.isEmpty() && display.getType() == Display.TYPE_VIRTUAL + && sysProp.equals(display.getOwnerPackageName()); + } + + static boolean isUseDisplaySettingEnabled(@Nullable Injector injector) { + return injector != null && injector.getFlags().resolutionAndEnableConnectedDisplaySetting(); + } + + static boolean isResolutionSettingEnabled(@Nullable Injector injector) { + return injector != null && injector.getFlags().resolutionAndEnableConnectedDisplaySetting(); + } + + static boolean isRotationSettingEnabled(@Nullable Injector injector) { + return injector != null && injector.getFlags().rotationConnectedDisplaySetting(); + } +} diff --git a/src/com/android/settings/connecteddevice/display/ExternalDisplayUpdater.java b/src/com/android/settings/connecteddevice/display/ExternalDisplayUpdater.java new file mode 100644 index 00000000000..64dd7bb2fdf --- /dev/null +++ b/src/com/android/settings/connecteddevice/display/ExternalDisplayUpdater.java @@ -0,0 +1,181 @@ +/* + * 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.connecteddevice.display; + +import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isDisplayAllowed; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.connecteddevice.DevicePreferenceCallback; +import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DisplayListener; +import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.Injector; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; +import com.android.settingslib.RestrictedPreference; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +public class ExternalDisplayUpdater { + + private static final String PREF_KEY = "external_display_settings"; + private final int mMetricsCategory; + @NonNull + private final MetricsFeatureProvider mMetricsFeatureProvider; + @NonNull + private final Runnable mUpdateRunnable = this::update; + @NonNull + private final DevicePreferenceCallback mDevicePreferenceCallback; + @Nullable + private RestrictedPreference mPreference; + @Nullable + private Injector mInjector; + private final DisplayListener mListener = new DisplayListener() { + @Override + public void update(int displayId) { + scheduleUpdate(); + } + }; + + public ExternalDisplayUpdater(@NonNull DevicePreferenceCallback callback, int metricsCategory) { + mDevicePreferenceCallback = callback; + mMetricsCategory = metricsCategory; + mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); + } + + /** + * Set the context to generate the {@link Preference}, so it could get the correct theme. + */ + public void initPreference(@NonNull Context context) { + initPreference(context, new Injector(context)); + } + + @VisibleForTesting + void initPreference(@NonNull Context context, Injector injector) { + mInjector = injector; + mPreference = new RestrictedPreference(context, null /* AttributeSet */); + mPreference.setTitle(R.string.external_display_settings_title); + mPreference.setSummary(getSummary()); + mPreference.setIcon(getDrawable(context)); + mPreference.setKey(PREF_KEY); + mPreference.setDisabledByAdmin(checkIfUsbDataSignalingIsDisabled(context)); + mPreference.setOnPreferenceClickListener((Preference p) -> { + mMetricsFeatureProvider.logClickedPreference(p, mMetricsCategory); + // New version - uses a separate screen. + new SubSettingLauncher(context) + .setDestination(ExternalDisplayPreferenceFragment.class.getName()) + .setTitleRes(R.string.external_display_settings_title) + .setSourceMetricsCategory(mMetricsCategory) + .launch(); + return true; + }); + + scheduleUpdate(); + } + + /** + * Unregister the display listener. + */ + public void unregisterCallback() { + if (mInjector != null) { + mInjector.unregisterDisplayListener(mListener); + } + } + + /** + * Register the display listener. + */ + public void registerCallback() { + if (mInjector != null) { + mInjector.registerDisplayListener(mListener); + } + } + + @VisibleForTesting + @Nullable + protected RestrictedLockUtils.EnforcedAdmin checkIfUsbDataSignalingIsDisabled(Context context) { + return RestrictedLockUtilsInternal.checkIfUsbDataSignalingIsDisabled(context, + UserHandle.myUserId()); + } + + @VisibleForTesting + @Nullable + protected Drawable getDrawable(Context context) { + return context.getDrawable(R.drawable.ic_external_display_32dp); + } + + @Nullable + protected CharSequence getSummary() { + if (mInjector == null) { + return null; + } + var context = mInjector.getContext(); + if (context == null) { + return null; + } + + for (var display : mInjector.getEnabledDisplays()) { + if (display != null && isDisplayAllowed(display, mInjector)) { + return context.getString(R.string.external_display_on); + } + } + + for (var display : mInjector.getAllDisplays()) { + if (display != null && isDisplayAllowed(display, mInjector)) { + return context.getString(R.string.external_display_off); + } + } + + return null; + } + + private void scheduleUpdate() { + if (mInjector == null) { + return; + } + unscheduleUpdate(); + mInjector.getHandler().post(mUpdateRunnable); + } + + private void unscheduleUpdate() { + if (mInjector == null) { + return; + } + mInjector.getHandler().removeCallbacks(mUpdateRunnable); + } + + private void update() { + var summary = getSummary(); + if (mPreference == null) { + return; + } + mPreference.setSummary(summary); + if (summary != null) { + mDevicePreferenceCallback.onDeviceAdded(mPreference); + } else { + mDevicePreferenceCallback.onDeviceRemoved(mPreference); + } + } +} diff --git a/src/com/android/settings/connecteddevice/display/OWNERS b/src/com/android/settings/connecteddevice/display/OWNERS new file mode 100644 index 00000000000..78aecb9dcdf --- /dev/null +++ b/src/com/android/settings/connecteddevice/display/OWNERS @@ -0,0 +1,7 @@ +# Default reviewers for this and subdirectories. +santoscordon@google.com +petsjonkin@google.com +flc@google.com +wilczynskip@google.com +brup@google.com +olb@google.com diff --git a/src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragment.java b/src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragment.java new file mode 100644 index 00000000000..10314cb1e21 --- /dev/null +++ b/src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragment.java @@ -0,0 +1,343 @@ +/* + * 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.connecteddevice.display; + +import static android.view.Display.INVALID_DISPLAY; + +import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DISPLAY_ID_ARG; +import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.EXTERNAL_DISPLAY_HELP_URL; +import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE; +import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isDisplayAllowed; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.res.Resources; +import android.os.Bundle; +import android.util.Log; +import android.util.Pair; +import android.view.Display; +import android.view.Display.Mode; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; + +import com.android.internal.util.ToBooleanFunction; +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragmentBase; +import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DisplayListener; +import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.Injector; +import com.android.settingslib.widget.SelectorWithWidgetPreference; + +import java.util.ArrayList; +import java.util.HashSet; + +public class ResolutionPreferenceFragment extends SettingsPreferenceFragmentBase { + private static final String TAG = "ResolutionPreferenceFragment"; + static final int DEFAULT_LOW_REFRESH_RATE = 60; + static final String MORE_OPTIONS_KEY = "more_options"; + static final String TOP_OPTIONS_KEY = "top_options"; + static final int MORE_OPTIONS_TITLE_RESOURCE = + R.string.external_display_more_options_title; + static final int EXTERNAL_DISPLAY_RESOLUTION_SETTINGS_RESOURCE = + R.xml.external_display_resolution_settings; + @Nullable + private Injector mInjector; + @Nullable + private PreferenceCategory mTopOptionsPreference; + @Nullable + private PreferenceCategory mMoreOptionsPreference; + private boolean mStarted; + private final HashSet mResolutionPreferences = new HashSet<>(); + private int mExternalDisplayPeakWidth; + private int mExternalDisplayPeakHeight; + private int mExternalDisplayPeakRefreshRate; + private boolean mRefreshRateSynchronizationEnabled; + private boolean mMoreOptionsExpanded; + private final Runnable mUpdateRunnable = this::update; + private final DisplayListener mListener = new DisplayListener() { + @Override + public void update(int displayId) { + scheduleUpdate(); + } + }; + + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY; + } + + @Override + public int getHelpResource() { + return EXTERNAL_DISPLAY_HELP_URL; + } + + @Override + public void onCreateCallback(@Nullable Bundle icicle) { + if (mInjector == null) { + mInjector = new Injector(getPrefContext()); + } + addPreferencesFromResource(EXTERNAL_DISPLAY_RESOLUTION_SETTINGS_RESOURCE); + updateDisplayModeLimits(mInjector.getContext()); + } + + @Override + public void onActivityCreatedCallback(@Nullable Bundle savedInstanceState) { + View view = getView(); + TextView emptyView = null; + if (view != null) { + emptyView = (TextView) view.findViewById(android.R.id.empty); + } + if (emptyView != null) { + emptyView.setText(EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE); + setEmptyView(emptyView); + } + } + + @Override + public void onStartCallback() { + mStarted = true; + if (mInjector == null) { + return; + } + mInjector.registerDisplayListener(mListener); + scheduleUpdate(); + } + + @Override + public void onStopCallback() { + mStarted = false; + if (mInjector == null) { + return; + } + mInjector.unregisterDisplayListener(mListener); + unscheduleUpdate(); + } + + public ResolutionPreferenceFragment() {} + + @VisibleForTesting + ResolutionPreferenceFragment(@NonNull Injector injector) { + mInjector = injector; + } + + @VisibleForTesting + protected int getDisplayIdArg() { + var args = getArguments(); + return args != null ? args.getInt(DISPLAY_ID_ARG, INVALID_DISPLAY) : INVALID_DISPLAY; + } + + @VisibleForTesting + @NonNull + protected Resources getResources(@NonNull Context context) { + return context.getResources(); + } + + private void update() { + final PreferenceScreen screen = getPreferenceScreen(); + if (screen == null || mInjector == null) { + return; + } + var context = mInjector.getContext(); + if (context == null) { + return; + } + var display = mInjector.getDisplay(getDisplayIdArg()); + if (display == null || !isDisplayAllowed(display, mInjector)) { + screen.removeAll(); + mTopOptionsPreference = null; + mMoreOptionsPreference = null; + return; + } + mResolutionPreferences.clear(); + var remainingModes = addModePreferences(context, + getTopPreference(context, screen), + display.getSupportedModes(), this::isTopMode, display); + addRemainingPreferences(context, + getMorePreference(context, screen), + display, remainingModes.first, remainingModes.second); + } + + private PreferenceCategory getTopPreference(@NonNull Context context, + @NonNull PreferenceScreen screen) { + if (mTopOptionsPreference == null) { + mTopOptionsPreference = new PreferenceCategory(context); + mTopOptionsPreference.setPersistent(false); + mTopOptionsPreference.setKey(TOP_OPTIONS_KEY); + screen.addPreference(mTopOptionsPreference); + } else { + mTopOptionsPreference.removeAll(); + } + return mTopOptionsPreference; + } + + private PreferenceCategory getMorePreference(@NonNull Context context, + @NonNull PreferenceScreen screen) { + if (mMoreOptionsPreference == null) { + mMoreOptionsPreference = new PreferenceCategory(context); + mMoreOptionsPreference.setPersistent(false); + mMoreOptionsPreference.setTitle(MORE_OPTIONS_TITLE_RESOURCE); + mMoreOptionsPreference.setOnExpandButtonClickListener(() -> { + mMoreOptionsExpanded = true; + }); + mMoreOptionsPreference.setKey(MORE_OPTIONS_KEY); + screen.addPreference(mMoreOptionsPreference); + } else { + mMoreOptionsPreference.removeAll(); + } + return mMoreOptionsPreference; + } + + private void addRemainingPreferences(@NonNull Context context, + @NonNull PreferenceCategory group, @NonNull Display display, + boolean isSelectedModeFound, @NonNull Mode[] moreModes) { + if (moreModes.length == 0) { + return; + } + mMoreOptionsExpanded |= !isSelectedModeFound; + group.setInitialExpandedChildrenCount(mMoreOptionsExpanded ? Integer.MAX_VALUE : 0); + addModePreferences(context, group, moreModes, /*checkMode=*/ null, display); + } + + private Pair addModePreferences(@NonNull Context context, + @NonNull PreferenceGroup group, + @NonNull Mode[] modes, + @Nullable ToBooleanFunction checkMode, + @NonNull Display display) { + Display.Mode curMode = display.getMode(); + var currentResolution = curMode.getPhysicalWidth() + "x" + curMode.getPhysicalHeight(); + var rotatedResolution = curMode.getPhysicalHeight() + "x" + curMode.getPhysicalWidth(); + var skippedModes = new ArrayList(); + var isAnyOfModesSelected = false; + for (var mode : modes) { + var modeStr = mode.getPhysicalWidth() + "x" + mode.getPhysicalHeight(); + SelectorWithWidgetPreference pref = group.findPreference(modeStr); + if (pref != null) { + continue; + } + if (checkMode != null && !checkMode.apply(mode)) { + skippedModes.add(mode); + continue; + } + var isCurrentMode = + currentResolution.equals(modeStr) || rotatedResolution.equals(modeStr); + if (!isCurrentMode && !isAllowedMode(mode)) { + continue; + } + if (mResolutionPreferences.contains(modeStr)) { + // Added to "Top modes" already. + continue; + } + mResolutionPreferences.add(modeStr); + pref = new SelectorWithWidgetPreference(context); + pref.setPersistent(false); + pref.setKey(modeStr); + pref.setTitle(mode.getPhysicalWidth() + " x " + mode.getPhysicalHeight()); + pref.setSingleLineTitle(true); + pref.setOnClickListener(preference -> onDisplayModeClicked(preference, display)); + pref.setChecked(isCurrentMode); + isAnyOfModesSelected |= isCurrentMode; + group.addPreference(pref); + } + return new Pair<>(isAnyOfModesSelected, skippedModes.toArray(Mode.EMPTY_ARRAY)); + } + + private boolean isTopMode(@NonNull Mode mode) { + return mTopOptionsPreference != null + && mTopOptionsPreference.getPreferenceCount() < 3; + } + + private boolean isAllowedMode(@NonNull Mode mode) { + if (mRefreshRateSynchronizationEnabled + && (mode.getRefreshRate() < DEFAULT_LOW_REFRESH_RATE - 1 + || mode.getRefreshRate() > DEFAULT_LOW_REFRESH_RATE + 1)) { + Log.d(TAG, mode + " refresh rate is out of synchronization range"); + return false; + } + if (mExternalDisplayPeakHeight > 0 + && mode.getPhysicalHeight() > mExternalDisplayPeakHeight) { + Log.d(TAG, mode + " height is above the allowed limit"); + return false; + } + if (mExternalDisplayPeakWidth > 0 + && mode.getPhysicalWidth() > mExternalDisplayPeakWidth) { + Log.d(TAG, mode + " width is above the allowed limit"); + return false; + } + if (mExternalDisplayPeakRefreshRate > 0 + && mode.getRefreshRate() > mExternalDisplayPeakRefreshRate) { + Log.d(TAG, mode + " refresh rate is above the allowed limit"); + return false; + } + return true; + } + + private void scheduleUpdate() { + if (mInjector == null || !mStarted) { + return; + } + unscheduleUpdate(); + mInjector.getHandler().post(mUpdateRunnable); + } + + private void unscheduleUpdate() { + if (mInjector == null || !mStarted) { + return; + } + mInjector.getHandler().removeCallbacks(mUpdateRunnable); + } + + private void onDisplayModeClicked(@NonNull SelectorWithWidgetPreference preference, + @NonNull Display display) { + if (mInjector == null) { + return; + } + String[] modeResolution = preference.getKey().split("x"); + int width = Integer.parseInt(modeResolution[0]); + int height = Integer.parseInt(modeResolution[1]); + for (var mode : display.getSupportedModes()) { + if (mode.getPhysicalWidth() == width && mode.getPhysicalHeight() == height + && isAllowedMode(mode)) { + mInjector.setUserPreferredDisplayMode(display.getDisplayId(), mode); + return; + } + } + } + + private void updateDisplayModeLimits(@Nullable Context context) { + if (context == null) { + return; + } + mExternalDisplayPeakRefreshRate = getResources(context).getInteger( + com.android.internal.R.integer.config_externalDisplayPeakRefreshRate); + mExternalDisplayPeakWidth = getResources(context).getInteger( + com.android.internal.R.integer.config_externalDisplayPeakWidth); + mExternalDisplayPeakHeight = getResources(context).getInteger( + com.android.internal.R.integer.config_externalDisplayPeakHeight); + mRefreshRateSynchronizationEnabled = getResources(context).getBoolean( + com.android.internal.R.bool.config_refreshRateSynchronizationEnabled); + Log.d(TAG, "mExternalDisplayPeakRefreshRate=" + mExternalDisplayPeakRefreshRate); + Log.d(TAG, "mExternalDisplayPeakWidth=" + mExternalDisplayPeakWidth); + Log.d(TAG, "mExternalDisplayPeakHeight=" + mExternalDisplayPeakHeight); + Log.d(TAG, "mRefreshRateSynchronizationEnabled=" + mRefreshRateSynchronizationEnabled); + } +} diff --git a/src/com/android/settings/inputmethod/PointerScaleSeekBarController.java b/src/com/android/settings/inputmethod/PointerScaleSeekBarController.java new file mode 100644 index 00000000000..06d52030783 --- /dev/null +++ b/src/com/android/settings/inputmethod/PointerScaleSeekBarController.java @@ -0,0 +1,95 @@ +/* + * Copyright 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.inputmethod; + +import static android.view.PointerIcon.DEFAULT_POINTER_SCALE; + +import android.content.Context; +import android.content.res.Resources; +import android.os.UserHandle; +import android.provider.Settings; +import android.widget.SeekBar; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.widget.LabeledSeekBarPreference; + +public class PointerScaleSeekBarController extends BasePreferenceController { + + private final int mProgressMin; + private final int mProgressMax; + private final float mScaleMin; + private final float mScaleMax; + + public PointerScaleSeekBarController(@NonNull Context context, @NonNull String key) { + super(context, key); + + Resources res = context.getResources(); + mProgressMin = res.getInteger(R.integer.pointer_scale_seek_bar_start); + mProgressMax = res.getInteger(R.integer.pointer_scale_seek_bar_end); + mScaleMin = res.getFloat(R.dimen.pointer_scale_size_start); + mScaleMax = res.getFloat(R.dimen.pointer_scale_size_end); + } + + @AvailabilityStatus + public int getAvailabilityStatus() { + return android.view.flags.Flags.enableVectorCursorA11ySettings() ? AVAILABLE + : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public void displayPreference(@NonNull PreferenceScreen screen) { + super.displayPreference(screen); + + LabeledSeekBarPreference seekBarPreference = screen.findPreference(getPreferenceKey()); + seekBarPreference.setMax(mProgressMax); + seekBarPreference.setContinuousUpdates(/* continuousUpdates= */ true); + seekBarPreference.setProgress(scaleToProgress( + Settings.System.getFloatForUser(mContext.getContentResolver(), + Settings.System.POINTER_SCALE, DEFAULT_POINTER_SCALE, + UserHandle.USER_CURRENT))); + seekBarPreference.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(@NonNull SeekBar seekBar, int progress, + boolean fromUser) { + Settings.System.putFloatForUser(mContext.getContentResolver(), + Settings.System.POINTER_SCALE, progressToScale(progress), + UserHandle.USER_CURRENT); + } + + @Override + public void onStartTrackingTouch(@NonNull SeekBar seekBar) {} + + @Override + public void onStopTrackingTouch(@NonNull SeekBar seekBar) {} + }); + } + + private float progressToScale(int progress) { + return (((progress - mProgressMin) * (mScaleMax - mScaleMin)) / (mProgressMax + - mProgressMin)) + mScaleMin; + } + + private int scaleToProgress(float scale) { + return (int) ( + (((scale - mScaleMin) * (mProgressMax - mProgressMin)) / (mScaleMax - mScaleMin)) + + mProgressMin); + } +} diff --git a/src/com/android/settings/network/MobileNetworkRepository.java b/src/com/android/settings/network/MobileNetworkRepository.java index ce6f8842f9c..ebb341e0fec 100644 --- a/src/com/android/settings/network/MobileNetworkRepository.java +++ b/src/com/android/settings/network/MobileNetworkRepository.java @@ -85,7 +85,6 @@ public class MobileNetworkRepository extends SubscriptionManager.OnSubscriptions private List mActiveSubInfoEntityList = new ArrayList<>(); private Context mContext; private AirplaneModeObserver mAirplaneModeObserver; - private DataRoamingObserver mDataRoamingObserver; private MetricsFeatureProvider mMetricsFeatureProvider; private int mPhysicalSlotIndex = SubscriptionManager.INVALID_SIM_SLOT_INDEX; private int mLogicalSlotIndex = SubscriptionManager.INVALID_SIM_SLOT_INDEX; @@ -122,7 +121,6 @@ public class MobileNetworkRepository extends SubscriptionManager.OnSubscriptions mSubscriptionInfoDao = mMobileNetworkDatabase.mSubscriptionInfoDao(); mMobileNetworkInfoDao = mMobileNetworkDatabase.mMobileNetworkInfoDao(); mAirplaneModeObserver = new AirplaneModeObserver(new Handler(Looper.getMainLooper())); - mDataRoamingObserver = new DataRoamingObserver(new Handler(Looper.getMainLooper())); } private class AirplaneModeObserver extends ContentObserver { @@ -153,47 +151,6 @@ public class MobileNetworkRepository extends SubscriptionManager.OnSubscriptions } } - private class DataRoamingObserver extends ContentObserver { - private int mRegSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; - private String mBaseField = Settings.Global.DATA_ROAMING; - - DataRoamingObserver(Handler handler) { - super(handler); - } - - public void register(Context context, int subId) { - mRegSubId = subId; - String lastField = mBaseField; - createTelephonyManagerBySubId(subId); - TelephonyManager tm = mTelephonyManagerMap.get(subId); - if (tm.getSimCount() != 1) { - lastField += subId; - } - context.getContentResolver().registerContentObserver( - Settings.Global.getUriFor(lastField), false, this); - } - - public void unRegister(Context context) { - context.getContentResolver().unregisterContentObserver(this); - } - - @Override - public void onChange(boolean selfChange, Uri uri) { - TelephonyManager tm = mTelephonyManagerMap.get(mRegSubId); - if (tm == null) { - return; - } - sExecutor.execute(() -> { - Log.d(TAG, "DataRoamingObserver changed"); - insertMobileNetworkInfo(mContext, mRegSubId, tm); - }); - boolean isDataRoamingEnabled = tm.isDataRoamingEnabled(); - for (MobileNetworkCallback callback : sCallbacks) { - callback.onDataRoamingChanged(mRegSubId, isDataRoamingEnabled); - } - } - } - /** * Register all callbacks and listener. * @@ -219,7 +176,6 @@ public class MobileNetworkRepository extends SubscriptionManager.OnSubscriptions observeAllMobileNetworkInfo(lifecycleOwner); if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { createTelephonyManagerBySubId(subId); - mDataRoamingObserver.register(mContext, subId); } // When one client registers callback first time, convey the cached results to the client // so that the client is aware of the content therein. @@ -283,7 +239,6 @@ public class MobileNetworkRepository extends SubscriptionManager.OnSubscriptions if (sCallbacks.isEmpty()) { mSubscriptionManager.removeOnSubscriptionsChangedListener(this); mAirplaneModeObserver.unRegister(mContext); - mDataRoamingObserver.unRegister(mContext); mTelephonyManagerMap.forEach((id, manager) -> { TelephonyCallback callback = mTelephonyCallbackMap.get(id); @@ -588,10 +543,8 @@ public class MobileNetworkRepository extends SubscriptionManager.OnSubscriptions private MobileNetworkInfoEntity convertToMobileNetworkInfoEntity(Context context, int subId, TelephonyManager telephonyManager) { boolean isDataEnabled = false; - boolean isDataRoamingEnabled = false; if (telephonyManager != null) { isDataEnabled = telephonyManager.isDataEnabled(); - isDataRoamingEnabled = telephonyManager.isDataRoamingEnabled(); } else { Log.d(TAG, "TelephonyManager is null, subId = " + subId); } @@ -607,7 +560,7 @@ public class MobileNetworkRepository extends SubscriptionManager.OnSubscriptions MobileNetworkUtils.isTdscdmaSupported(context, subId), MobileNetworkUtils.activeNetworkIsCellular(context), SubscriptionUtil.showToggleForPhysicalSim(mSubscriptionManager), - isDataRoamingEnabled + /* deprecated isDataRoamingEnabled = */ false ); } @@ -754,12 +707,6 @@ public class MobileNetworkRepository extends SubscriptionManager.OnSubscriptions default void onAirplaneModeChanged(boolean enabled) { } - - /** - * Notify clients data roaming changed of subscription. - */ - default void onDataRoamingChanged(int subId, boolean enabled) { - } } public void dump(IndentingPrintWriter printwriter) { diff --git a/src/com/android/settings/network/apn/ApnPreference.java b/src/com/android/settings/network/apn/ApnPreference.java index 879fcb602f1..55258c139c6 100644 --- a/src/com/android/settings/network/apn/ApnPreference.java +++ b/src/com/android/settings/network/apn/ApnPreference.java @@ -85,10 +85,11 @@ public class ApnPreference extends Preference final RelativeLayout textArea = (RelativeLayout) view.findViewById(R.id.text_layout); textArea.setOnClickListener(this); + final View radioButtonFrame = view.itemView.requireViewById(R.id.apn_radio_button_frame); final RadioButton rb = view.itemView.requireViewById(R.id.apn_radiobutton); mRadioButton = rb; if (mDefaultSelectable) { - view.itemView.requireViewById(R.id.apn_radio_button_frame).setOnClickListener((v) -> { + radioButtonFrame.setOnClickListener((v) -> { rb.performClick(); }); rb.setOnCheckedChangeListener(this); @@ -96,9 +97,9 @@ public class ApnPreference extends Preference mProtectFromCheckedChange = true; rb.setChecked(mIsChecked); mProtectFromCheckedChange = false; - rb.setVisibility(View.VISIBLE); + radioButtonFrame.setVisibility(View.VISIBLE); } else { - rb.setVisibility(View.GONE); + radioButtonFrame.setVisibility(View.GONE); } } diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettings.java b/src/com/android/settings/network/telephony/MobileNetworkSettings.java index 34d2fbd5436..d70ef25dd3a 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkSettings.java +++ b/src/com/android/settings/network/telephony/MobileNetworkSettings.java @@ -20,11 +20,14 @@ import static com.android.settings.network.MobileNetworkListFragment.collectAirp import android.app.Activity; import android.app.settings.SettingsEnums; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.os.Bundle; import android.os.UserManager; import android.provider.Settings; +import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; @@ -106,6 +109,15 @@ public class MobileNetworkSettings extends AbstractMobileNetworkSettings impleme private SubscriptionInfoEntity mSubscriptionInfoEntity; private MobileNetworkInfoEntity mMobileNetworkInfoEntity; + private BroadcastReceiver mBrocastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)) { + redrawPreferenceControllers(); + } + } + }; + public MobileNetworkSettings() { super(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS); } @@ -351,6 +363,10 @@ public class MobileNetworkSettings extends AbstractMobileNetworkSettings impleme mMobileNetworkRepository.updateEntity(); // TODO: remove log after fixing b/182326102 Log.d(LOG_TAG, "onResume() subId=" + mSubId); + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED); + getContext().registerReceiver(mBrocastReceiver, intentFilter, Context.RECEIVER_EXPORTED); } private void onSubscriptionDetailChanged() { @@ -370,6 +386,7 @@ public class MobileNetworkSettings extends AbstractMobileNetworkSettings impleme @Override public void onPause() { mMobileNetworkRepository.removeRegister(this); + getContext().unregisterReceiver(mBrocastReceiver); super.onPause(); } diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java index 9637764cee0..5897c4dd680 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeFragment.java @@ -16,10 +16,14 @@ package com.android.settings.notification.modes; +import android.app.AlertDialog; import android.app.Application; import android.app.AutomaticZenRule; import android.app.settings.SettingsEnums; import android.content.Context; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import com.android.settings.R; import com.android.settingslib.applications.ApplicationsState; @@ -31,6 +35,9 @@ import java.util.List; public class ZenModeFragment extends ZenModeFragmentBase { + // for mode deletion menu + private static final int DELETE_MODE = 1; + @Override protected int getPreferenceScreenResId() { return R.xml.modes_rule_settings; @@ -77,4 +84,43 @@ public class ZenModeFragment extends ZenModeFragmentBase { // TODO: b/332937635 - make this the correct metrics category return SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION; } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + menu.add(Menu.NONE, DELETE_MODE, Menu.NONE, R.string.zen_mode_menu_delete_mode); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + protected boolean onOptionsItemSelected(MenuItem item, ZenMode zenMode) { + switch (item.getItemId()) { + case DELETE_MODE: + new AlertDialog.Builder(mContext) + .setTitle(mContext.getString(R.string.zen_mode_delete_mode_confirmation, + zenMode.getRule().getName())) + .setPositiveButton(R.string.zen_mode_schedule_delete, + (dialog, which) -> { + // start finishing before calling removeMode() so that we don't + // try to update this activity with a nonexistent mode when the + // zen mode config is updated + finish(); + mBackend.removeMode(zenMode); + }) + .setNegativeButton(R.string.cancel, null) + .show(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + protected void updateZenModeState() { + // Because this fragment may be asked to finish by the delete menu but not be done doing + // so yet, ignore any attempts to update info in that case. + if (getActivity() != null && getActivity().isFinishing()) { + return; + } + super.updateZenModeState(); + } } diff --git a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java index 00df9fea1bf..d08f7ea0229 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java +++ b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java @@ -22,6 +22,7 @@ import android.app.AutomaticZenRule; import android.content.Context; import android.os.Bundle; import android.util.Log; +import android.view.MenuItem; import android.widget.Toast; import androidx.annotation.NonNull; @@ -114,6 +115,18 @@ abstract class ZenModeFragmentBase extends ZenModesFragmentBase { updateControllers(); } + @Override + public final boolean onOptionsItemSelected(MenuItem item) { + if (mZenMode != null) { + return onOptionsItemSelected(item, mZenMode); + } + return super.onOptionsItemSelected(item); + } + + protected boolean onOptionsItemSelected(MenuItem item, @NonNull ZenMode zenMode) { + return true; + } + private void updateControllers() { if (getPreferenceControllers() == null || mZenMode == null) { return; diff --git a/src/com/android/settings/notification/modes/ZenModesListItemPreference.java b/src/com/android/settings/notification/modes/ZenModesListItemPreference.java index 1bc6e55acce..261ab1d60c5 100644 --- a/src/com/android/settings/notification/modes/ZenModesListItemPreference.java +++ b/src/com/android/settings/notification/modes/ZenModesListItemPreference.java @@ -79,7 +79,7 @@ class ZenModesListItemPreference extends RestrictedPreference { mZenMode.getRule().getTriggerDescription()); case ENABLED -> mZenMode.getRule().getTriggerDescription(); case DISABLED_BY_USER -> mContext.getString(R.string.zen_mode_disabled_by_user); - case DISABLED_BY_OTHER -> mContext.getString(R.string.zen_mode_disabled_tap_to_set_up); + case DISABLED_BY_OTHER -> mContext.getString(R.string.zen_mode_disabled_needs_setup); }; setSummary(statusText); diff --git a/src/com/android/settings/system/FactoryResetPreferenceController.java b/src/com/android/settings/system/FactoryResetPreferenceController.java index df7cc3df3a2..54c97a389b8 100644 --- a/src/com/android/settings/system/FactoryResetPreferenceController.java +++ b/src/com/android/settings/system/FactoryResetPreferenceController.java @@ -16,6 +16,7 @@ package com.android.settings.system; import android.Manifest; +import android.annotation.RequiresPermission; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; @@ -37,8 +38,8 @@ public class FactoryResetPreferenceController extends BasePreferenceController { private static final String TAG = "FactoryResetPreference"; - @VisibleForTesting - static final String ACTION_PREPARE_FACTORY_RESET = + @RequiresPermission(Manifest.permission.PREPARE_FACTORY_RESET) + public static final String ACTION_PREPARE_FACTORY_RESET = "com.android.settings.ACTION_PREPARE_FACTORY_RESET"; private final UserManager mUm; diff --git a/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java b/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java index ce5533e5c64..fe90a2a25b8 100644 --- a/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java +++ b/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java @@ -42,9 +42,18 @@ public class AddUserWhenLockedPreferenceController extends TogglePreferenceContr if (!isAvailable()) { restrictedSwitchPreference.setVisible(false); } else { - restrictedSwitchPreference.setDisabledByAdmin( - mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null); - restrictedSwitchPreference.setVisible(mUserCaps.mUserSwitcherEnabled); + if (android.multiuser.Flags.newMultiuserSettingsUx()) { + restrictedSwitchPreference.setVisible(true); + if (mUserCaps.mDisallowAddUserSetByAdmin) { + restrictedSwitchPreference.setDisabledByAdmin(mUserCaps.mEnforcedAdmin); + } else if (mUserCaps.mDisallowAddUser) { + restrictedSwitchPreference.setVisible(false); + } + } else { + restrictedSwitchPreference.setDisabledByAdmin( + mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null); + restrictedSwitchPreference.setVisible(mUserCaps.mUserSwitcherEnabled); + } } } @@ -52,6 +61,8 @@ public class AddUserWhenLockedPreferenceController extends TogglePreferenceContr public int getAvailabilityStatus() { if (!mUserCaps.isAdmin()) { return DISABLED_FOR_USER; + } else if (android.multiuser.Flags.newMultiuserSettingsUx()) { + return AVAILABLE; } else if (mUserCaps.disallowAddUser() || mUserCaps.disallowAddUserSetByAdmin()) { return DISABLED_FOR_USER; } else { diff --git a/src/com/android/settings/users/GuestTelephonyPreferenceController.java b/src/com/android/settings/users/GuestTelephonyPreferenceController.java index 4fbd4493bb7..e730cbf075f 100644 --- a/src/com/android/settings/users/GuestTelephonyPreferenceController.java +++ b/src/com/android/settings/users/GuestTelephonyPreferenceController.java @@ -19,12 +19,16 @@ package com.android.settings.users; import android.content.Context; import android.content.pm.PackageManager; import android.os.Bundle; +import android.os.UserHandle; import android.os.UserManager; import androidx.preference.Preference; import com.android.settings.R; import com.android.settings.core.TogglePreferenceController; +import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; +import com.android.settingslib.RestrictedSwitchPreference; /** * Controls the preference on the user settings screen which determines whether the guest user @@ -43,10 +47,21 @@ public class GuestTelephonyPreferenceController extends TogglePreferenceControll @Override public int getAvailabilityStatus() { - if (!mUserCaps.isAdmin() || !mUserCaps.mCanAddGuest) { + if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY) + || UserManager.isHeadlessSystemUserMode()) { return DISABLED_FOR_USER; + } + if (android.multiuser.Flags.newMultiuserSettingsUx()) { + if (!mUserCaps.isAdmin()) { + return DISABLED_FOR_USER; + } + return AVAILABLE; } else { - return mUserCaps.mUserSwitcherEnabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + if (!mUserCaps.isAdmin() || !mUserCaps.mCanAddGuest) { + return DISABLED_FOR_USER; + } else { + return mUserCaps.mUserSwitcherEnabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + } } } @@ -74,8 +89,31 @@ public class GuestTelephonyPreferenceController extends TogglePreferenceControll public void updateState(Preference preference) { super.updateState(preference); mUserCaps.updateAddUserCapabilities(mContext); - preference.setVisible(isAvailable() && mUserCaps.mUserSwitcherEnabled - && mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY) - && !UserManager.isHeadlessSystemUserMode()); + final RestrictedSwitchPreference restrictedSwitchPreference = + (RestrictedSwitchPreference) preference; + restrictedSwitchPreference.setChecked(isChecked()); + if (!isAvailable()) { + restrictedSwitchPreference.setVisible(false); + } else { + if (android.multiuser.Flags.newMultiuserSettingsUx()) { + restrictedSwitchPreference.setVisible(true); + final RestrictedLockUtils.EnforcedAdmin disallowRemoveUserAdmin = + RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext, + UserManager.DISALLOW_REMOVE_USER, UserHandle.myUserId()); + if (disallowRemoveUserAdmin != null) { + restrictedSwitchPreference.setDisabledByAdmin(disallowRemoveUserAdmin); + } else if (mUserCaps.mDisallowAddUserSetByAdmin) { + restrictedSwitchPreference.setDisabledByAdmin(mUserCaps.mEnforcedAdmin); + } else if (mUserCaps.mDisallowAddUser) { + // Adding user is restricted by system + restrictedSwitchPreference.setVisible(false); + } + } else { + restrictedSwitchPreference.setDisabledByAdmin( + mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null); + restrictedSwitchPreference.setVisible(mUserCaps.mUserSwitcherEnabled + && isAvailable()); + } + } } } diff --git a/src/com/android/settings/users/MultiUserSwitchBarController.java b/src/com/android/settings/users/MultiUserSwitchBarController.java index f57b7959c37..641ae51c9e3 100644 --- a/src/com/android/settings/users/MultiUserSwitchBarController.java +++ b/src/com/android/settings/users/MultiUserSwitchBarController.java @@ -57,11 +57,6 @@ public class MultiUserSwitchBarController implements SwitchWidgetController.OnSw mSwitchBar.setDisabledByAdmin(RestrictedLockUtilsInternal .checkIfRestrictionEnforced(mContext, UserManager.DISALLOW_USER_SWITCH, UserHandle.myUserId())); - - } else if (mUserCapabilities.mDisallowAddUser) { - mSwitchBar.setDisabledByAdmin(RestrictedLockUtilsInternal - .checkIfRestrictionEnforced(mContext, UserManager.DISALLOW_ADD_USER, - UserHandle.myUserId())); } else { mSwitchBar.setEnabled(mUserCapabilities.mIsMain); } diff --git a/src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java b/src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java index 01df5fddc2e..345b5068788 100644 --- a/src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java +++ b/src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java @@ -22,6 +22,7 @@ import android.content.DialogInterface; import android.content.pm.UserInfo; import android.os.Bundle; import android.os.Handler; +import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.util.Log; @@ -33,6 +34,8 @@ import androidx.preference.Preference; import com.android.settings.R; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedSwitchPreference; /** @@ -70,9 +73,24 @@ public class RemoveGuestOnExitPreferenceController extends BasePreferenceControl if (!isAvailable()) { restrictedSwitchPreference.setVisible(false); } else { - restrictedSwitchPreference.setDisabledByAdmin( - mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null); - restrictedSwitchPreference.setVisible(mUserCaps.mUserSwitcherEnabled); + if (android.multiuser.Flags.newMultiuserSettingsUx()) { + restrictedSwitchPreference.setVisible(true); + final RestrictedLockUtils.EnforcedAdmin disallowRemoveUserAdmin = + RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext, + UserManager.DISALLOW_REMOVE_USER, UserHandle.myUserId()); + if (disallowRemoveUserAdmin != null) { + restrictedSwitchPreference.setDisabledByAdmin(disallowRemoveUserAdmin); + } else if (mUserCaps.mDisallowAddUserSetByAdmin) { + restrictedSwitchPreference.setDisabledByAdmin(mUserCaps.mEnforcedAdmin); + } else if (mUserCaps.mDisallowAddUser) { + // Adding user is restricted by system + restrictedSwitchPreference.setVisible(false); + } + } else { + restrictedSwitchPreference.setDisabledByAdmin( + mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null); + restrictedSwitchPreference.setVisible(mUserCaps.mUserSwitcherEnabled); + } } } @@ -82,14 +100,24 @@ public class RemoveGuestOnExitPreferenceController extends BasePreferenceControl // then disable this controller // also disable this controller for non-admin users // also disable when config_guestUserAllowEphemeralStateChange is false - if (mUserManager.isGuestUserAlwaysEphemeral() - || !UserManager.isGuestUserAllowEphemeralStateChange() - || !mUserCaps.isAdmin() - || mUserCaps.disallowAddUser() - || mUserCaps.disallowAddUserSetByAdmin()) { - return DISABLED_FOR_USER; + if (android.multiuser.Flags.newMultiuserSettingsUx()) { + if (mUserManager.isGuestUserAlwaysEphemeral() + || !UserManager.isGuestUserAllowEphemeralStateChange() + || !mUserCaps.isAdmin()) { + return DISABLED_FOR_USER; + } else { + return AVAILABLE; + } } else { - return mUserCaps.mUserSwitcherEnabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + if (mUserManager.isGuestUserAlwaysEphemeral() + || !UserManager.isGuestUserAllowEphemeralStateChange() + || !mUserCaps.isAdmin() + || mUserCaps.disallowAddUser() + || mUserCaps.disallowAddUserSetByAdmin()) { + return DISABLED_FOR_USER; + } else { + return mUserCaps.mUserSwitcherEnabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + } } } diff --git a/src/com/android/settings/users/UserDetailsSettings.java b/src/com/android/settings/users/UserDetailsSettings.java index 71dd43f92a0..588f01aaa79 100644 --- a/src/com/android/settings/users/UserDetailsSettings.java +++ b/src/com/android/settings/users/UserDetailsSettings.java @@ -126,7 +126,11 @@ public class UserDetailsSettings extends SettingsPreferenceFragment @Override public void onResume() { super.onResume(); - mSwitchUserPref.setEnabled(canSwitchUserNow()); + if (android.multiuser.Flags.newMultiuserSettingsUx()) { + mSwitchUserPref.setEnabled(canSwitchUserNow() && mUserCaps.mUserSwitcherEnabled); + } else { + mSwitchUserPref.setEnabled(canSwitchUserNow()); + } if (mUserInfo.isGuest() && mGuestUserAutoCreated) { mRemoveUserPref.setEnabled((mUserInfo.flags & UserInfo.FLAG_INITIALIZED) != 0); } @@ -358,7 +362,12 @@ public class UserDetailsSettings extends SettingsPreferenceFragment mSwitchUserPref.setDisabledByAdmin(RestrictedLockUtilsInternal.getDeviceOwner(context)); } else { mSwitchUserPref.setDisabledByAdmin(null); - mSwitchUserPref.setSelectable(true); + if (android.multiuser.Flags.newMultiuserSettingsUx()) { + mSwitchUserPref.setEnabled(mUserCaps.mUserSwitcherEnabled); + mSwitchUserPref.setSelectable(mUserCaps.mUserSwitcherEnabled); + } else { + mSwitchUserPref.setSelectable(true); + } mSwitchUserPref.setOnPreferenceClickListener(this); } if (mUserInfo.isMain() || mUserInfo.isGuest() || !UserManager.isMultipleAdminEnabled() diff --git a/src/com/android/settings/users/UserSettings.java b/src/com/android/settings/users/UserSettings.java index bf21c9b913a..0cf01e311e1 100644 --- a/src/com/android/settings/users/UserSettings.java +++ b/src/com/android/settings/users/UserSettings.java @@ -463,7 +463,8 @@ public class UserSettings extends SettingsPreferenceFragment @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { int pos = 0; - if (!isCurrentUserAdmin() && canSwitchUserNow() && !isCurrentUserGuest()) { + if (!isCurrentUserAdmin() && (canSwitchUserNow() || Flags.newMultiuserSettingsUx()) + && !isCurrentUserGuest()) { String nickname = mUserManager.getUserName(); MenuItem removeThisUser = menu.add(0, MENU_REMOVE_USER, pos++, getResources().getString(R.string.user_remove_user_menu, nickname)); @@ -1198,15 +1199,23 @@ public class UserSettings extends SettingsPreferenceFragment } List users; - if (mUserCaps.mUserSwitcherEnabled) { + if (Flags.newMultiuserSettingsUx()) { // Only users that can be switched to should show up here. // e.g. Managed profiles appear under Accounts Settings instead users = mUserManager.getAliveUsers().stream() .filter(UserInfo::supportsSwitchToByUser) .collect(Collectors.toList()); } else { - // Only current user will be displayed in case of multi-user switch is disabled - users = List.of(mUserManager.getUserInfo(context.getUserId())); + if (mUserCaps.mUserSwitcherEnabled) { + // Only users that can be switched to should show up here. + // e.g. Managed profiles appear under Accounts Settings instead + users = mUserManager.getAliveUsers().stream() + .filter(UserInfo::supportsSwitchToByUser) + .collect(Collectors.toList()); + } else { + // Only current user will be displayed in case of multi-user switch is disabled + users = List.of(mUserManager.getUserInfo(context.getUserId())); + } } final ArrayList missingIcons = new ArrayList<>(); @@ -1257,7 +1266,10 @@ public class UserSettings extends SettingsPreferenceFragment pref.setSummary(R.string.user_summary_not_set_up); // Disallow setting up user which results in user switching when the // restriction is set. - pref.setEnabled(!mUserCaps.mDisallowSwitchUser && canSwitchUserNow()); + // If newMultiuserSettingsUx flag is enabled, allow opening user details page + // since switch to user will be disabled + pref.setEnabled((!mUserCaps.mDisallowSwitchUser && canSwitchUserNow()) + || Flags.newMultiuserSettingsUx()); } } else if (user.isRestricted()) { pref.setSummary(R.string.user_summary_restricted_profile); @@ -1417,16 +1429,22 @@ public class UserSettings extends SettingsPreferenceFragment getContext().getResources(), icon))); pref.setKey(KEY_USER_GUEST); pref.setOrder(Preference.DEFAULT_ORDER); - if (mUserCaps.mDisallowSwitchUser) { + if (mUserCaps.mDisallowSwitchUser && !Flags.newMultiuserSettingsUx()) { pref.setDisabledByAdmin( RestrictedLockUtilsInternal.getDeviceOwner(context)); } else { pref.setDisabledByAdmin(null); } - if (mUserCaps.mUserSwitcherEnabled) { + if (Flags.newMultiuserSettingsUx()) { mGuestUserCategory.addPreference(pref); // guest user preference is shown hence also make guest category visible mGuestUserCategory.setVisible(true); + } else { + if (mUserCaps.mUserSwitcherEnabled) { + mGuestUserCategory.addPreference(pref); + // guest user preference is shown hence also make guest category visible + mGuestUserCategory.setVisible(true); + } } isGuestAlreadyCreated = true; } @@ -1450,10 +1468,11 @@ public class UserSettings extends SettingsPreferenceFragment private boolean updateAddGuestPreference(Context context, boolean isGuestAlreadyCreated) { boolean isVisible = false; - if (!isGuestAlreadyCreated && mUserCaps.mCanAddGuest + if (!isGuestAlreadyCreated && (mUserCaps.mCanAddGuest + || (Flags.newMultiuserSettingsUx() && mUserCaps.mDisallowAddUser)) && mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_GUEST) && WizardManagerHelper.isDeviceProvisioned(context) - && mUserCaps.mUserSwitcherEnabled) { + && (mUserCaps.mUserSwitcherEnabled || Flags.newMultiuserSettingsUx())) { Drawable icon = context.getDrawable( com.android.settingslib.R.drawable.ic_account_circle); mAddGuest.setIcon(centerAndTint(icon)); @@ -1466,7 +1485,25 @@ public class UserSettings extends SettingsPreferenceFragment mAddGuest.setEnabled(false); } else { mAddGuest.setTitle(com.android.settingslib.R.string.guest_new_guest); - mAddGuest.setEnabled(canSwitchUserNow()); + if (Flags.newMultiuserSettingsUx() + && mUserCaps.mDisallowAddUserSetByAdmin) { + mAddGuest.setDisabledByAdmin(mUserCaps.mEnforcedAdmin); + } else if (Flags.newMultiuserSettingsUx() && mUserCaps.mDisallowAddUser) { + final List enforcingUsers = + mUserManager.getUserRestrictionSources(UserManager.DISALLOW_ADD_USER, + UserHandle.of(UserHandle.myUserId())); + if (!enforcingUsers.isEmpty()) { + final UserManager.EnforcingUser enforcingUser = enforcingUsers.get(0); + final int restrictionSource = enforcingUser.getUserRestrictionSource(); + if (restrictionSource == UserManager.RESTRICTION_SOURCE_SYSTEM) { + mAddGuest.setEnabled(false); + } else { + mAddGuest.setVisible(false); + } + } + } else { + mAddGuest.setEnabled(canSwitchUserNow() || Flags.newMultiuserSettingsUx()); + } } } else { mAddGuest.setVisible(false); @@ -1494,16 +1531,18 @@ public class UserSettings extends SettingsPreferenceFragment private void updateAddUserCommon(Context context, RestrictedPreference addUser, boolean canAddRestrictedProfile) { - if ((mUserCaps.mCanAddUser && !mUserCaps.mDisallowAddUserSetByAdmin) + if ((mUserCaps.mCanAddUser + && !(mUserCaps.mDisallowAddUserSetByAdmin && Flags.newMultiuserSettingsUx())) && WizardManagerHelper.isDeviceProvisioned(context) - && mUserCaps.mUserSwitcherEnabled) { + && (mUserCaps.mUserSwitcherEnabled || Flags.newMultiuserSettingsUx())) { addUser.setVisible(true); addUser.setSelectable(true); final boolean canAddMoreUsers = mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY) || (canAddRestrictedProfile && mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_RESTRICTED)); - addUser.setEnabled(canAddMoreUsers && !mAddingUser && canSwitchUserNow()); + addUser.setEnabled(canAddMoreUsers && !mAddingUser + && (canSwitchUserNow() || Flags.newMultiuserSettingsUx())); if (!canAddMoreUsers) { addUser.setSummary(getString(R.string.user_add_max_count)); @@ -1514,6 +1553,23 @@ public class UserSettings extends SettingsPreferenceFragment addUser.setDisabledByAdmin( mUserCaps.mDisallowAddUser ? mUserCaps.mEnforcedAdmin : null); } + } else if (Flags.newMultiuserSettingsUx() && mUserCaps.mDisallowAddUserSetByAdmin) { + addUser.setVisible(true); + addUser.setDisabledByAdmin(mUserCaps.mEnforcedAdmin); + } else if (Flags.newMultiuserSettingsUx() && mUserCaps.mDisallowAddUser) { + final List enforcingUsers = + mUserManager.getUserRestrictionSources(UserManager.DISALLOW_ADD_USER, + UserHandle.of(UserHandle.myUserId())); + if (!enforcingUsers.isEmpty()) { + final UserManager.EnforcingUser enforcingUser = enforcingUsers.get(0); + final int restrictionSource = enforcingUser.getUserRestrictionSource(); + if (restrictionSource == UserManager.RESTRICTION_SOURCE_SYSTEM) { + addUser.setVisible(true); + addUser.setEnabled(false); + } else { + addUser.setVisible(false); + } + } } else { addUser.setVisible(false); } diff --git a/tests/Enable16KbTests/Android.bp b/tests/Enable16KbTests/Android.bp index 781ea8f7667..fa05d33954d 100644 --- a/tests/Enable16KbTests/Android.bp +++ b/tests/Enable16KbTests/Android.bp @@ -33,7 +33,6 @@ android_test { ], platform_apis: true, certificate: "platform", - test_suites: ["device-tests"], libs: [ "android.test.runner", "android.test.base", @@ -57,6 +56,6 @@ java_test_host { data: [ ":test_16kb_app", ], - test_suites: ["device-tests"], + test_suites: ["general-tests"], test_config: "AndroidTest.xml", } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java index d28ab3b928b..5a9f2bc8692 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java @@ -17,6 +17,8 @@ package com.android.settings.connecteddevice; import static com.android.settings.core.BasePreferenceController.AVAILABLE_UNSEARCHABLE; import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; +import static com.android.settings.flags.Flags.FLAG_RESOLUTION_AND_ENABLE_CONNECTED_DISPLAY_SETTING; +import static com.android.settings.flags.Flags.FLAG_ROTATION_CONNECTED_DISPLAY_SETTING; import static com.google.common.truth.Truth.assertThat; @@ -30,6 +32,7 @@ import static org.mockito.Mockito.when; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.hardware.input.InputManager; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; @@ -40,13 +43,16 @@ import androidx.preference.Preference; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceManager; import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; import com.android.settings.bluetooth.ConnectedBluetoothDeviceUpdater; import com.android.settings.bluetooth.Utils; +import com.android.settings.connecteddevice.display.ExternalDisplayUpdater; import com.android.settings.connecteddevice.dock.DockUpdater; import com.android.settings.connecteddevice.stylus.StylusDeviceUpdater; import com.android.settings.connecteddevice.usb.ConnectedUsbDeviceUpdater; import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.flags.FakeFeatureFlagsImpl; import com.android.settings.flags.Flags; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; import com.android.settings.testutils.shadow.ShadowBluetoothUtils; @@ -65,7 +71,6 @@ import org.mockito.Answers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; import org.robolectric.Shadows; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowApplicationPackageManager; @@ -84,6 +89,8 @@ public class ConnectedDeviceGroupControllerTest { @Mock private DashboardFragment mDashboardFragment; @Mock + private ExternalDisplayUpdater mExternalDisplayUpdater; + @Mock private ConnectedBluetoothDeviceUpdater mConnectedBluetoothDeviceUpdater; @Mock private ConnectedUsbDeviceUpdater mConnectedUsbDeviceUpdater; @@ -105,6 +112,9 @@ public class ConnectedDeviceGroupControllerTest { private CachedBluetoothDevice mCachedDevice; @Mock private BluetoothDevice mDevice; + @Mock + private Resources mResources; + private final FakeFeatureFlagsImpl mFakeFeatureFlags = new FakeFeatureFlagsImpl(); private ShadowApplicationPackageManager mPackageManager; private PreferenceGroup mPreferenceGroup; @@ -118,8 +128,10 @@ public class ConnectedDeviceGroupControllerTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); + mFakeFeatureFlags.setFlag(FLAG_ROTATION_CONNECTED_DISPLAY_SETTING, true); + mFakeFeatureFlags.setFlag(FLAG_RESOLUTION_AND_ENABLE_CONNECTED_DISPLAY_SETTING, true); - mContext = spy(RuntimeEnvironment.application); + mContext = spy(ApplicationProvider.getApplicationContext()); mPreference = new Preference(mContext); mPreference.setKey(PREFERENCE_KEY_1); mPackageManager = (ShadowApplicationPackageManager) Shadows.shadowOf( @@ -129,15 +141,19 @@ public class ConnectedDeviceGroupControllerTest { doReturn(mContext).when(mDashboardFragment).getContext(); mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, true); when(mContext.getSystemService(InputManager.class)).thenReturn(mInputManager); + when(mContext.getResources()).thenReturn(mResources); when(mInputManager.getInputDeviceIds()).thenReturn(new int[]{}); ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager; mLocalBluetoothManager = Utils.getLocalBtManager(mContext); when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager); - mConnectedDeviceGroupController = new ConnectedDeviceGroupController(mContext); - mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, - mConnectedUsbDeviceUpdater, mConnectedDockUpdater, mStylusDeviceUpdater); + mConnectedDeviceGroupController = spy(new ConnectedDeviceGroupController(mContext)); + when(mConnectedDeviceGroupController.getFeatureFlags()).thenReturn(mFakeFeatureFlags); + + mConnectedDeviceGroupController.init(mExternalDisplayUpdater, + mConnectedBluetoothDeviceUpdater, mConnectedUsbDeviceUpdater, mConnectedDockUpdater, + mStylusDeviceUpdater); mConnectedDeviceGroupController.mPreferenceGroup = mPreferenceGroup; when(mCachedDevice.getName()).thenReturn(DEVICE_NAME); @@ -147,6 +163,7 @@ public class ConnectedDeviceGroupControllerTest { FeatureFlagUtils.setEnabled(mContext, FeatureFlagUtils.SETTINGS_SHOW_STYLUS_PREFERENCES, true); + when(mPreferenceScreen.getContext()).thenReturn(mContext); } @Test @@ -193,6 +210,7 @@ public class ConnectedDeviceGroupControllerTest { // register the callback in onStart() mConnectedDeviceGroupController.onStart(); + verify(mExternalDisplayUpdater).registerCallback(); verify(mConnectedBluetoothDeviceUpdater).registerCallback(); verify(mConnectedUsbDeviceUpdater).registerCallback(); verify(mConnectedDockUpdater).registerCallback(); @@ -204,6 +222,7 @@ public class ConnectedDeviceGroupControllerTest { public void onStop_shouldUnregisterUpdaters() { // unregister the callback in onStop() mConnectedDeviceGroupController.onStop(); + verify(mExternalDisplayUpdater).unregisterCallback(); verify(mConnectedBluetoothDeviceUpdater).unregisterCallback(); verify(mConnectedUsbDeviceUpdater).unregisterCallback(); verify(mConnectedDockUpdater).unregisterCallback(); @@ -212,22 +231,36 @@ public class ConnectedDeviceGroupControllerTest { @Test public void getAvailabilityStatus_noBluetoothUsbDockFeature_returnUnSupported() { + mFakeFeatureFlags.setFlag(FLAG_ROTATION_CONNECTED_DISPLAY_SETTING, false); + mFakeFeatureFlags.setFlag(FLAG_RESOLUTION_AND_ENABLE_CONNECTED_DISPLAY_SETTING, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, false); - mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, + mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater, mConnectedUsbDeviceUpdater, null, null); assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo( UNSUPPORTED_ON_DEVICE); } + @Test + public void getAvailabilityStatus_connectedDisplay_returnSupported() { + mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, false); + mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false); + mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, false); + mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater, + mConnectedUsbDeviceUpdater, null, null); + + assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo( + AVAILABLE_UNSEARCHABLE); + } + @Test public void getAvailabilityStatus_BluetoothFeature_returnSupported() { mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, true); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, false); - mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, + mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater, mConnectedUsbDeviceUpdater, null, null); assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo( @@ -239,7 +272,7 @@ public class ConnectedDeviceGroupControllerTest { mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, true); - mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, + mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater, mConnectedUsbDeviceUpdater, null, null); assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo( @@ -251,7 +284,7 @@ public class ConnectedDeviceGroupControllerTest { mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, false); - mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, + mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater, mConnectedUsbDeviceUpdater, mConnectedDockUpdater, null); assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo( @@ -261,6 +294,8 @@ public class ConnectedDeviceGroupControllerTest { @Test public void getAvailabilityStatus_noUsiStylusFeature_returnUnSupported() { + mFakeFeatureFlags.setFlag(FLAG_ROTATION_CONNECTED_DISPLAY_SETTING, false); + mFakeFeatureFlags.setFlag(FLAG_RESOLUTION_AND_ENABLE_CONNECTED_DISPLAY_SETTING, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, false); @@ -268,7 +303,7 @@ public class ConnectedDeviceGroupControllerTest { when(mInputManager.getInputDevice(0)).thenReturn(new InputDevice.Builder().setSources( InputDevice.SOURCE_DPAD).setExternal(false).build()); - mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, + mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater, mConnectedUsbDeviceUpdater, null, mStylusDeviceUpdater); assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo( @@ -284,7 +319,7 @@ public class ConnectedDeviceGroupControllerTest { when(mInputManager.getInputDevice(0)).thenReturn(new InputDevice.Builder().setSources( InputDevice.SOURCE_STYLUS).setExternal(false).build()); - mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, + mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater, mConnectedUsbDeviceUpdater, mConnectedDockUpdater, mStylusDeviceUpdater); assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo( diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceBadCodeStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceBadCodeStateTest.java index aba300e08bc..391a7b106b6 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceBadCodeStateTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceBadCodeStateTest.java @@ -58,6 +58,7 @@ public class AddSourceBadCodeStateTest { @Test public void testGetInstance() { + mInstance = AddSourceBadCodeState.getInstance(); assertThat(mInstance).isNotNull(); assertThat(mInstance).isInstanceOf(SyncedState.class); } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceFailedStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceFailedStateTest.java index 1bc9f9148f5..917d8dee99c 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceFailedStateTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceFailedStateTest.java @@ -58,6 +58,7 @@ public class AddSourceFailedStateTest { @Test public void testGetInstance() { + mInstance = AddSourceFailedState.getInstance(); assertThat(mInstance).isNotNull(); assertThat(mInstance).isInstanceOf(SyncedState.class); } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseStateTest.java index 950ad38a64a..ce21658f64e 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseStateTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseStateTest.java @@ -80,6 +80,7 @@ public class AddSourceWaitForResponseStateTest { @Test public void testGetInstance() { + mInstance = AddSourceWaitForResponseState.getInstance(); assertThat(mInstance).isNotNull(); assertThat(mInstance).isInstanceOf(AudioStreamStateHandler.class); } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedStateTest.java index 082735a31fc..59a42a1023d 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedStateTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedStateTest.java @@ -85,6 +85,7 @@ public class SourceAddedStateTest { @Test public void testGetInstance() { + mInstance = SourceAddedState.getInstance(); assertThat(mInstance).isNotNull(); assertThat(mInstance).isInstanceOf(SourceAddedState.class); } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncStateTest.java index d97bf8fe58e..813ed2b4bd4 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncStateTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncStateTest.java @@ -76,6 +76,7 @@ public class WaitForSyncStateTest { @Test public void testGetInstance() { + mInstance = WaitForSyncState.getInstance(); assertThat(mInstance).isNotNull(); assertThat(mInstance).isInstanceOf(AudioStreamStateHandler.class); } diff --git a/tests/robotests/src/com/android/settings/inputmethod/PointerScaleSeekBarControllerTest.java b/tests/robotests/src/com/android/settings/inputmethod/PointerScaleSeekBarControllerTest.java new file mode 100644 index 00000000000..152649fb3c5 --- /dev/null +++ b/tests/robotests/src/com/android/settings/inputmethod/PointerScaleSeekBarControllerTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.inputmethod; + +import static android.view.flags.Flags.enableVectorCursorA11ySettings; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.os.UserHandle; +import android.provider.Settings; +import android.widget.SeekBar; + +import androidx.preference.PreferenceScreen; + +import com.android.settings.testutils.shadow.ShadowSystemSettings; +import com.android.settings.widget.LabeledSeekBarPreference; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +/** Tests for {@link PointerScaleSeekBarController} */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = { + ShadowSystemSettings.class, +}) +public class PointerScaleSeekBarControllerTest { + + private static final String PREFERENCE_KEY = "pointer_scale"; + + @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Mock private PreferenceScreen mPreferenceScreen; + + private Context mContext; + private LabeledSeekBarPreference mPreference; + private PointerScaleSeekBarController mController; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mPreference = new LabeledSeekBarPreference(mContext, null); + mController = new PointerScaleSeekBarController(mContext, PREFERENCE_KEY); + } + + @Test + public void getAvailabilityStatus_flagEnabled() { + assumeTrue(enableVectorCursorA11ySettings()); + + assertEquals(mController.getAvailabilityStatus(), AVAILABLE); + } + + @Test + public void onProgressChanged_changeListenerUpdatesSetting() { + when(mPreferenceScreen.findPreference(anyString())).thenReturn(mPreference); + mController.displayPreference(mPreferenceScreen); + SeekBar seekBar = mPreference.getSeekbar(); + int sliderValue = 1; + + mPreference.onProgressChanged(seekBar, sliderValue, false); + + float expectedScale = 1.5f; + float currentScale = Settings.System.getFloatForUser(mContext.getContentResolver(), + Settings.System.POINTER_SCALE, -1, UserHandle.USER_CURRENT); + assertEquals(expectedScale, currentScale, /* delta= */ 0.001f); + } +} diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModesListItemPreferenceTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModesListItemPreferenceTest.java index 495a24c8c34..aaf70595132 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModesListItemPreferenceTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModesListItemPreferenceTest.java @@ -100,7 +100,7 @@ public class ZenModesListItemPreferenceTest { ShadowLooper.idleMainLooper(); assertThat(preference.getTitle()).isEqualTo("Mode disabled by app"); - assertThat(preference.getSummary()).isEqualTo("Tap to set up"); + assertThat(preference.getSummary()).isEqualTo("Not set"); assertThat(preference.getIcon()).isNotNull(); } @@ -120,7 +120,7 @@ public class ZenModesListItemPreferenceTest { ShadowLooper.idleMainLooper(); assertThat(preference.getTitle()).isEqualTo("Mode disabled by user"); - assertThat(preference.getSummary()).isEqualTo("Paused"); + assertThat(preference.getSummary()).isEqualTo("Disabled"); assertThat(preference.getIcon()).isNotNull(); } } diff --git a/tests/robotests/src/com/android/settings/users/UserDetailsSettingsTest.java b/tests/robotests/src/com/android/settings/users/UserDetailsSettingsTest.java index 44e1cc6986c..e035274a5de 100644 --- a/tests/robotests/src/com/android/settings/users/UserDetailsSettingsTest.java +++ b/tests/robotests/src/com/android/settings/users/UserDetailsSettingsTest.java @@ -42,9 +42,13 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.UserInfo; +import android.multiuser.Flags; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.telephony.TelephonyManager; import androidx.fragment.app.FragmentActivity; @@ -63,6 +67,7 @@ import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -123,6 +128,8 @@ public class UserDetailsSettingsTest { private Bundle mArguments; private UserInfo mUserInfo; + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -244,6 +251,19 @@ public class UserDetailsSettingsTest { verify(mSwitchUserPref).setEnabled(false); } + @Test + @RequiresFlagsEnabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX}) + public void onResume_UserSwitcherDisabled_shouldDisableSwitchPref() { + setupSelectedUser(); + mUserCapabilities.mUserSwitcherEnabled = false; + mFragment.mSwitchUserPref = mSwitchUserPref; + mFragment.onAttach(mContext); + + mFragment.onResume(); + + verify(mSwitchUserPref).setEnabled(false); + } + @Test public void onResume_switchDisallowed_shouldDisableSwitchPref() { setupSelectedUser(); diff --git a/tests/robotests/src/com/android/settings/users/UserSettingsTest.java b/tests/robotests/src/com/android/settings/users/UserSettingsTest.java index 5826ca25cc9..85db0bd88b9 100644 --- a/tests/robotests/src/com/android/settings/users/UserSettingsTest.java +++ b/tests/robotests/src/com/android/settings/users/UserSettingsTest.java @@ -34,6 +34,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; import android.app.settings.SettingsEnums; @@ -46,10 +47,15 @@ import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; +import android.multiuser.Flags; import android.os.Bundle; import android.os.Looper; import android.os.UserHandle; import android.os.UserManager; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; import android.text.SpannableStringBuilder; import android.view.Menu; @@ -75,6 +81,7 @@ import com.android.settingslib.search.SearchIndexableRaw; import org.junit.After; import org.junit.Before; import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.AdditionalMatchers; @@ -142,6 +149,9 @@ public class UserSettingsTest { private UserSettings mFragment; private UserCapabilities mUserCapabilities; + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -359,6 +369,7 @@ public class UserSettingsTest { } @Test + @RequiresFlagsDisabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX}) public void updateUserList_cannotSwitchUser_shouldDisableAddUser() { mUserCapabilities.mCanAddUser = true; doReturn(true).when(mUserManager).canAddMoreUsers(anyString()); @@ -374,6 +385,20 @@ public class UserSettingsTest { verify(mAddUserPreference).setSelectable(true); } + @Test + @RequiresFlagsEnabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX}) + public void updateUserList_disallowAddUser_shouldDisableAddUserAndAddGuest() { + mUserCapabilities.mDisallowAddUserSetByAdmin = true; + doReturn(true).when(mUserManager).canAddMoreUsers(anyString()); + doReturn(SWITCHABILITY_STATUS_OK) + .when(mUserManager).getUserSwitchability(); + + mFragment.updateUserList(); + + verify(mAddUserPreference).setVisible(true); + verify(mAddUserPreference).setDisabledByAdmin(any()); + } + @Test public void updateUserList_canNotAddMoreUsers_shouldDisableAddUserWithSummary() { mUserCapabilities.mCanAddUser = true; @@ -392,6 +417,7 @@ public class UserSettingsTest { } @Test + @RequiresFlagsDisabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX}) public void updateUserList_cannotSwitchUser_shouldDisableAddGuest() { mUserCapabilities.mCanAddGuest = true; doReturn(true) @@ -406,6 +432,54 @@ public class UserSettingsTest { } @Test + @RequiresFlagsEnabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX}) + public void updateUserList_cannotSwitchUser_shouldKeepPreferencesVisibleAndEnabled() { + givenUsers(getAdminUser(true)); + mUserCapabilities.mCanAddGuest = true; + mUserCapabilities.mCanAddUser = true; + mUserCapabilities.mDisallowSwitchUser = true; + doReturn(true) + .when(mUserManager).canAddMoreUsers(eq(UserManager.USER_TYPE_FULL_GUEST)); + doReturn(true) + .when(mUserManager).canAddMoreUsers(eq(UserManager.USER_TYPE_FULL_SECONDARY)); + + mFragment.updateUserList(); + + verify(mAddGuestPreference).setVisible(true); + verify(mAddGuestPreference).setEnabled(true); + verify(mAddUserPreference).setVisible(true); + verify(mAddUserPreference).setEnabled(true); + } + + @Test + @RequiresFlagsEnabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX}) + public void updateUserList_disallowAddUser_shouldShowButDisableAddActions() { + givenUsers(getAdminUser(true)); + mUserCapabilities.mCanAddGuest = true; + mUserCapabilities.mCanAddUser = false; + mUserCapabilities.mDisallowAddUser = true; + mUserCapabilities.mDisallowAddUserSetByAdmin = false; + List enforcingUsers = new ArrayList<>(); + enforcingUsers.add(new UserManager.EnforcingUser(UserHandle.myUserId(), + UserManager.RESTRICTION_SOURCE_SYSTEM)); + when(mUserManager.getUserRestrictionSources(UserManager.DISALLOW_ADD_USER, + UserHandle.of(UserHandle.myUserId()))).thenReturn(enforcingUsers); + + doReturn(true) + .when(mUserManager).canAddMoreUsers(eq(UserManager.USER_TYPE_FULL_GUEST)); + doReturn(true) + .when(mUserManager).canAddMoreUsers(eq(UserManager.USER_TYPE_FULL_SECONDARY)); + + mFragment.updateUserList(); + + verify(mAddGuestPreference).setVisible(true); + verify(mAddGuestPreference).setEnabled(false); + verify(mAddUserPreference).setVisible(true); + verify(mAddUserPreference).setEnabled(false); + } + + @Test + @RequiresFlagsDisabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX}) public void updateUserList_addUserDisallowedByAdmin_shouldNotShowAddUser() { RestrictedLockUtils.EnforcedAdmin enforcedAdmin = mock( RestrictedLockUtils.EnforcedAdmin.class); @@ -420,6 +494,22 @@ public class UserSettingsTest { verify(mAddUserPreference).setVisible(false); } + @Test + @RequiresFlagsEnabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX}) + public void updateUserList_addUserDisallowedByAdmin_shouldShowPrefDisabledByAdmin() { + RestrictedLockUtils.EnforcedAdmin enforcedAdmin = mock( + RestrictedLockUtils.EnforcedAdmin.class); + + mUserCapabilities.mEnforcedAdmin = enforcedAdmin; + mUserCapabilities.mCanAddUser = false; + mUserCapabilities.mDisallowAddUser = true; + mUserCapabilities.mDisallowAddUserSetByAdmin = true; + doReturn(true).when(mAddUserPreference).isEnabled(); + + mFragment.updateUserList(); + + verify(mAddUserPreference).setDisabledByAdmin(enforcedAdmin); + } @Test public void updateUserList_cannotAddUserButCanSwitchUser_shouldNotShowAddUser() { mUserCapabilities.mCanAddUser = false; @@ -461,18 +551,31 @@ public class UserSettingsTest { } @Test - public void updateUserList_userSwitcherDisabled_shouldNotShowAddUser() { + public void updateUserList_userSwitcherDisabled_shouldShowAddUser() { givenUsers(getAdminUser(true)); mUserCapabilities.mCanAddUser = true; mUserCapabilities.mUserSwitcherEnabled = false; mFragment.updateUserList(); - verify(mAddUserPreference).setVisible(false); + verify(mAddUserPreference).setVisible(true); } @Test - public void updateUserList_userSwitcherDisabled_shouldNotShowAddGuest() { + public void updateUserList_userSwitcherDisabled_shouldShowAddGuest() { + givenUsers(getAdminUser(true)); + mUserCapabilities.mCanAddGuest = true; + mUserCapabilities.mUserSwitcherEnabled = false; + doReturn(true) + .when(mUserManager).canAddMoreUsers(eq(UserManager.USER_TYPE_FULL_GUEST)); + + mFragment.updateUserList(); + + verify(mAddGuestPreference).setVisible(true); + } + + @Test + public void updateUserList_userSwitcherDisabledCannotAddMoreGuests_shouldNotShowAddGuest() { givenUsers(getAdminUser(true)); mUserCapabilities.mCanAddGuest = true; mUserCapabilities.mUserSwitcherEnabled = false; @@ -533,18 +636,18 @@ public class UserSettingsTest { } @Test - public void updateUserList_existingSecondaryUser_shouldAddOnlyCurrUser_MultiUserIsDisabled() { + public void updateUserList_existingSecondaryUser_shouldAddAllUsers_MultiUserIsDisabled() { givenUsers(getAdminUser(true), getSecondaryUser(false)); mUserCapabilities.mUserSwitcherEnabled = false; mFragment.updateUserList(); ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class); - verify(mFragment.mUserListCategory, times(1)) + verify(mFragment.mUserListCategory, times(2)) .addPreference(captor.capture()); List userPrefs = captor.getAllValues(); - assertThat(userPrefs.size()).isEqualTo(1); + assertThat(userPrefs.size()).isEqualTo(2); assertThat(userPrefs.get(0)).isSameInstanceAs(mMePreference); } @@ -631,6 +734,7 @@ public class UserSettingsTest { } @Test + @RequiresFlagsDisabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX}) public void updateUserList_uninitializedUserAndCanNotSwitchUser_shouldDisablePref() { UserInfo uninitializedUser = getSecondaryUser(false); removeFlag(uninitializedUser, UserInfo.FLAG_INITIALIZED); diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp index bc5824f6cb2..55df480e787 100644 --- a/tests/unit/Android.bp +++ b/tests/unit/Android.bp @@ -33,6 +33,7 @@ android_test { "kotlinx_coroutines_test", "Settings-testutils2", "MediaDrmSettingsFlagsLib", + "servicestests-utils", // Don't add SettingsLib libraries here - you can use them directly as they are in the // instrumented Settings app. ], diff --git a/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragmentTest.java b/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragmentTest.java new file mode 100644 index 00000000000..019ade7ae58 --- /dev/null +++ b/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragmentTest.java @@ -0,0 +1,409 @@ +/* + * 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.connecteddevice.display; + + +import static android.view.Display.INVALID_DISPLAY; + +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.PREVIOUSLY_SHOWN_LIST_KEY; +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.DISPLAYS_LIST_PREFERENCE_KEY; +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE; +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_NOT_FOUND_FOOTER_RESOURCE; +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY; +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_RESOLUTION_TITLE_RESOURCE; +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_ROTATION_KEY; +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_ROTATION_TITLE_RESOURCE; +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_SETTINGS_RESOURCE; +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_USE_PREFERENCE_KEY; +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_USE_TITLE_RESOURCE; +import static com.android.settingslib.widget.FooterPreference.KEY_FOOTER; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.view.Display; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; +import androidx.test.annotation.UiThreadTest; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.DisplayPreference; +import com.android.settingslib.widget.FooterPreference; +import com.android.settingslib.widget.MainSwitchPreference; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +/** Unit tests for {@link ExternalDisplayPreferenceFragment}. */ +@RunWith(AndroidJUnit4.class) +public class ExternalDisplayPreferenceFragmentTest extends ExternalDisplayTestBase { + @Nullable + private ExternalDisplayPreferenceFragment mFragment; + private int mPreferenceIdFromResource; + private int mDisplayIdArg = INVALID_DISPLAY; + private int mResolutionSelectorDisplayId = INVALID_DISPLAY; + @Mock + private MetricsLogger mMockedMetricsLogger; + + @Test + @UiThreadTest + public void testCreateAndStart() { + initFragment(); + assertThat(mPreferenceIdFromResource).isEqualTo(EXTERNAL_DISPLAY_SETTINGS_RESOURCE); + } + + @Test + @UiThreadTest + public void testShowDisplayList() { + var fragment = initFragment(); + var outState = new Bundle(); + fragment.onSaveInstanceStateCallback(outState); + assertThat(outState.getBoolean(PREVIOUSLY_SHOWN_LIST_KEY)).isFalse(); + assertThat(mHandler.getPendingMessages().size()).isEqualTo(1); + PreferenceCategory pref = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY); + assertThat(pref).isNull(); + verify(mMockedInjector, never()).getAllDisplays(); + mHandler.flush(); + assertThat(mHandler.getPendingMessages().size()).isEqualTo(0); + verify(mMockedInjector).getAllDisplays(); + pref = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY); + assertThat(pref).isNotNull(); + assertThat(pref.getPreferenceCount()).isEqualTo(2); + fragment.onSaveInstanceStateCallback(outState); + assertThat(outState.getBoolean(PREVIOUSLY_SHOWN_LIST_KEY)).isTrue(); + } + + @Test + @UiThreadTest + public void testLaunchDisplaySettingFromList() { + initFragment(); + mHandler.flush(); + PreferenceCategory pref = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY); + assertThat(pref).isNotNull(); + DisplayPreference display1Pref = (DisplayPreference) pref.getPreference(0); + DisplayPreference display2Pref = (DisplayPreference) pref.getPreference(1); + assertThat(display1Pref.getKey()).isEqualTo("display_id_" + 1); + assertThat("" + display1Pref.getTitle()).isEqualTo("HDMI"); + assertThat("" + display1Pref.getSummary()).isEqualTo("1920 x 1080"); + display1Pref.onPreferenceClick(display1Pref); + assertThat(mDisplayIdArg).isEqualTo(1); + verify(mMockedMetricsLogger).writePreferenceClickMetric(display1Pref); + assertThat(display2Pref.getKey()).isEqualTo("display_id_" + 2); + assertThat("" + display2Pref.getTitle()).isEqualTo("Overlay #1"); + assertThat("" + display2Pref.getSummary()).isEqualTo("1240 x 780"); + display2Pref.onPreferenceClick(display2Pref); + assertThat(mDisplayIdArg).isEqualTo(2); + verify(mMockedMetricsLogger).writePreferenceClickMetric(display2Pref); + } + + @Test + @UiThreadTest + public void testShowDisplayListForOnlyOneDisplay_PreviouslyShownList() { + var fragment = initFragment(); + // Previously shown list of displays + fragment.onActivityCreatedCallback(createBundleForPreviouslyShownList()); + // Only one display available + doReturn(new Display[] {mDisplays[1]}).when(mMockedInjector).getAllDisplays(); + mHandler.flush(); + PreferenceCategory pref = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY); + assertThat(pref).isNotNull(); + assertThat(pref.getPreferenceCount()).isEqualTo(1); + } + + @Test + @UiThreadTest + public void testShowEnabledDisplay_OnlyOneDisplayAvailable() { + doReturn(true).when(mMockedInjector).isDisplayEnabled(any()); + // Only one display available + doReturn(new Display[] {mDisplays[1]}).when(mMockedInjector).getAllDisplays(); + // Init + initFragment(); + mHandler.flush(); + PreferenceCategory list = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY); + assertThat(list).isNull(); + var pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY); + assertThat(pref).isNotNull(); + pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_ROTATION_KEY); + assertThat(pref).isNotNull(); + var footerPref = (FooterPreference) mPreferenceScreen.findPreference(KEY_FOOTER); + assertThat(footerPref).isNotNull(); + verify(footerPref).setTitle(EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE); + } + + @Test + @UiThreadTest + public void testShowOneEnabledDisplay_FewAvailable() { + mDisplayIdArg = 1; + doReturn(true).when(mMockedInjector).isDisplayEnabled(any()); + initFragment(); + verify(mMockedInjector, never()).getDisplay(anyInt()); + mHandler.flush(); + verify(mMockedInjector).getDisplay(mDisplayIdArg); + var pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY); + assertThat(pref).isNotNull(); + pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_ROTATION_KEY); + assertThat(pref).isNotNull(); + var footerPref = (FooterPreference) mPreferenceScreen.findPreference(KEY_FOOTER); + assertThat(footerPref).isNotNull(); + verify(footerPref).setTitle(EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE); + } + + @Test + @UiThreadTest + public void testShowDisabledDisplay() { + mDisplayIdArg = 1; + initFragment(); + verify(mMockedInjector, never()).getDisplay(anyInt()); + mHandler.flush(); + verify(mMockedInjector).getDisplay(mDisplayIdArg); + var mainPref = (MainSwitchPreference) mPreferenceScreen.findPreference( + EXTERNAL_DISPLAY_USE_PREFERENCE_KEY); + assertThat(mainPref).isNotNull(); + assertThat("" + mainPref.getTitle()).isEqualTo( + getText(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE)); + assertThat(mainPref.isChecked()).isFalse(); + assertThat(mainPref.isEnabled()).isTrue(); + assertThat(mainPref.getOnPreferenceChangeListener()).isNotNull(); + var pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY); + assertThat(pref).isNull(); + pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_ROTATION_KEY); + assertThat(pref).isNull(); + var footerPref = (FooterPreference) mPreferenceScreen.findPreference(KEY_FOOTER); + assertThat(footerPref).isNull(); + } + + @Test + @UiThreadTest + public void testNoDisplays() { + doReturn(new Display[0]).when(mMockedInjector).getAllDisplays(); + initFragment(); + mHandler.flush(); + var mainPref = (MainSwitchPreference) mPreferenceScreen.findPreference( + EXTERNAL_DISPLAY_USE_PREFERENCE_KEY); + assertThat(mainPref).isNotNull(); + assertThat("" + mainPref.getTitle()).isEqualTo( + getText(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE)); + assertThat(mainPref.isChecked()).isFalse(); + assertThat(mainPref.isEnabled()).isFalse(); + assertThat(mainPref.getOnPreferenceChangeListener()).isNull(); + var footerPref = (FooterPreference) mPreferenceScreen.findPreference(KEY_FOOTER); + assertThat(footerPref).isNotNull(); + verify(footerPref).setTitle(EXTERNAL_DISPLAY_NOT_FOUND_FOOTER_RESOURCE); + } + + @Test + @UiThreadTest + public void testDisplayRotationPreference() { + mDisplayIdArg = 1; + doReturn(true).when(mMockedInjector).isDisplayEnabled(any()); + var fragment = initFragment(); + mHandler.flush(); + var pref = fragment.getRotationPreference(mContext); + assertThat(pref.getKey()).isEqualTo(EXTERNAL_DISPLAY_ROTATION_KEY); + assertThat("" + pref.getTitle()).isEqualTo( + getText(EXTERNAL_DISPLAY_ROTATION_TITLE_RESOURCE)); + assertThat(pref.getEntries().length).isEqualTo(4); + assertThat(pref.getEntryValues().length).isEqualTo(4); + assertThat(pref.getEntryValues()[0].toString()).isEqualTo("0"); + assertThat(pref.getEntryValues()[1].toString()).isEqualTo("1"); + assertThat(pref.getEntryValues()[2].toString()).isEqualTo("2"); + assertThat(pref.getEntryValues()[3].toString()).isEqualTo("3"); + assertThat(pref.getEntries()[0].length()).isGreaterThan(0); + assertThat(pref.getEntries()[1].length()).isGreaterThan(0); + assertThat("" + pref.getSummary()).isEqualTo(pref.getEntries()[0].toString()); + assertThat(pref.getValue()).isEqualTo("0"); + assertThat(pref.getOnPreferenceChangeListener()).isNotNull(); + assertThat(pref.isEnabled()).isTrue(); + var rotation = 1; + doReturn(true).when(mMockedInjector).freezeDisplayRotation(mDisplayIdArg, rotation); + assertThat(pref.getOnPreferenceChangeListener().onPreferenceChange(pref, rotation + "")) + .isTrue(); + verify(mMockedInjector).freezeDisplayRotation(mDisplayIdArg, rotation); + assertThat(pref.getValue()).isEqualTo(rotation + ""); + verify(mMockedMetricsLogger).writePreferenceClickMetric(pref); + } + + @Test + @UiThreadTest + public void testDisplayResolutionPreference() { + mDisplayIdArg = 1; + doReturn(true).when(mMockedInjector).isDisplayEnabled(any()); + var fragment = initFragment(); + mHandler.flush(); + var pref = fragment.getResolutionPreference(mContext); + assertThat(pref.getKey()).isEqualTo(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY); + assertThat("" + pref.getTitle()).isEqualTo( + getText(EXTERNAL_DISPLAY_RESOLUTION_TITLE_RESOURCE)); + assertThat("" + pref.getSummary()).isEqualTo("1920 x 1080"); + assertThat(pref.isEnabled()).isTrue(); + assertThat(pref.getOnPreferenceClickListener()).isNotNull(); + assertThat(pref.getOnPreferenceClickListener().onPreferenceClick(pref)).isTrue(); + assertThat(mResolutionSelectorDisplayId).isEqualTo(mDisplayIdArg); + verify(mMockedMetricsLogger).writePreferenceClickMetric(pref); + } + + @Test + @UiThreadTest + public void testUseDisplayPreference_EnabledDisplay() { + mDisplayIdArg = 1; + doReturn(true).when(mMockedInjector).isDisplayEnabled(any()); + doReturn(true).when(mMockedInjector).enableConnectedDisplay(mDisplayIdArg); + doReturn(true).when(mMockedInjector).disableConnectedDisplay(mDisplayIdArg); + var fragment = initFragment(); + mHandler.flush(); + var pref = fragment.getUseDisplayPreference(mContext); + assertThat(pref.getKey()).isEqualTo(EXTERNAL_DISPLAY_USE_PREFERENCE_KEY); + assertThat("" + pref.getTitle()).isEqualTo(getText(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE)); + assertThat(pref.isEnabled()).isTrue(); + assertThat(pref.isChecked()).isTrue(); + assertThat(pref.getOnPreferenceChangeListener()).isNotNull(); + assertThat(pref.getOnPreferenceChangeListener().onPreferenceChange(pref, false)).isTrue(); + verify(mMockedInjector).disableConnectedDisplay(mDisplayIdArg); + assertThat(pref.isChecked()).isFalse(); + assertThat(pref.getOnPreferenceChangeListener().onPreferenceChange(pref, true)).isTrue(); + verify(mMockedInjector).enableConnectedDisplay(mDisplayIdArg); + assertThat(pref.isChecked()).isTrue(); + verify(mMockedMetricsLogger, times(2)).writePreferenceClickMetric(pref); + } + + @NonNull + private ExternalDisplayPreferenceFragment initFragment() { + if (mFragment != null) { + return mFragment; + } + mFragment = new TestableExternalDisplayPreferenceFragment(); + mFragment.onCreateCallback(null); + mFragment.onActivityCreatedCallback(null); + mFragment.onStartCallback(); + return mFragment; + } + + @NonNull + private Bundle createBundleForPreviouslyShownList() { + var state = new Bundle(); + state.putBoolean(PREVIOUSLY_SHOWN_LIST_KEY, true); + return state; + } + + @NonNull + private String getText(int id) { + return mContext.getResources().getText(id).toString(); + } + + private class TestableExternalDisplayPreferenceFragment extends + ExternalDisplayPreferenceFragment { + private final View mMockedRootView; + private final TextView mEmptyView; + private final Activity mMockedActivity; + private final FooterPreference mMockedFooterPreference; + private final MetricsLogger mLogger; + + TestableExternalDisplayPreferenceFragment() { + super(mMockedInjector); + mMockedActivity = mock(Activity.class); + mMockedRootView = mock(View.class); + mMockedFooterPreference = mock(FooterPreference.class); + doReturn(KEY_FOOTER).when(mMockedFooterPreference).getKey(); + mEmptyView = new TextView(mContext); + doReturn(mEmptyView).when(mMockedRootView).findViewById(android.R.id.empty); + mLogger = mMockedMetricsLogger; + } + + @Override + public PreferenceScreen getPreferenceScreen() { + return mPreferenceScreen; + } + + @Override + protected Activity getCurrentActivity() { + return mMockedActivity; + } + + @Override + public View getView() { + return mMockedRootView; + } + + @Override + public void setEmptyView(View view) { + assertThat(view).isEqualTo(mEmptyView); + } + + @Override + public View getEmptyView() { + return mEmptyView; + } + + @Override + public void addPreferencesFromResource(int resource) { + mPreferenceIdFromResource = resource; + } + + @Override + @NonNull + FooterPreference getFooterPreference(@NonNull Context context) { + return mMockedFooterPreference; + } + + @Override + protected int getDisplayIdArg() { + return mDisplayIdArg; + } + + @Override + protected void launchResolutionSelector(@NonNull Context context, int displayId) { + mResolutionSelectorDisplayId = displayId; + } + + @Override + protected void launchDisplaySettings(final int displayId) { + mDisplayIdArg = displayId; + } + + @Override + protected void writePreferenceClickMetric(Preference preference) { + mLogger.writePreferenceClickMetric(preference); + } + } + + /** + * Interface allowing to mock and spy on log events. + */ + public interface MetricsLogger { + + /** + * On preference click metric + */ + void writePreferenceClickMetric(Preference preference); + } +} diff --git a/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayTestBase.java b/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayTestBase.java new file mode 100644 index 00000000000..60b034288a0 --- /dev/null +++ b/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayTestBase.java @@ -0,0 +1,149 @@ +/* + * 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.connecteddevice.display; + +import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.VIRTUAL_DISPLAY_PACKAGE_NAME_SYSTEM_PROPERTY; +import static com.android.settings.flags.Flags.FLAG_ROTATION_CONNECTED_DISPLAY_SETTING; +import static com.android.settings.flags.Flags.FLAG_RESOLUTION_AND_ENABLE_CONNECTED_DISPLAY_SETTING; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.content.Context; +import android.content.res.Resources; +import android.hardware.display.DisplayManagerGlobal; +import android.hardware.display.IDisplayManager; +import android.os.RemoteException; +import android.view.Display; +import android.view.DisplayAdjustments; +import android.view.DisplayInfo; + +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; + +import com.android.server.testutils.TestHandler; +import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DisplayListener; +import com.android.settings.flags.FakeFeatureFlagsImpl; + +import org.junit.Before; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class ExternalDisplayTestBase { + @Mock + ExternalDisplaySettingsConfiguration.Injector mMockedInjector; + @Mock + IDisplayManager mMockedIDisplayManager; + Resources mResources; + DisplayManagerGlobal mDisplayManagerGlobal; + FakeFeatureFlagsImpl mFlags = new FakeFeatureFlagsImpl(); + Context mContext; + DisplayListener mListener; + TestHandler mHandler = new TestHandler(null); + PreferenceManager mPreferenceManager; + PreferenceScreen mPreferenceScreen; + Display[] mDisplays; + + /** + * Setup. + */ + @Before + public void setUp() throws RemoteException { + MockitoAnnotations.initMocks(this); + mContext = spy(ApplicationProvider.getApplicationContext()); + mResources = spy(mContext.getResources()); + doReturn(mResources).when(mContext).getResources(); + mPreferenceManager = new PreferenceManager(mContext); + mPreferenceScreen = mPreferenceManager.createPreferenceScreen(mContext); + doReturn(0).when(mMockedIDisplayManager).getPreferredWideGamutColorSpaceId(); + mDisplayManagerGlobal = new DisplayManagerGlobal(mMockedIDisplayManager); + mFlags.setFlag(FLAG_ROTATION_CONNECTED_DISPLAY_SETTING, true); + mFlags.setFlag(FLAG_RESOLUTION_AND_ENABLE_CONNECTED_DISPLAY_SETTING, true); + mDisplays = new Display[] { + createDefaultDisplay(), createExternalDisplay(), createOverlayDisplay()}; + doReturn(mDisplays).when(mMockedInjector).getAllDisplays(); + doReturn(mDisplays).when(mMockedInjector).getEnabledDisplays(); + for (var display : mDisplays) { + doReturn(display).when(mMockedInjector).getDisplay(display.getDisplayId()); + } + doReturn(mFlags).when(mMockedInjector).getFlags(); + doReturn(mHandler).when(mMockedInjector).getHandler(); + doReturn("").when(mMockedInjector).getSystemProperty( + VIRTUAL_DISPLAY_PACKAGE_NAME_SYSTEM_PROPERTY); + doAnswer((arg) -> { + mListener = arg.getArgument(0); + return null; + }).when(mMockedInjector).registerDisplayListener(any()); + doReturn(0).when(mMockedInjector).getDisplayUserRotation(anyInt()); + doReturn(mContext).when(mMockedInjector).getContext(); + } + + Display createDefaultDisplay() throws RemoteException { + int displayId = 0; + var displayInfo = new DisplayInfo(); + doReturn(displayInfo).when(mMockedIDisplayManager).getDisplayInfo(displayId); + displayInfo.displayId = displayId; + displayInfo.name = "Built-in"; + displayInfo.type = Display.TYPE_INTERNAL; + displayInfo.supportedModes = new Display.Mode[]{ + new Display.Mode(0, 2048, 1024, 60, 60, new float[0], + new int[0])}; + displayInfo.appsSupportedModes = displayInfo.supportedModes; + return createDisplay(displayInfo); + } + + Display createExternalDisplay() throws RemoteException { + int displayId = 1; + var displayInfo = new DisplayInfo(); + doReturn(displayInfo).when(mMockedIDisplayManager).getDisplayInfo(displayId); + displayInfo.displayId = displayId; + displayInfo.name = "HDMI"; + displayInfo.type = Display.TYPE_EXTERNAL; + displayInfo.supportedModes = new Display.Mode[]{ + new Display.Mode(0, 1920, 1080, 60, 60, new float[0], new int[0]), + new Display.Mode(1, 800, 600, 60, 60, new float[0], new int[0]), + new Display.Mode(2, 320, 240, 70, 70, new float[0], new int[0]), + new Display.Mode(3, 640, 480, 60, 60, new float[0], new int[0]), + new Display.Mode(4, 640, 480, 50, 60, new float[0], new int[0]), + new Display.Mode(5, 2048, 1024, 60, 60, new float[0], new int[0]), + new Display.Mode(6, 720, 480, 60, 60, new float[0], new int[0])}; + displayInfo.appsSupportedModes = displayInfo.supportedModes; + return createDisplay(displayInfo); + } + + Display createOverlayDisplay() throws RemoteException { + int displayId = 2; + var displayInfo = new DisplayInfo(); + doReturn(displayInfo).when(mMockedIDisplayManager).getDisplayInfo(displayId); + displayInfo.displayId = displayId; + displayInfo.name = "Overlay #1"; + displayInfo.type = Display.TYPE_OVERLAY; + displayInfo.supportedModes = new Display.Mode[]{ + new Display.Mode(0, 1240, 780, 60, 60, new float[0], + new int[0])}; + displayInfo.appsSupportedModes = displayInfo.supportedModes; + return createDisplay(displayInfo); + } + + Display createDisplay(DisplayInfo displayInfo) { + return new Display(mDisplayManagerGlobal, displayInfo.displayId, displayInfo, + (DisplayAdjustments) null); + } +} diff --git a/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayUpdaterTest.java b/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayUpdaterTest.java new file mode 100644 index 00000000000..824974ad854 --- /dev/null +++ b/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayUpdaterTest.java @@ -0,0 +1,121 @@ +/* + * 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.connecteddevice.display; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.RemoteException; +import android.view.Display; + +import androidx.annotation.Nullable; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.settings.connecteddevice.DevicePreferenceCallback; +import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedPreference; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +/** Unit tests for {@link ExternalDisplayUpdater}. */ +@RunWith(AndroidJUnit4.class) +public class ExternalDisplayUpdaterTest extends ExternalDisplayTestBase { + + private ExternalDisplayUpdater mUpdater; + @Mock + private DevicePreferenceCallback mMockedCallback; + @Mock + private Drawable mMockedDrawable; + private RestrictedPreference mPreferenceAdded; + private RestrictedPreference mPreferenceRemoved; + + @Before + public void setUp() throws RemoteException { + super.setUp(); + mUpdater = new TestableExternalDisplayUpdater(mMockedCallback, /*metricsCategory=*/ 0); + } + + @Test + public void testPreferenceAdded() { + doAnswer((v) -> { + mPreferenceAdded = v.getArgument(0); + return null; + }).when(mMockedCallback).onDeviceAdded(any()); + mUpdater.initPreference(mContext, mMockedInjector); + mUpdater.registerCallback(); + mHandler.flush(); + assertThat(mPreferenceAdded).isNotNull(); + var summary = mPreferenceAdded.getSummary(); + assertThat(summary).isNotNull(); + assertThat(summary.length()).isGreaterThan(0); + var title = mPreferenceAdded.getTitle(); + assertThat(title).isNotNull(); + assertThat(title.length()).isGreaterThan(0); + } + + @Test + public void testPreferenceRemoved() { + doAnswer((v) -> { + mPreferenceAdded = v.getArgument(0); + return null; + }).when(mMockedCallback).onDeviceAdded(any()); + doAnswer((v) -> { + mPreferenceRemoved = v.getArgument(0); + return null; + }).when(mMockedCallback).onDeviceRemoved(any()); + mUpdater.initPreference(mContext, mMockedInjector); + mUpdater.registerCallback(); + mHandler.flush(); + assertThat(mPreferenceAdded).isNotNull(); + assertThat(mPreferenceRemoved).isNull(); + // Remove display + doReturn(new Display[0]).when(mMockedInjector).getAllDisplays(); + doReturn(new Display[0]).when(mMockedInjector).getEnabledDisplays(); + mListener.onDisplayRemoved(1); + mHandler.flush(); + assertThat(mPreferenceRemoved).isEqualTo(mPreferenceAdded); + } + + class TestableExternalDisplayUpdater extends ExternalDisplayUpdater { + TestableExternalDisplayUpdater( + DevicePreferenceCallback callback, + int metricsCategory) { + super(callback, metricsCategory); + } + + @Override + @Nullable + protected RestrictedLockUtils.EnforcedAdmin checkIfUsbDataSignalingIsDisabled( + Context context) { + // if null is returned - usb signalling is enabled + return null; + } + + @Override + @Nullable + protected Drawable getDrawable(Context context) { + return mMockedDrawable; + } + } +} diff --git a/tests/unit/src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragmentTest.java b/tests/unit/src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragmentTest.java new file mode 100644 index 00000000000..ee38a1cbae2 --- /dev/null +++ b/tests/unit/src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragmentTest.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.connecteddevice.display; + +import static android.view.Display.INVALID_DISPLAY; + +import static com.android.settings.connecteddevice.display.ResolutionPreferenceFragment.EXTERNAL_DISPLAY_RESOLUTION_SETTINGS_RESOURCE; +import static com.android.settings.connecteddevice.display.ResolutionPreferenceFragment.MORE_OPTIONS_KEY; +import static com.android.settings.connecteddevice.display.ResolutionPreferenceFragment.TOP_OPTIONS_KEY; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.content.res.Resources; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; +import androidx.test.annotation.UiThreadTest; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.settingslib.widget.SelectorWithWidgetPreference; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +/** Unit tests for {@link ResolutionPreferenceFragment}. */ +@RunWith(AndroidJUnit4.class) +public class ResolutionPreferenceFragmentTest extends ExternalDisplayTestBase { + @Nullable + private ResolutionPreferenceFragment mFragment; + private int mPreferenceIdFromResource; + private int mDisplayIdArg = INVALID_DISPLAY; + @Mock + private MetricsLogger mMockedMetricsLogger; + + @Test + @UiThreadTest + public void testCreateAndStart() { + initFragment(); + mHandler.flush(); + assertThat(mPreferenceIdFromResource).isEqualTo( + EXTERNAL_DISPLAY_RESOLUTION_SETTINGS_RESOURCE); + var pref = mPreferenceScreen.findPreference(TOP_OPTIONS_KEY); + assertThat(pref).isNull(); + pref = mPreferenceScreen.findPreference(MORE_OPTIONS_KEY); + assertThat(pref).isNull(); + } + + @Test + @UiThreadTest + public void testCreateAndStartDefaultDisplayNotAllowed() { + mDisplayIdArg = 0; + initFragment(); + mHandler.flush(); + var pref = mPreferenceScreen.findPreference(TOP_OPTIONS_KEY); + assertThat(pref).isNull(); + pref = mPreferenceScreen.findPreference(MORE_OPTIONS_KEY); + assertThat(pref).isNull(); + } + + @Test + @UiThreadTest + public void testModePreferences() { + mDisplayIdArg = 1; + initFragment(); + mHandler.flush(); + PreferenceCategory topPref = mPreferenceScreen.findPreference(TOP_OPTIONS_KEY); + assertThat(topPref).isNotNull(); + PreferenceCategory morePref = mPreferenceScreen.findPreference(MORE_OPTIONS_KEY); + assertThat(morePref).isNotNull(); + assertThat(topPref.getPreferenceCount()).isEqualTo(3); + assertThat(morePref.getPreferenceCount()).isEqualTo(1); + } + + @Test + @UiThreadTest + public void testModeChange() { + mDisplayIdArg = 1; + initFragment(); + mHandler.flush(); + PreferenceCategory topPref = mPreferenceScreen.findPreference(TOP_OPTIONS_KEY); + assertThat(topPref).isNotNull(); + var modePref = (SelectorWithWidgetPreference) topPref.getPreference(1); + modePref.onClick(); + var mode = mDisplays[mDisplayIdArg].getSupportedModes()[1]; + verify(mMockedInjector).setUserPreferredDisplayMode(mDisplayIdArg, mode); + } + + private void initFragment() { + if (mFragment != null) { + return; + } + mFragment = new TestableResolutionPreferenceFragment(); + mFragment.onCreateCallback(null); + mFragment.onActivityCreatedCallback(null); + mFragment.onStartCallback(); + } + + private class TestableResolutionPreferenceFragment extends ResolutionPreferenceFragment { + private final View mMockedRootView; + private final TextView mEmptyView; + private final Resources mMockedResources; + private final MetricsLogger mLogger; + TestableResolutionPreferenceFragment() { + super(mMockedInjector); + mMockedResources = mock(Resources.class); + doReturn(61).when(mMockedResources).getInteger( + com.android.internal.R.integer.config_externalDisplayPeakRefreshRate); + doReturn(1920).when(mMockedResources).getInteger( + com.android.internal.R.integer.config_externalDisplayPeakWidth); + doReturn(1080).when(mMockedResources).getInteger( + com.android.internal.R.integer.config_externalDisplayPeakHeight); + doReturn(true).when(mMockedResources).getBoolean( + com.android.internal.R.bool.config_refreshRateSynchronizationEnabled); + mMockedRootView = mock(View.class); + mEmptyView = new TextView(mContext); + doReturn(mEmptyView).when(mMockedRootView).findViewById(android.R.id.empty); + mLogger = mMockedMetricsLogger; + } + + @Override + public PreferenceScreen getPreferenceScreen() { + return mPreferenceScreen; + } + + @Override + public View getView() { + return mMockedRootView; + } + + @Override + public void setEmptyView(View view) { + assertThat(view).isEqualTo(mEmptyView); + } + + @Override + public View getEmptyView() { + return mEmptyView; + } + + @Override + public void addPreferencesFromResource(int resource) { + mPreferenceIdFromResource = resource; + } + + @Override + protected int getDisplayIdArg() { + return mDisplayIdArg; + } + + @Override + protected void writePreferenceClickMetric(Preference preference) { + mLogger.writePreferenceClickMetric(preference); + } + + @Override + @NonNull + protected Resources getResources(@NonNull Context context) { + return mMockedResources; + } + } + + /** + * Interface allowing to mock and spy on log events. + */ + public interface MetricsLogger { + /** + * On preference click metric + */ + void writePreferenceClickMetric(Preference preference); + } +}