package com.limelight.preferences; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.media.MediaCodecInfo; import android.os.Build; import android.os.Bundle; import android.app.Activity; import android.os.Handler; import android.os.Vibrator; import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceCategory; import android.preference.PreferenceFragment; import android.preference.PreferenceManager; import android.preference.PreferenceScreen; import android.util.DisplayMetrics; import android.util.Range; import android.view.Display; import android.view.DisplayCutout; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; import com.limelight.LimeLog; import com.limelight.PcView; import com.limelight.R; import com.limelight.binding.video.MediaCodecHelper; import com.limelight.utils.Dialog; import com.limelight.utils.UiHelper; import java.lang.reflect.Method; import java.util.Arrays; public class StreamSettings extends Activity { private PreferenceConfiguration previousPrefs; // HACK for Android 9 static DisplayCutout displayCutoutP; void reloadSettings() { getFragmentManager().beginTransaction().replace( R.id.stream_settings, new SettingsFragment() ).commit(); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); previousPrefs = PreferenceConfiguration.readPreferences(this); UiHelper.setLocale(this); setContentView(R.layout.activity_stream_settings); UiHelper.notifyNewRootView(this); } @Override public void onAttachedToWindow() { super.onAttachedToWindow(); // We have to use this hack on Android 9 because we don't have Display.getCutout() // which was added in Android 10. if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { // Insets can be null when the activity is recreated on screen rotation // https://stackoverflow.com/questions/61241255/windowinsets-getdisplaycutout-is-null-everywhere-except-within-onattachedtowindo WindowInsets insets = getWindow().getDecorView().getRootWindowInsets(); if (insets != null) { displayCutoutP = insets.getDisplayCutout(); } } reloadSettings(); } @Override public void onBackPressed() { finish(); // Check for changes that require a UI reload to take effect PreferenceConfiguration newPrefs = PreferenceConfiguration.readPreferences(this); if (!newPrefs.language.equals(previousPrefs.language)) { // Restart the PC view to apply UI changes Intent intent = new Intent(this, PcView.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent, null); } } public static class SettingsFragment extends PreferenceFragment { private int nativeResolutionStartIndex = Integer.MAX_VALUE; private void setValue(String preferenceKey, String value) { ListPreference pref = (ListPreference) findPreference(preferenceKey); pref.setValue(value); } private void addNativeResolutionEntry(int nativeWidth, int nativeHeight, boolean insetsRemoved) { ListPreference pref = (ListPreference) findPreference(PreferenceConfiguration.RESOLUTION_PREF_STRING); String newName; if (insetsRemoved) { newName = getResources().getString(R.string.resolution_prefix_native_fullscreen); } else { newName = getResources().getString(R.string.resolution_prefix_native); } newName += " ("+nativeWidth+"x"+nativeHeight+")"; String newValue = nativeWidth+"x"+nativeHeight; CharSequence[] values = pref.getEntryValues(); // Check if the native resolution is already present for (CharSequence value : values) { if (newValue.equals(value.toString())) { // It is present in the default list, so don't add it again return; } } CharSequence[] newEntries = Arrays.copyOf(pref.getEntries(), pref.getEntries().length + 1); CharSequence[] newValues = Arrays.copyOf(values, values.length + 1); // Add the new native option newEntries[newEntries.length - 1] = newName; newValues[newValues.length - 1] = newValue; pref.setEntries(newEntries); pref.setEntryValues(newValues); if (newValues.length - 1 < nativeResolutionStartIndex) { nativeResolutionStartIndex = newValues.length - 1; } } private void removeValue(String preferenceKey, String value, Runnable onMatched) { int matchingCount = 0; ListPreference pref = (ListPreference) findPreference(preferenceKey); // Count the number of matching entries we'll be removing for (CharSequence seq : pref.getEntryValues()) { if (seq.toString().equalsIgnoreCase(value)) { matchingCount++; } } // Create the new arrays CharSequence[] entries = new CharSequence[pref.getEntries().length-matchingCount]; CharSequence[] entryValues = new CharSequence[pref.getEntryValues().length-matchingCount]; int outIndex = 0; for (int i = 0; i < pref.getEntryValues().length; i++) { if (pref.getEntryValues()[i].toString().equalsIgnoreCase(value)) { // Skip matching values continue; } entries[outIndex] = pref.getEntries()[i]; entryValues[outIndex] = pref.getEntryValues()[i]; outIndex++; } if (pref.getValue().equalsIgnoreCase(value)) { onMatched.run(); } // Update the preference with the new list pref.setEntries(entries); pref.setEntryValues(entryValues); } private void resetBitrateToDefault(SharedPreferences prefs, String res, String fps) { if (res == null) { res = prefs.getString(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION); } if (fps == null) { fps = prefs.getString(PreferenceConfiguration.FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS); } prefs.edit() .putInt(PreferenceConfiguration.BITRATE_PREF_STRING, PreferenceConfiguration.getDefaultBitrate(res, fps)) .apply(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); UiHelper.applyStatusBarPadding(view); return view; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.preferences); PreferenceScreen screen = getPreferenceScreen(); // hide on-screen controls category on non touch screen devices if (!getActivity().getPackageManager(). hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) { { PreferenceCategory category = (PreferenceCategory) findPreference("category_onscreen_controls"); screen.removePreference(category); } { PreferenceCategory category = (PreferenceCategory) findPreference("category_input_settings"); category.removePreference(findPreference("checkbox_touchscreen_trackpad")); } } // Remove PiP mode on devices pre-Oreo, where the feature is not available (some low RAM devices), // and on Fire OS where it violates the Amazon App Store guidelines for some reason. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !getActivity().getPackageManager().hasSystemFeature("android.software.picture_in_picture") || getActivity().getPackageManager().hasSystemFeature("com.amazon.software.fireos")) { PreferenceCategory category = (PreferenceCategory) findPreference("category_ui_settings"); category.removePreference(findPreference("checkbox_enable_pip")); } // Remove the vibration options if the device can't vibrate if (!((Vibrator)getActivity().getSystemService(Context.VIBRATOR_SERVICE)).hasVibrator()) { PreferenceCategory category = (PreferenceCategory) findPreference("category_input_settings"); category.removePreference(findPreference("checkbox_vibrate_fallback")); // The entire OSC category may have already been removed by the touchscreen check above category = (PreferenceCategory) findPreference("category_onscreen_controls"); if (category != null) { category.removePreference(findPreference("checkbox_vibrate_osc")); } } int maxSupportedFps = 0; // Hide non-supported resolution/FPS combinations if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Display display = getActivity().getWindowManager().getDefaultDisplay(); int maxSupportedResW = 0; // Add a native resolution with any insets included for users that don't want content // behind the notch of their display boolean hasInsets = false; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { DisplayCutout cutout; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Use the much nicer Display.getCutout() API on Android 10+ cutout = display.getCutout(); } else { // Android 9 only cutout = displayCutoutP; } if (cutout != null) { int widthInsets = cutout.getSafeInsetLeft() + cutout.getSafeInsetRight(); int heightInsets = cutout.getSafeInsetBottom() + cutout.getSafeInsetTop(); if (widthInsets != 0 || heightInsets != 0) { DisplayMetrics metrics = new DisplayMetrics(); display.getRealMetrics(metrics); int width = Math.max(metrics.widthPixels - widthInsets, metrics.heightPixels - heightInsets); int height = Math.min(metrics.widthPixels - widthInsets, metrics.heightPixels - heightInsets); addNativeResolutionEntry(width, height, false); hasInsets = true; } } } // Always allow resolutions that are smaller or equal to the active // display resolution because decoders can report total non-sense to us. // For example, a p201 device reports: // AVC Decoder: OMX.amlogic.avc.decoder.awesome // HEVC Decoder: OMX.amlogic.hevc.decoder.awesome // AVC supported width range: 64 - 384 // HEVC supported width range: 64 - 544 for (Display.Mode candidate : display.getSupportedModes()) { // Some devices report their dimensions in the portrait orientation // where height > width. Normalize these to the conventional width > height // arrangement before we process them. int width = Math.max(candidate.getPhysicalWidth(), candidate.getPhysicalHeight()); int height = Math.min(candidate.getPhysicalWidth(), candidate.getPhysicalHeight()); // Some TVs report strange values here, so let's avoid native resolutions on a TV // unless they report greater than 4K resolutions. if (!getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION) || (width > 3840 || height > 2160)) { addNativeResolutionEntry(width, height, hasInsets); } if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) { maxSupportedResW = 3840; } else if ((width >= 2560 || height >= 1440) && maxSupportedResW < 2560) { maxSupportedResW = 2560; } else if ((width >= 1920 || height >= 1080) && maxSupportedResW < 1920) { maxSupportedResW = 1920; } if (candidate.getRefreshRate() > maxSupportedFps) { maxSupportedFps = (int)candidate.getRefreshRate(); } } // This must be called to do runtime initialization before calling functions that evaluate // decoder lists. MediaCodecHelper.initialize(getContext(), GlPreferences.readPreferences(getContext()).glRenderer); MediaCodecInfo avcDecoder = MediaCodecHelper.findProbableSafeDecoder("video/avc", -1); MediaCodecInfo hevcDecoder = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1); if (avcDecoder != null) { Range avcWidthRange = avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getSupportedWidths(); LimeLog.info("AVC supported width range: "+avcWidthRange.getLower()+" - "+avcWidthRange.getUpper()); // If 720p is not reported as supported, ignore all results from this API if (avcWidthRange.contains(1280)) { if (avcWidthRange.contains(3840) && maxSupportedResW < 3840) { maxSupportedResW = 3840; } else if (avcWidthRange.contains(1920) && maxSupportedResW < 1920) { maxSupportedResW = 1920; } else if (maxSupportedResW < 1280) { maxSupportedResW = 1280; } } } if (hevcDecoder != null) { Range hevcWidthRange = hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getSupportedWidths(); LimeLog.info("HEVC supported width range: "+hevcWidthRange.getLower()+" - "+hevcWidthRange.getUpper()); // If 720p is not reported as supported, ignore all results from this API if (hevcWidthRange.contains(1280)) { if (hevcWidthRange.contains(3840) && maxSupportedResW < 3840) { maxSupportedResW = 3840; } else if (hevcWidthRange.contains(1920) && maxSupportedResW < 1920) { maxSupportedResW = 1920; } else if (maxSupportedResW < 1280) { maxSupportedResW = 1280; } } } LimeLog.info("Maximum resolution slot: "+maxSupportedResW); if (maxSupportedResW != 0) { if (maxSupportedResW < 3840) { // 4K is unsupported removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_4K, new Runnable() { @Override public void run() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity()); setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1440P); resetBitrateToDefault(prefs, null, null); } }); } if (maxSupportedResW < 2560) { // 1440p is unsupported removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1440P, new Runnable() { @Override public void run() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity()); setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1080P); resetBitrateToDefault(prefs, null, null); } }); } if (maxSupportedResW < 1920) { // 1080p is unsupported removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1080P, new Runnable() { @Override public void run() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity()); setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_720P); resetBitrateToDefault(prefs, null, null); } }); } // Never remove 720p } } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { // On Android 4.2 and later, we can get the true metrics via the // getRealMetrics() function (unlike the lies that getWidth() and getHeight() // tell to us). DisplayMetrics metrics = new DisplayMetrics(); getActivity().getWindowManager().getDefaultDisplay().getRealMetrics(metrics); int width = Math.max(metrics.widthPixels, metrics.heightPixels); int height = Math.min(metrics.widthPixels, metrics.heightPixels); addNativeResolutionEntry(width, height, false); } else { // On Android 4.1, we have to resort to reflection to invoke hidden APIs // to get the real screen dimensions. Display display = getActivity().getWindowManager().getDefaultDisplay(); try { Method getRawHeightFunc = Display.class.getMethod("getRawHeight"); Method getRawWidthFunc = Display.class.getMethod("getRawWidth"); int width = (Integer) getRawWidthFunc.invoke(display); int height = (Integer) getRawHeightFunc.invoke(display); addNativeResolutionEntry(Math.max(width, height), Math.min(width, height), false); } catch (Exception e) { e.printStackTrace(); } } if (!PreferenceConfiguration.readPreferences(this.getActivity()).unlockFps) { // We give some extra room in case the FPS is rounded down if (maxSupportedFps < 118) { removeValue(PreferenceConfiguration.FPS_PREF_STRING, "120", new Runnable() { @Override public void run() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity()); setValue(PreferenceConfiguration.FPS_PREF_STRING, "90"); resetBitrateToDefault(prefs, null, null); } }); } if (maxSupportedFps < 88) { // 1080p is unsupported removeValue(PreferenceConfiguration.FPS_PREF_STRING, "90", new Runnable() { @Override public void run() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity()); setValue(PreferenceConfiguration.FPS_PREF_STRING, "60"); resetBitrateToDefault(prefs, null, null); } }); } // Never remove 30 FPS or 60 FPS } // Android L introduces proper 7.1 surround sound support. Remove the 7.1 option // for earlier versions of Android to prevent AudioTrack initialization issues. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { LimeLog.info("Excluding 7.1 surround sound option based on OS"); removeValue(PreferenceConfiguration.AUDIO_CONFIG_PREF_STRING, "71", new Runnable() { @Override public void run() { setValue(PreferenceConfiguration.AUDIO_CONFIG_PREF_STRING, "51"); } }); } // Android L introduces the drop duplicate behavior of releaseOutputBuffer() // that the unlock FPS option relies on to not massively increase latency. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { LimeLog.info("Excluding unlock FPS toggle based on OS"); PreferenceCategory category = (PreferenceCategory) findPreference("category_advanced_settings"); category.removePreference(findPreference("checkbox_unlock_fps")); } else { findPreference(PreferenceConfiguration.UNLOCK_FPS_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { // HACK: We need to let the preference change succeed before reinitializing to ensure // it's reflected in the new layout. final Handler h = new Handler(); h.postDelayed(new Runnable() { @Override public void run() { // Ensure the activity is still open when this timeout expires StreamSettings settingsActivity = (StreamSettings)SettingsFragment.this.getActivity(); if (settingsActivity != null) { settingsActivity.reloadSettings(); } } }, 500); // Allow the original preference change to take place return true; } }); } // Remove HDR preference for devices below Nougat if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { LimeLog.info("Excluding HDR toggle based on OS"); PreferenceCategory category = (PreferenceCategory) findPreference("category_advanced_settings"); category.removePreference(findPreference("checkbox_enable_hdr")); } else { Display display = getActivity().getWindowManager().getDefaultDisplay(); Display.HdrCapabilities hdrCaps = display.getHdrCapabilities(); // We must now ensure our display is compatible with HDR10 boolean foundHdr10 = false; if (hdrCaps != null) { // getHdrCapabilities() returns null on Lenovo Lenovo Mirage Solo (vega), Android 8.0 for (int hdrType : hdrCaps.getSupportedHdrTypes()) { if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) { foundHdr10 = true; } } } if (!foundHdr10) { LimeLog.info("Excluding HDR toggle based on display capabilities"); PreferenceCategory category = (PreferenceCategory) findPreference("category_advanced_settings"); category.removePreference(findPreference("checkbox_enable_hdr")); } } // Add a listener to the FPS and resolution preference // so the bitrate can be auto-adjusted findPreference(PreferenceConfiguration.RESOLUTION_PREF_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity()); String valueStr = (String) newValue; // Detect if this value is the native resolution option CharSequence[] values = ((ListPreference)preference).getEntryValues(); boolean isNativeRes = true; for (int i = 0; i < values.length; i++) { // Look for a match prior to the start of the native resolution entries if (valueStr.equals(values[i].toString()) && i < nativeResolutionStartIndex) { isNativeRes = false; break; } } // If this is native resolution, show the warning dialog if (isNativeRes) { Dialog.displayDialog(getActivity(), getResources().getString(R.string.title_native_res_dialog), getResources().getString(R.string.text_native_res_dialog), false); } // Write the new bitrate value resetBitrateToDefault(prefs, valueStr, null); // Allow the original preference change to take place return true; } }); findPreference(PreferenceConfiguration.FPS_PREF_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity()); String valueStr = (String) newValue; // Write the new bitrate value resetBitrateToDefault(prefs, null, valueStr); // Allow the original preference change to take place return true; } }); } } }