2323import com .limelight .utils .UiHelper ;
2424
2525import android .app .Activity ;
26+ import android .app .AlertDialog ;
2627import android .app .Service ;
2728import android .content .ComponentName ;
29+ import android .content .DialogInterface ;
2830import android .content .Intent ;
2931import android .content .ServiceConnection ;
3032import android .content .SharedPreferences ;
3537import android .os .Bundle ;
3638import android .os .IBinder ;
3739import android .view .ContextMenu ;
40+ import android .view .LayoutInflater ;
3841import android .view .Menu ;
3942import android .view .MenuItem ;
4043import android .view .View ;
4144import android .view .ContextMenu .ContextMenuInfo ;
4245import android .widget .AbsListView ;
4346import android .widget .AdapterView ;
4447import android .widget .AdapterView .OnItemClickListener ;
48+ import android .widget .ArrayAdapter ;
4549import android .widget .ImageView ;
50+ import android .widget .SeekBar ;
51+ import android .widget .Spinner ;
4652import android .widget .TextView ;
4753import android .widget .Toast ;
4854import android .widget .AdapterView .AdapterContextMenuInfo ;
4955
56+ import com .limelight .preferences .PerAppConfiguration ;
57+
5058import org .xmlpull .v1 .XmlPullParserException ;
5159
5260public class AppView extends Activity implements AdapterFragmentCallbacks {
@@ -70,6 +78,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
7078 private final static int VIEW_DETAILS_ID = 5 ;
7179 private final static int CREATE_SHORTCUT_ID = 6 ;
7280 private final static int HIDE_APP_ID = 7 ;
81+ private final static int APP_SETTINGS_ID = 8 ;
7382
7483 public final static String HIDDEN_APPS_PREF_FILENAME = "HiddenApps" ;
7584
@@ -415,6 +424,7 @@ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuIn
415424 }
416425
417426 menu .add (Menu .NONE , VIEW_DETAILS_ID , 4 , getResources ().getString (R .string .applist_menu_details ));
427+ menu .add (Menu .NONE , APP_SETTINGS_ID , 5 , getResources ().getString (R .string .applist_menu_streaming_settings ));
418428
419429 if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .O ) {
420430 // Only add an option to create shortcut if box art is loaded
@@ -500,6 +510,10 @@ public void run() {
500510 }
501511 return true ;
502512
513+ case APP_SETTINGS_ID :
514+ showPerAppSettingsDialog (app );
515+ return true ;
516+
503517 default :
504518 return super .onContextItemSelected (item );
505519 }
@@ -645,6 +659,117 @@ public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
645659 listView .requestFocus ();
646660 }
647661
662+ private void showPerAppSettingsDialog (final AppObject app ) {
663+ View view = LayoutInflater .from (this ).inflate (R .layout .dialog_per_app_settings , null );
664+ final Spinner resSpinner = view .findViewById (R .id .resolution_spinner );
665+ final Spinner fpsSpinner = view .findViewById (R .id .fps_spinner );
666+ final SeekBar bitrateSeekBar = view .findViewById (R .id .bitrate_seekbar );
667+ final TextView bitrateLabel = view .findViewById (R .id .bitrate_label );
668+
669+ final String [] resEntries = {
670+ getString (R .string .per_app_settings_global_default ),
671+ "360p (640×360)" , "480p (854×480)" , "720p (1280×720)" ,
672+ "1080p (1920×1080)" , "1440p (2560×1440)" , "4K (3840×2160)"
673+ };
674+ final int [][] resValues = {
675+ {0 , 0 }, {640 , 360 }, {854 , 480 }, {1280 , 720 },
676+ {1920 , 1080 }, {2560 , 1440 }, {3840 , 2160 }
677+ };
678+
679+ final int [] fpsValues = {0 , 30 , 60 , 90 , 120 , 144 , 240 };
680+ final String [] fpsEntries = new String [fpsValues .length ];
681+ fpsEntries [0 ] = getString (R .string .per_app_settings_global_default );
682+ for (int i = 1 ; i < fpsValues .length ; i ++) {
683+ fpsEntries [i ] = fpsValues [i ] + " FPS" ;
684+ }
685+
686+ ArrayAdapter <String > resAdapter = new ArrayAdapter <>(this , android .R .layout .simple_spinner_item , resEntries );
687+ resAdapter .setDropDownViewResource (android .R .layout .simple_spinner_dropdown_item );
688+ resSpinner .setAdapter (resAdapter );
689+
690+ ArrayAdapter <String > fpsAdapter = new ArrayAdapter <>(this , android .R .layout .simple_spinner_item , fpsEntries );
691+ fpsAdapter .setDropDownViewResource (android .R .layout .simple_spinner_dropdown_item );
692+ fpsSpinner .setAdapter (fpsAdapter );
693+
694+ // SeekBar: progress 0 = default, progress N = N * 500 kbps (max 150 Mbps)
695+ final int bitrateStep = 500 ;
696+ bitrateSeekBar .setMax (150000 / bitrateStep );
697+ bitrateSeekBar .setOnSeekBarChangeListener (new SeekBar .OnSeekBarChangeListener () {
698+ @ Override
699+ public void onProgressChanged (SeekBar seekBar , int progress , boolean fromUser ) {
700+ if (progress == 0 ) {
701+ bitrateLabel .setText (getString (R .string .per_app_settings_bitrate_default ));
702+ } else {
703+ bitrateLabel .setText (String .format ("%.1f Mbps" , progress * bitrateStep / 1000.0 ));
704+ }
705+ }
706+
707+ @ Override
708+ public void onStartTrackingTouch (SeekBar seekBar ) {
709+ }
710+
711+ @ Override
712+ public void onStopTrackingTouch (SeekBar seekBar ) {
713+ }
714+ });
715+
716+ // Load existing overrides
717+ PerAppConfiguration override = PerAppConfiguration .readOverride (this , uuidString , app .app .getAppId ());
718+
719+ int resIndex = 0 ;
720+ if (override .width > 0 ) {
721+ for (int i = 1 ; i < resValues .length ; i ++) {
722+ if (resValues [i ][0 ] == override .width && resValues [i ][1 ] == override .height ) {
723+ resIndex = i ;
724+ break ;
725+ }
726+ }
727+ }
728+ resSpinner .setSelection (resIndex );
729+
730+ int fpsIndex = 0 ;
731+ if (override .fps > 0 ) {
732+ for (int i = 1 ; i < fpsValues .length ; i ++) {
733+ if (fpsValues [i ] == override .fps ) {
734+ fpsIndex = i ;
735+ break ;
736+ }
737+ }
738+ }
739+ fpsSpinner .setSelection (fpsIndex );
740+
741+ int bitrateProgress = override .bitrate > 0 ? override .bitrate / bitrateStep : 0 ;
742+ bitrateSeekBar .setProgress (bitrateProgress );
743+ bitrateLabel .setText (bitrateProgress == 0
744+ ? getString (R .string .per_app_settings_bitrate_default )
745+ : String .format ("%.1f Mbps" , bitrateProgress * bitrateStep / 1000.0 ));
746+
747+ new AlertDialog .Builder (this )
748+ .setTitle (getString (R .string .per_app_settings_title ) + ": " + app .app .getAppName ())
749+ .setView (view )
750+ .setPositiveButton (android .R .string .ok , new DialogInterface .OnClickListener () {
751+ @ Override
752+ public void onClick (DialogInterface dialog , int which ) {
753+ PerAppConfiguration newOverride = new PerAppConfiguration ();
754+ int ri = resSpinner .getSelectedItemPosition ();
755+ newOverride .width = resValues [ri ][0 ];
756+ newOverride .height = resValues [ri ][1 ];
757+ newOverride .fps = fpsValues [fpsSpinner .getSelectedItemPosition ()];
758+ int progress = bitrateSeekBar .getProgress ();
759+ newOverride .bitrate = progress > 0 ? progress * bitrateStep : 0 ;
760+ PerAppConfiguration .saveOverride (AppView .this , uuidString , app .app .getAppId (), newOverride );
761+ }
762+ })
763+ .setNegativeButton (android .R .string .cancel , null )
764+ .setNeutralButton (R .string .per_app_settings_reset , new DialogInterface .OnClickListener () {
765+ @ Override
766+ public void onClick (DialogInterface dialog , int which ) {
767+ PerAppConfiguration .deleteOverride (AppView .this , uuidString , app .app .getAppId ());
768+ }
769+ })
770+ .show ();
771+ }
772+
648773 public static class AppObject {
649774 public final NvApp app ;
650775 public boolean isRunning ;
0 commit comments