/* * 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.display; import android.app.settings.SettingsEnums; import android.content.Context; import android.content.res.Resources; import android.graphics.Point; import android.graphics.drawable.Drawable; import android.hardware.display.DisplayManager; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; import android.view.Display; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.core.instrumentation.SettingsStatsLog; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.widget.RadioButtonPickerFragment; import com.android.settingslib.display.DisplayDensityUtils; import com.android.settingslib.search.SearchIndexable; import com.android.settingslib.widget.CandidateInfo; import com.android.settingslib.widget.FooterPreference; import com.android.settingslib.widget.IllustrationPreference; import com.android.settingslib.widget.SelectorWithWidgetPreference; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; /** Preference fragment used for switch screen resolution */ @SearchIndexable public class ScreenResolutionFragment extends RadioButtonPickerFragment { private static final String TAG = "ScreenResolution"; private Resources mResources; private static final String SCREEN_RESOLUTION = "user_selected_resolution"; private static final String SCREEN_RESOLUTION_KEY = "screen_resolution"; private Display mDefaultDisplay; private String[] mScreenResolutionOptions; private Set mResolutions; private String[] mScreenResolutionSummaries; private IllustrationPreference mImagePreference; private DisplayObserver mDisplayObserver; private AccessibilityManager mAccessibilityManager; private int mHighWidth; private int mFullWidth; @Override public void onAttach(Context context) { super.onAttach(context); mDefaultDisplay = context.getSystemService(DisplayManager.class).getDisplay(Display.DEFAULT_DISPLAY); mAccessibilityManager = context.getSystemService(AccessibilityManager.class); mResources = context.getResources(); mScreenResolutionOptions = mResources.getStringArray(R.array.config_screen_resolution_options_strings); mImagePreference = new IllustrationPreference(context); mDisplayObserver = new DisplayObserver(context); ScreenResolutionController controller = new ScreenResolutionController(context, SCREEN_RESOLUTION_KEY); mResolutions = controller.getAllSupportedResolutions(); mHighWidth = controller.getHighWidth(); mFullWidth = controller.getFullWidth(); Log.i(TAG, "mHighWidth:" + mHighWidth + "mFullWidth:" + mFullWidth); mScreenResolutionSummaries = new String[] { mHighWidth + " x " + controller.getHighHeight(), mFullWidth + " x " + controller.getFullHeight() }; } @Override protected int getPreferenceScreenResId() { return R.xml.screen_resolution_settings; } @Override protected void addStaticPreferences(PreferenceScreen screen) { updateIllustrationImage(mImagePreference); screen.addPreference(mImagePreference); final FooterPreference footerPreference = new FooterPreference(screen.getContext()); footerPreference.setTitle(R.string.screen_resolution_footer); footerPreference.setSelectable(false); footerPreference.setLayoutResource( com.android.settingslib.widget.preference.footer.R.layout.preference_footer); screen.addPreference(footerPreference); } @Override public void bindPreferenceExtra( SelectorWithWidgetPreference pref, String key, CandidateInfo info, String defaultKey, String systemDefaultKey) { final ScreenResolutionCandidateInfo candidateInfo = (ScreenResolutionCandidateInfo) info; final CharSequence summary = candidateInfo.loadSummary(); if (summary != null) pref.setSummary(summary); } @Override protected List getCandidates() { final List candidates = new ArrayList<>(); for (int i = 0; i < mScreenResolutionOptions.length; i++) { candidates.add( new ScreenResolutionCandidateInfo( mScreenResolutionOptions[i], mScreenResolutionSummaries[i], mScreenResolutionOptions[i], true /* enabled */)); } return candidates; } /** Get prefer display mode. */ private Display.Mode getPreferMode(int width) { for (Point resolution : mResolutions) { if (resolution.x == width) { return new Display.Mode( resolution.x, resolution.y, getDisplayMode().getRefreshRate()); } } return getDisplayMode(); } /** Get current display mode. */ @VisibleForTesting public Display.Mode getDisplayMode() { return mDefaultDisplay.getMode(); } /** Using display manager to set the display mode. */ @VisibleForTesting public void setDisplayMode(final int width) { Display.Mode mode = getPreferMode(width); mDisplayObserver.startObserve(); /** For store settings globally. */ /** TODO(b/259797244): Remove this once the atom is fully populated. */ Settings.System.putString( getContext().getContentResolver(), SCREEN_RESOLUTION, mode.getPhysicalWidth() + "x" + mode.getPhysicalHeight()); try { /** Apply the resolution change. */ Log.i(TAG, "setUserPreferredDisplayMode: " + mode); mDefaultDisplay.setUserPreferredDisplayMode(mode); } catch (Exception e) { Log.e(TAG, "setUserPreferredDisplayMode() failed", e); return; } /** Send the atom after resolution changed successfully. */ SettingsStatsLog.write( SettingsStatsLog.USER_SELECTED_RESOLUTION, mDefaultDisplay.getUniqueId().hashCode(), mode.getPhysicalWidth(), mode.getPhysicalHeight()); } /** Get the key corresponding to the resolution. */ @VisibleForTesting String getKeyForResolution(int width) { return width == mHighWidth ? mScreenResolutionOptions[ScreenResolutionController.HIGHRESOLUTION_IDX] : width == mFullWidth ? mScreenResolutionOptions[ScreenResolutionController.FULLRESOLUTION_IDX] : null; } /** Get the width corresponding to the resolution key. */ int getWidthForResoluitonKey(String key) { return mScreenResolutionOptions[ScreenResolutionController.HIGHRESOLUTION_IDX].equals(key) ? mHighWidth : mScreenResolutionOptions[ScreenResolutionController.FULLRESOLUTION_IDX].equals( key) ? mFullWidth : -1; } @Override protected String getDefaultKey() { int physicalWidth = getDisplayMode().getPhysicalWidth(); return getKeyForResolution(physicalWidth); } @Override protected boolean setDefaultKey(final String key) { int width = getWidthForResoluitonKey(key); if (width < 0) { return false; } setDisplayMode(width); updateIllustrationImage(mImagePreference); return true; } @Override public void onRadioButtonClicked(SelectorWithWidgetPreference selected) { String selectedKey = selected.getKey(); int selectedWidth = getWidthForResoluitonKey(selectedKey); if (!mDisplayObserver.setPendingResolutionChange(selectedWidth)) { return; } if (mAccessibilityManager.isEnabled()) { AccessibilityEvent event = AccessibilityEvent.obtain(); event.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT); event.getText().add(mResources.getString(R.string.screen_resolution_selected_a11y)); mAccessibilityManager.sendAccessibilityEvent(event); } super.onRadioButtonClicked(selected); } /** Update the resolution image according display mode. */ private void updateIllustrationImage(IllustrationPreference preference) { String key = getDefaultKey(); if (TextUtils.equals( mScreenResolutionOptions[ScreenResolutionController.HIGHRESOLUTION_IDX], key)) { preference.setLottieAnimationResId(R.drawable.screen_resolution_high); } else if (TextUtils.equals( mScreenResolutionOptions[ScreenResolutionController.FULLRESOLUTION_IDX], key)) { preference.setLottieAnimationResId(R.drawable.screen_resolution_full); } } @Override public int getMetricsCategory() { return SettingsEnums.SCREEN_RESOLUTION; } /** This is an extension of the CandidateInfo class, which adds summary information. */ public static class ScreenResolutionCandidateInfo extends CandidateInfo { private final CharSequence mLabel; private final CharSequence mSummary; private final String mKey; ScreenResolutionCandidateInfo( CharSequence label, CharSequence summary, String key, boolean enabled) { super(enabled); mLabel = label; mSummary = summary; mKey = key; } @Override public CharSequence loadLabel() { return mLabel; } /** It is the summary for radio options. */ public CharSequence loadSummary() { return mSummary; } @Override public Drawable loadIcon() { return null; } @Override public String getKey() { return mKey; } } public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider(R.xml.screen_resolution_settings) { @Override protected boolean isPageSearchEnabled(Context context) { ScreenResolutionController mController = new ScreenResolutionController(context, SCREEN_RESOLUTION_KEY); return mController.checkSupportedResolutions(); } }; private static final class DisplayObserver implements DisplayManager.DisplayListener { private final @Nullable Context mContext; private int mDefaultDensity; private int mCurrentIndex; private AtomicInteger mPreviousWidth = new AtomicInteger(-1); DisplayObserver(Context context) { mContext = context; } public void startObserve() { if (mContext == null) { return; } final DisplayDensityUtils density = new DisplayDensityUtils(mContext); final int currentIndex = density.getCurrentIndexForDefaultDisplay(); final int defaultDensity = density.getDefaultDensityForDefaultDisplay(); if (density.getDefaultDisplayDensityValues()[mCurrentIndex] == density.getDefaultDensityForDefaultDisplay()) { return; } mDefaultDensity = defaultDensity; mCurrentIndex = currentIndex; final DisplayManager dm = mContext.getSystemService(DisplayManager.class); dm.registerDisplayListener(this, null); } public void stopObserve() { if (mContext == null) { return; } final DisplayManager dm = mContext.getSystemService(DisplayManager.class); dm.unregisterDisplayListener(this); } @Override public void onDisplayAdded(int displayId) {} @Override public void onDisplayRemoved(int displayId) {} @Override public void onDisplayChanged(int displayId) { if (displayId != Display.DEFAULT_DISPLAY) { return; } if (!isDensityChanged() || !isResolutionChangeApplied()) { return; } restoreDensity(); stopObserve(); } private void restoreDensity() { final DisplayDensityUtils density = new DisplayDensityUtils(mContext); /* If current density is the same as a default density of other resolutions, * then mCurrentIndex may be out of boundary. */ if (density.getDefaultDisplayDensityValues().length <= mCurrentIndex) { mCurrentIndex = density.getCurrentIndexForDefaultDisplay(); } if (density.getDefaultDisplayDensityValues()[mCurrentIndex] != density.getDefaultDensityForDefaultDisplay()) { density.setForcedDisplayDensity(mCurrentIndex); } mDefaultDensity = density.getDefaultDensityForDefaultDisplay(); } private boolean isDensityChanged() { final DisplayDensityUtils density = new DisplayDensityUtils(mContext); if (density.getDefaultDensityForDefaultDisplay() == mDefaultDensity) { return false; } return true; } private int getCurrentWidth() { final DisplayManager dm = mContext.getSystemService(DisplayManager.class); return dm.getDisplay(Display.DEFAULT_DISPLAY).getMode().getPhysicalWidth(); } private boolean setPendingResolutionChange(int selectedWidth) { int currentWidth = getCurrentWidth(); if (selectedWidth == currentWidth) { return false; } if (mPreviousWidth.get() != -1 && !isResolutionChangeApplied()) { return false; } mPreviousWidth.set(currentWidth); return true; } private boolean isResolutionChangeApplied() { if (mPreviousWidth.get() == getCurrentWidth()) { return false; } Log.i(TAG, "resolution changed from " + mPreviousWidth.get() + " to " + getCurrentWidth()); return true; } } }