diff --git a/app/src/main/java/com/limelight/preferences/CustomResolutionsPreference.java b/app/src/main/java/com/limelight/preferences/CustomResolutionsPreference.java new file mode 100644 index 0000000000..34bcf03dd5 --- /dev/null +++ b/app/src/main/java/com/limelight/preferences/CustomResolutionsPreference.java @@ -0,0 +1,245 @@ +package com.limelight.preferences; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.DialogPreference; +import android.text.Editable; +import android.text.InputType; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.*; +import com.limelight.R; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class CustomResolutionsConsts { + public static final String CUSTOM_RESOLUTIONS_FILE = "custom_resolutions"; + public static final String CUSTOM_RESOLUTIONS_KEY = "custom_resolutions"; +} +class Validate { + static public boolean isValidResolution(String res){ + String regex = "^\\d{3,5}x\\d{3,5}$"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(res); + return matcher.matches(); + } +} + +public class CustomResolutionsPreference extends DialogPreference { + private final Context context; + private final CustomResolutionsAdapter adapter; + + public CustomResolutionsPreference(Context context, AttributeSet attrs) { + super(context, attrs); + this.context = context; + this.adapter = new CustomResolutionsAdapter(context); + adapter.setOnDataChangedListener(new UpdateStorageEventListener()); + } + + void onSubmitResolution(EditText field){ + field.setError(null); + + Editable editable = field.getText(); + String content = editable.toString(); + + + if(!Validate.isValidResolution(content)) { + field.setError("Invalid resolution."); + return; + } + if(adapter.exists(content)) { + field.setError("Item already exists."); + return; + } + + adapter.addItem(content); + } + + @Override + protected void onBindDialogView(View view) { + SharedPreferences prefs = context.getSharedPreferences(CustomResolutionsConsts.CUSTOM_RESOLUTIONS_FILE, Context.MODE_PRIVATE); + Set stored = prefs.getStringSet(CustomResolutionsConsts.CUSTOM_RESOLUTIONS_KEY, null); + + if(stored == null) return; + + ArrayList list = new ArrayList<>(stored); + Comparator lengthComparator = new Comparator() { + @Override + public int compare(String s1, String s2) { + String[] s1Size = s1.split("x"); + String[] s2Size = s2.split("x"); + + int w1 = Integer.parseInt(s1Size[0]); + int w2 = Integer.parseInt(s2Size[0]); + + int h1 = Integer.parseInt(s1Size[1]); + int h2 = Integer.parseInt(s2Size[1]); + + if(w1 == w2) { + return Integer.compare(h1, h2); + } + return Integer.compare(w1, w2); + } + }; + Collections.sort(list, lengthComparator); + + adapter.addAll(list); + super.onBindDialogView(view); + } + + @Override + protected View onCreateDialogView() { + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, AbsListView.LayoutParams.WRAP_CONTENT); + + LinearLayout body = new LinearLayout(context); + LayoutInflater inflater = LayoutInflater.from(context); + ListView list = new ListView(context); + + body.setLayoutParams(layoutParams); + list.setLayoutParams(layoutParams); + + View inputRow = inflater.inflate(R.layout.custom_resolutions_form, null); + EditText textEditingField = inputRow.findViewById(R.id.custom_resolution_field); + ImageButton doneEditingButton = inputRow.findViewById(R.id.done_editing); + + body.addView(list); + body.addView(inputRow); + body.setOrientation(LinearLayout.VERTICAL); + + inputRow.setLayoutParams(layoutParams); + + textEditingField.setHint("2400x1080"); + list.setAdapter(adapter); + doneEditingButton.setOnClickListener(view -> onSubmitResolution(textEditingField)); + + return body; + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + StreamSettings settingsActivity = (StreamSettings) getContext(); + settingsActivity.reloadSettings(); + } + + private class UpdateStorageEventListener implements EventListener { + @Override + public void onTrigger() { + SharedPreferences prefs = context.getSharedPreferences(CustomResolutionsConsts.CUSTOM_RESOLUTIONS_FILE, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + Set set = new HashSet<>(adapter.getAll()); + editor.putStringSet(CustomResolutionsConsts.CUSTOM_RESOLUTIONS_KEY, set).apply(); + } + } +} + +interface EventListener { + void onTrigger(); +} + +class CustomResolutionsAdapter extends BaseAdapter { + ArrayList resolutions = new ArrayList(); + private final Context context; + private EventListener listener; + + public CustomResolutionsAdapter(Context context) { + this.context = context; + } + + public void setOnDataChangedListener(EventListener listener) { + this.listener = listener; + } + + @Override + public void notifyDataSetChanged() { + if (listener != null) { + listener.onTrigger(); + } + super.notifyDataSetChanged(); + } + + @Override + public View getView(int i, View view, ViewGroup viewGroup) { + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + + LinearLayout row = new LinearLayout(context); + TextView listItemText = new TextView(context); + ImageButton deleteButton = new ImageButton(context); + + row.setLayoutParams(layoutParams); + row.setPadding(dpToPx(8), dpToPx(4), dpToPx(8), dpToPx(4)); + row.setOrientation(LinearLayout.HORIZONTAL); + + row.addView(listItemText); + row.addView(deleteButton); + + layoutParams.gravity = Gravity.CENTER; + layoutParams.weight = 1; + + listItemText.setLayoutParams(layoutParams); + listItemText.setText(resolutions.get(i)); + + LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams(dpToPx(64), dpToPx(64)); + + deleteButton.setLayoutParams(buttonParams); + deleteButton.setImageResource(R.drawable.ic_delete); + + deleteButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + resolutions.remove(i); + notifyDataSetChanged(); + } + }); + + view = row; + return view; + } + + @Override + public int getCount() { + return resolutions.size(); + } + + @Override + public Object getItem(int i) { + return resolutions.get(i); + } + + public void addItem(String value) { + if (resolutions.contains(value)) { + return; + } + resolutions.add(value); + notifyDataSetChanged(); + } + + public ArrayList getAll() { + return resolutions; + } + public void addAll(ArrayList list) { + this.resolutions.addAll(list); + notifyDataSetChanged(); + } + + public boolean exists(String item){ + return resolutions.contains(item); + } + + @Override + public long getItemId(int i) { + return 0; + } + + private int dpToPx(int value) { + float density = this.context.getResources().getDisplayMetrics().density; + return (int) (value * density + 0.5f); + } +} + diff --git a/app/src/main/java/com/limelight/preferences/StreamSettings.java b/app/src/main/java/com/limelight/preferences/StreamSettings.java index 7070104168..9cfc5253ff 100644 --- a/app/src/main/java/com/limelight/preferences/StreamSettings.java +++ b/app/src/main/java/com/limelight/preferences/StreamSettings.java @@ -19,6 +19,7 @@ import android.preference.PreferenceManager; import android.preference.PreferenceScreen; import android.util.DisplayMetrics; +import android.util.Log; import android.util.Range; import android.view.Display; import android.view.DisplayCutout; @@ -31,11 +32,12 @@ import com.limelight.PcView; import com.limelight.R; import com.limelight.binding.video.MediaCodecHelper; +import com.limelight.utils.AspectRatioConverter; import com.limelight.utils.Dialog; import com.limelight.utils.UiHelper; import java.lang.reflect.Method; -import java.util.Arrays; +import java.util.*; public class StreamSettings extends Activity { private PreferenceConfiguration previousPrefs; @@ -188,6 +190,58 @@ private void addNativeResolutionEntries(int nativeWidth, int nativeHeight, boole } addNativeResolutionEntry(nativeWidth, nativeHeight, insetsRemoved, false); } + private void addCustomResolutionsEntries() { + SharedPreferences storage = this.getActivity().getSharedPreferences(CustomResolutionsConsts.CUSTOM_RESOLUTIONS_FILE, Context.MODE_PRIVATE); + Set stored = storage.getStringSet(CustomResolutionsConsts.CUSTOM_RESOLUTIONS_KEY, null); + ListPreference pref = (ListPreference) findPreference(PreferenceConfiguration.RESOLUTION_PREF_STRING); + + List preferencesList = Arrays.asList(pref.getEntryValues()); + + if(stored == null) { + return; + }; + + Comparator lengthComparator = new Comparator() { + @Override + public int compare(String s1, String s2) { + String[] s1Size = s1.split("x"); + String[] s2Size = s2.split("x"); + + int w1 = Integer.parseInt(s1Size[0]); + int w2 = Integer.parseInt(s2Size[0]); + + int h1 = Integer.parseInt(s1Size[1]); + int h2 = Integer.parseInt(s2Size[1]); + + if(w1 == w2) { + return Integer.compare(h1, h2); + } + return Integer.compare(w1, w2); + } + }; + + ArrayList list = new ArrayList<>(stored); + Collections.sort(list, lengthComparator); + + for (String storedResolution : list) { + if(preferencesList.contains(storedResolution)){ + continue; + } + String[] resolution = storedResolution.split("x"); + int width = Integer.parseInt(resolution[0]); + int height = Integer.parseInt(resolution[1]); + String aspectRatio = AspectRatioConverter.getAspectRatio(width,height); + String displayText = "Custom "; + + if(aspectRatio != null){ + displayText+=aspectRatio+" "; + } + + displayText+="("+storedResolution+")"; + + appendPreferenceEntry(pref, displayText, storedResolution); + } + } private void addNativeFrameRateEntry(float framerate) { int frameRateRounded = Math.round(framerate); @@ -667,6 +721,8 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { return true; } }); + + addCustomResolutionsEntries(); } } } diff --git a/app/src/main/java/com/limelight/utils/AspectRatioConverter.java b/app/src/main/java/com/limelight/utils/AspectRatioConverter.java new file mode 100644 index 0000000000..061711bb18 --- /dev/null +++ b/app/src/main/java/com/limelight/utils/AspectRatioConverter.java @@ -0,0 +1,24 @@ +package com.limelight.utils; + +import android.util.Log; + +public class AspectRatioConverter { + public static String getAspectRatio(int width, int height) { + float ratio = (float) width / height; + float truncatedValue = (float) (Math.floor(ratio * 100) / 100); + + if (truncatedValue == 1.25f) return "5:4"; + if (truncatedValue == 1.33f) return "4:3"; + if (truncatedValue == 1.50f) return "3:2"; + if (truncatedValue == 1.60f) return "16:10"; + if (truncatedValue == 1.77f) return "16:9"; + if (truncatedValue == 1.85f) return "1.85:1"; + if (truncatedValue == 2.22f) return "20:9"; + if (truncatedValue >= 2.37f && truncatedValue <= 2.44f) return "21:9"; + if (truncatedValue == 2.76f) return "2.76:1"; + if (truncatedValue == 3.20f) return "32:10"; + if (truncatedValue == 3.55f) return "32:9"; + + return null; + } +} diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 0000000000..eb27865056 --- /dev/null +++ b/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_done.xml b/app/src/main/res/drawable/ic_done.xml new file mode 100644 index 0000000000..f36df87501 --- /dev/null +++ b/app/src/main/res/drawable/ic_done.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/layout/custom_resolutions_form.xml b/app/src/main/res/layout/custom_resolutions_form.xml new file mode 100644 index 0000000000..6f70d70854 --- /dev/null +++ b/app/src/main/res/layout/custom_resolutions_form.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 82cf3222e7..a4461f0cbd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -161,6 +161,8 @@ (Portrait) FPS Native FPS Warning + Custom resolutions + Create your own custom resolutions Audio Settings Surround sound configuration diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index d68c8c5772..e652835335 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -36,6 +36,10 @@ android:entryValues="@array/video_frame_pacing_values" android:summary="@string/summary_frame_pacing" android:defaultValue="latency" /> +