diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9314732b..01f8eb9a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1,7 +1,7 @@
@@ -29,6 +30,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gen/com/limelight/R.java b/gen/com/limelight/R.java
index c29d1047..38e1884a 100644
--- a/gen/com/limelight/R.java
+++ b/gen/com/limelight/R.java
@@ -34,29 +34,40 @@ or to a theme attribute in the form "?[package:][type:]na
public static final class drawable {
public static final int app_icon=0x7f020000;
public static final int ic_launcher=0x7f020001;
- public static final int ouya_icon=0x7f020002;
+ public static final int list_view_border=0x7f020002;
+ public static final int ouya_icon=0x7f020003;
}
public static final class id {
+ public static final int addPc=0x7f080001;
+ public static final int advancedSettingsButton=0x7f080012;
+ public static final int appListText=0x7f080009;
public static final int autoDec=0x7f080004;
- public static final int bitrateLabel=0x7f08000b;
- public static final int bitrateSeekBar=0x7f08000c;
- public static final int config1080p30Selected=0x7f080009;
- public static final int config1080p60Selected=0x7f08000a;
- public static final int config720p30Selected=0x7f080007;
- public static final int config720p60Selected=0x7f080008;
- public static final int decoderConfigGroup=0x7f080001;
+ public static final int bitrateLabel=0x7f080006;
+ public static final int bitrateSeekBar=0x7f080007;
+ public static final int config1080p30Selected=0x7f080010;
+ public static final int config1080p60Selected=0x7f080011;
+ public static final int config720p30Selected=0x7f08000e;
+ public static final int config720p60Selected=0x7f08000f;
+ public static final int decoderConfigGroup=0x7f080002;
+ public static final int discoveryText=0x7f08000b;
public static final int hardwareDec=0x7f080005;
public static final int hostTextView=0x7f080000;
- public static final int pairButton=0x7f080006;
- public static final int quitButton=0x7f08000e;
+ public static final int manuallyAddPc=0x7f080013;
+ public static final int pcListView=0x7f080008;
+ public static final int rowTextView=0x7f080014;
+ public static final int settingsButton=0x7f08000c;
public static final int softwareDec=0x7f080003;
- public static final int statusButton=0x7f08000d;
- public static final int streamConfigGroup=0x7f080002;
- public static final int surfaceView=0x7f08000f;
+ public static final int streamConfigGroup=0x7f08000d;
+ public static final int surfaceView=0x7f08000a;
}
public static final class layout {
- public static final int activity_connection=0x7f030000;
- public static final int activity_game=0x7f030001;
+ public static final int activity_add_computer_manually=0x7f030000;
+ public static final int activity_advanced_settings=0x7f030001;
+ public static final int activity_app_view=0x7f030002;
+ public static final int activity_game=0x7f030003;
+ public static final int activity_pc_view=0x7f030004;
+ public static final int activity_stream_settings=0x7f030005;
+ public static final int simplerow=0x7f030006;
}
public static final class string {
public static final int app_name=0x7f060000;
diff --git a/libs/limelight-common.jar b/libs/limelight-common.jar
index c71e1698..0b2f4329 100644
Binary files a/libs/limelight-common.jar and b/libs/limelight-common.jar differ
diff --git a/res/drawable/list_view_border.xml b/res/drawable/list_view_border.xml
new file mode 100644
index 00000000..50d484b8
--- /dev/null
+++ b/res/drawable/list_view_border.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/res/layout/activity_add_computer_manually.xml b/res/layout/activity_add_computer_manually.xml
new file mode 100644
index 00000000..645b461f
--- /dev/null
+++ b/res/layout/activity_add_computer_manually.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/activity_advanced_settings.xml b/res/layout/activity_advanced_settings.xml
new file mode 100644
index 00000000..21d0f0a0
--- /dev/null
+++ b/res/layout/activity_advanced_settings.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/activity_app_view.xml b/res/layout/activity_app_view.xml
new file mode 100644
index 00000000..301e7bad
--- /dev/null
+++ b/res/layout/activity_app_view.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/activity_connection.xml b/res/layout/activity_connection.xml
deleted file mode 100644
index 8ce33e8d..00000000
--- a/res/layout/activity_connection.xml
+++ /dev/null
@@ -1,143 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/res/layout/activity_pc_view.xml b/res/layout/activity_pc_view.xml
new file mode 100644
index 00000000..9ec5f82a
--- /dev/null
+++ b/res/layout/activity_pc_view.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/activity_stream_settings.xml b/res/layout/activity_stream_settings.xml
new file mode 100644
index 00000000..8847b66a
--- /dev/null
+++ b/res/layout/activity_stream_settings.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/simplerow.xml b/res/layout/simplerow.xml
new file mode 100644
index 00000000..c053584a
--- /dev/null
+++ b/res/layout/simplerow.xml
@@ -0,0 +1,12 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/com/limelight/AddComputerManually.java b/src/com/limelight/AddComputerManually.java
new file mode 100644
index 00000000..e2d8bb49
--- /dev/null
+++ b/src/com/limelight/AddComputerManually.java
@@ -0,0 +1,96 @@
+package com.limelight;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import com.limelight.computers.ComputerManagerService;
+import com.limelight.utils.Dialog;
+
+import android.app.Activity;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class AddComputerManually extends Activity {
+ private Button addPcButton;
+ private TextView hostText;
+ private ServiceConnection serviceConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, final IBinder binder) {
+ new Thread() {
+ @Override
+ public void run() {
+ String msg;
+ boolean finish = false;
+ try {
+ InetAddress addr = InetAddress.getByName(hostText.getText().toString());
+
+ if (!((ComputerManagerService.ComputerManagerBinder)binder).addComputerBlocking(addr)){
+ msg = "Unable to connect to the specified computer. Make sure the required ports are allowed through the firewall.";
+ }
+ else {
+ msg = "Successfully added computer";
+ finish = true;
+ }
+ } catch (UnknownHostException e) {
+ msg = "Unable to resolve PC address. Make sure you didn't make a typo in the address.";
+ }
+
+ final boolean toastFinish = finish;
+ final String toastMsg = msg;
+ AddComputerManually.this.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // Unbind from this service
+ unbindService(AddComputerManually.this.serviceConnection);
+
+ Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
+
+ if (toastFinish) {
+ // Close the activity
+ AddComputerManually.this.finish();
+ }
+ }
+ });
+ }
+ }.start();
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ }
+ };
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+
+ Dialog.closeDialogs();
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_add_computer_manually);
+
+ this.addPcButton = (Button) findViewById(R.id.addPc);
+ this.hostText = (TextView) findViewById(R.id.hostTextView);
+
+ addPcButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Toast.makeText(AddComputerManually.this, "Adding PC...", Toast.LENGTH_LONG).show();
+
+ // Bind to the service which will try to add the PC
+ bindService(new Intent(AddComputerManually.this, ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
+ }
+ });
+ }
+}
diff --git a/src/com/limelight/AdvancedSettings.java b/src/com/limelight/AdvancedSettings.java
new file mode 100644
index 00000000..86bbacff
--- /dev/null
+++ b/src/com/limelight/AdvancedSettings.java
@@ -0,0 +1,102 @@
+package com.limelight;
+
+import com.limelight.utils.Dialog;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.widget.CompoundButton;
+import android.widget.RadioButton;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+
+public class AdvancedSettings extends Activity {
+ private SharedPreferences prefs;
+ private RadioButton forceSoftDec, autoDec, forceHardDec;
+ private SeekBar bitrateSlider;
+ private TextView bitrateLabel;
+
+ @Override
+ public void onPause() {
+ SharedPreferences.Editor editor = prefs.edit();
+
+ editor.putInt(Game.BITRATE_PREF_STRING, bitrateSlider.getProgress());
+ editor.apply();
+
+ super.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+
+ Dialog.closeDialogs();
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_advanced_settings);
+
+ this.forceSoftDec = (RadioButton) findViewById(R.id.softwareDec);
+ this.autoDec = (RadioButton) findViewById(R.id.autoDec);
+ this.forceHardDec = (RadioButton) findViewById(R.id.hardwareDec);
+ this.bitrateLabel = (TextView) findViewById(R.id.bitrateLabel);
+ this.bitrateSlider = (SeekBar) findViewById(R.id.bitrateSeekBar);
+
+ prefs = getSharedPreferences(Game.PREFS_FILE_NAME, Context.MODE_MULTI_PROCESS);
+
+ bitrateSlider.setMax(Game.BITRATE_CEILING);
+ bitrateSlider.setProgress(prefs.getInt(Game.BITRATE_PREF_STRING, Game.DEFAULT_BITRATE));
+ updateBitrateLabel();
+
+ switch (prefs.getInt(Game.DECODER_PREF_STRING, Game.DEFAULT_DECODER)) {
+ case Game.FORCE_SOFTWARE_DECODER:
+ forceSoftDec.setChecked(true);
+ autoDec.setChecked(false);
+ forceHardDec.setChecked(false);
+ break;
+ case Game.AUTOSELECT_DECODER:
+ forceSoftDec.setChecked(false);
+ autoDec.setChecked(true);
+ forceHardDec.setChecked(false);
+ break;
+ case Game.FORCE_HARDWARE_DECODER:
+ forceSoftDec.setChecked(false);
+ autoDec.setChecked(false);
+ forceHardDec.setChecked(true);
+ break;
+ }
+
+ OnCheckedChangeListener occl = new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView,
+ boolean isChecked) {
+ if (!isChecked) {
+ // Ignore non-checked buttons
+ return;
+ }
+
+ if (buttonView == forceSoftDec) {
+ prefs.edit().putInt(Game.DECODER_PREF_STRING, Game.FORCE_SOFTWARE_DECODER).commit();
+ }
+ else if (buttonView == forceHardDec) {
+ prefs.edit().putInt(Game.DECODER_PREF_STRING, Game.FORCE_HARDWARE_DECODER).commit();
+ }
+ else if (buttonView == autoDec) {
+ prefs.edit().putInt(Game.DECODER_PREF_STRING, Game.AUTOSELECT_DECODER).commit();
+ }
+ }
+ };
+ forceSoftDec.setOnCheckedChangeListener(occl);
+ forceHardDec.setOnCheckedChangeListener(occl);
+ autoDec.setOnCheckedChangeListener(occl);
+ }
+
+ private void updateBitrateLabel() {
+ bitrateLabel.setText("Max Bitrate: "+bitrateSlider.getProgress()+" Mbps");
+ }
+}
diff --git a/src/com/limelight/AppView.java b/src/com/limelight/AppView.java
new file mode 100644
index 00000000..bf6f5ac6
--- /dev/null
+++ b/src/com/limelight/AppView.java
@@ -0,0 +1,251 @@
+package com.limelight;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.List;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import com.limelight.binding.PlatformBinding;
+import com.limelight.nvstream.http.GfeHttpResponseException;
+import com.limelight.nvstream.http.NvApp;
+import com.limelight.nvstream.http.NvHTTP;
+import com.limelight.utils.Dialog;
+import com.limelight.utils.SpinnerDialog;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.Toast;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+
+public class AppView extends Activity {
+ private ListView appList;
+ private ArrayAdapter appListAdapter;
+ private InetAddress ipAddress;
+ private String uniqueId;
+
+ private final static int RESUME_ID = 1;
+ private final static int QUIT_ID = 2;
+
+ public final static String ADDRESS_EXTRA = "Address";
+ public final static String UNIQUEID_EXTRA = "UniqueId";
+ public final static String NAME_EXTRA = "Name";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_app_view);
+
+ byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA);
+ uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA);
+ if (address == null || uniqueId == null) {
+ return;
+ }
+
+ setTitle("App List for "+getIntent().getStringExtra(NAME_EXTRA));
+
+ try {
+ ipAddress = InetAddress.getByAddress(address);
+ } catch (UnknownHostException e) {
+ return;
+ }
+
+ // Setup the list view
+ appList = (ListView)findViewById(R.id.pcListView);
+ appListAdapter = new ArrayAdapter(this, R.layout.simplerow);
+ appListAdapter.setNotifyOnChange(false);
+ appList.setAdapter(appListAdapter);
+ appList.setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> arg0, View arg1, int pos,
+ long id) {
+ AppObject app = (AppObject) appListAdapter.getItem(pos);
+ if (app == null || app.app == null) {
+ return;
+ }
+
+ // Only open the context menu if it's running, otherwise start it
+ if (app.app.getIsRunning()) {
+ openContextMenu(arg1);
+ }
+ else {
+ doStart(app.app);
+ }
+ }
+ });
+ registerForContextMenu(appList);
+ updateAppList();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+
+ Dialog.closeDialogs();
+ SpinnerDialog.closeDialogs();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ updateAppList();
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ menu.add(Menu.NONE, RESUME_ID, 1, "Resume Session");
+ menu.add(Menu.NONE, QUIT_ID, 2, "Quit Session");
+ }
+
+ @Override
+ public void onContextMenuClosed(Menu menu) {
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
+ AppObject app = (AppObject) appListAdapter.getItem(info.position);
+ switch (item.getItemId())
+ {
+ case RESUME_ID:
+ // Resume is the same as start for us
+ doStart(app.app);
+ return true;
+
+ case QUIT_ID:
+ doQuit(app.app);
+ return true;
+
+ default:
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ private static String generateString(NvApp app) {
+ StringBuilder str = new StringBuilder();
+ str.append(app.getAppName());
+ if (app.getIsRunning()) {
+ str.append(" - Running");
+ }
+ return str.toString();
+ }
+
+ private void addListPlaceholder() {
+ appListAdapter.add(new AppObject("No apps found. Try rescanning for games in GeForce Experience.", null));
+ }
+
+ private void updateAppList() {
+ final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, "App List", "Refreshing app list...", true);
+ new Thread() {
+ @Override
+ public void run() {
+ NvHTTP httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
+
+ try {
+ final List appList = httpConn.getAppList();
+
+ AppView.this.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ appListAdapter.clear();
+ if (appList.isEmpty()) {
+ addListPlaceholder();
+ }
+ else {
+ for (NvApp app : appList) {
+ appListAdapter.add(new AppObject(generateString(app), app));
+ }
+ }
+
+ appListAdapter.notifyDataSetChanged();
+ }
+ });
+
+ // Success case
+ return;
+ } catch (GfeHttpResponseException e) {
+ } catch (IOException e) {
+ } catch (XmlPullParserException e) {
+ } finally {
+ spinner.dismiss();
+ }
+
+ Dialog.displayDialog(AppView.this, "Error", "Failed to get app list", true);
+ }
+ }.start();
+ }
+
+ private void doStart(NvApp app) {
+ Intent intent = new Intent(this, Game.class);
+ intent.putExtra(Game.EXTRA_HOST, ipAddress.getHostAddress());
+ intent.putExtra(Game.EXTRA_APP, app.getAppName());
+ intent.putExtra(Game.EXTRA_UNIQUEID, uniqueId);
+ startActivity(intent);
+ }
+
+ private void doQuit(final NvApp app) {
+ Toast.makeText(AppView.this, "Quitting "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ NvHTTP httpConn;
+ String message;
+ try {
+ httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
+ if (httpConn.quitApp()) {
+ message = "Successfully quit "+app.getAppName();
+ }
+ else {
+ message = "Failed to quit "+app.getAppName();
+ }
+ updateAppList();
+ } catch (UnknownHostException e) {
+ message = "Failed to resolve host";
+ } catch (FileNotFoundException e) {
+ message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
+ + "Try rebooting your machine or reinstalling GFE.";
+ } catch (Exception e) {
+ message = e.getMessage();
+ }
+
+ final String toastMessage = message;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+ }).start();
+ }
+
+ public class AppObject {
+ public String text;
+ public NvApp app;
+
+ public AppObject(String text, NvApp app) {
+ this.text = text;
+ this.app = app;
+ }
+
+ @Override
+ public String toString() {
+ return text;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/limelight/Connection.java b/src/com/limelight/Connection.java
deleted file mode 100644
index 82cff577..00000000
--- a/src/com/limelight/Connection.java
+++ /dev/null
@@ -1,397 +0,0 @@
-package com.limelight;
-
-import java.io.FileNotFoundException;
-import java.net.InetAddress;
-import java.net.SocketException;
-import java.net.UnknownHostException;
-
-import com.limelight.binding.PlatformBinding;
-import com.limelight.nvstream.NvConnection;
-import com.limelight.nvstream.http.NvHTTP;
-import com.limelight.nvstream.http.PairingManager;
-import com.limelight.utils.Dialog;
-
-import android.os.Bundle;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.WindowManager;
-import android.widget.Button;
-import android.widget.CompoundButton;
-import android.widget.CompoundButton.OnCheckedChangeListener;
-import android.widget.RadioButton;
-import android.widget.SeekBar;
-import android.widget.SeekBar.OnSeekBarChangeListener;
-import android.widget.TextView;
-import android.widget.Toast;
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-
-public class Connection extends Activity {
- private Button statusButton, pairButton, quitButton;
- private TextView hostText;
- private SharedPreferences prefs;
- private RadioButton rbutton720p30, rbutton720p60, rbutton1080p30, rbutton1080p60;
- private RadioButton forceSoftDec, autoDec, forceHardDec;
- private SeekBar bitrateSlider;
- private TextView bitrateLabel;
-
- private static final String DEFAULT_HOST = "";
- public static final String HOST_KEY = "hostText";
-
- @Override
- public void onPause() {
- SharedPreferences.Editor editor = prefs.edit();
-
- editor.putString(Connection.HOST_KEY, this.hostText.getText().toString());
- editor.putInt(Game.BITRATE_PREF_STRING, bitrateSlider.getProgress());
- editor.apply();
-
- super.onPause();
- }
-
- @Override
- protected void onStop() {
- super.onStop();
-
- Dialog.closeDialogs();
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.activity_connection);
-
- // Hide the keyboard by default
- this.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
-
- this.statusButton = (Button) findViewById(R.id.statusButton);
- this.pairButton = (Button) findViewById(R.id.pairButton);
- this.quitButton = (Button) findViewById(R.id.quitButton);
- this.hostText = (TextView) findViewById(R.id.hostTextView);
- this.rbutton720p30 = (RadioButton) findViewById(R.id.config720p30Selected);
- this.rbutton720p60 = (RadioButton) findViewById(R.id.config720p60Selected);
- this.rbutton1080p30 = (RadioButton) findViewById(R.id.config1080p30Selected);
- this.rbutton1080p60 = (RadioButton) findViewById(R.id.config1080p60Selected);
- this.forceSoftDec = (RadioButton) findViewById(R.id.softwareDec);
- this.autoDec = (RadioButton) findViewById(R.id.autoDec);
- this.forceHardDec = (RadioButton) findViewById(R.id.hardwareDec);
- this.bitrateLabel = (TextView) findViewById(R.id.bitrateLabel);
- this.bitrateSlider = (SeekBar) findViewById(R.id.bitrateSeekBar);
-
- prefs = getSharedPreferences(Game.PREFS_FILE_NAME, Context.MODE_MULTI_PROCESS);
- this.hostText.setText(prefs.getString(Connection.HOST_KEY, Connection.DEFAULT_HOST));
-
- boolean res720p = prefs.getInt(Game.HEIGHT_PREF_STRING, Game.DEFAULT_HEIGHT) == 720;
- boolean fps30 = prefs.getInt(Game.REFRESH_RATE_PREF_STRING, Game.DEFAULT_REFRESH_RATE) == 30;
-
- bitrateSlider.setMax(Game.BITRATE_CEILING);
- bitrateSlider.setProgress(prefs.getInt(Game.BITRATE_PREF_STRING, Game.DEFAULT_BITRATE));
- updateBitrateLabel();
-
- rbutton720p30.setChecked(false);
- rbutton720p60.setChecked(false);
- rbutton1080p30.setChecked(false);
- rbutton1080p60.setChecked(false);
- if (res720p) {
- if (fps30) {
- rbutton720p30.setChecked(true);
- }
- else {
- rbutton720p60.setChecked(true);
- }
- }
- else {
- if (fps30) {
- rbutton1080p30.setChecked(true);
- }
- else {
- rbutton1080p60.setChecked(true);
- }
- }
-
- switch (prefs.getInt(Game.DECODER_PREF_STRING, Game.DEFAULT_DECODER)) {
- case Game.FORCE_SOFTWARE_DECODER:
- forceSoftDec.setChecked(true);
- autoDec.setChecked(false);
- forceHardDec.setChecked(false);
- break;
- case Game.AUTOSELECT_DECODER:
- forceSoftDec.setChecked(false);
- autoDec.setChecked(true);
- forceHardDec.setChecked(false);
- break;
- case Game.FORCE_HARDWARE_DECODER:
- forceSoftDec.setChecked(false);
- autoDec.setChecked(false);
- forceHardDec.setChecked(true);
- break;
- }
-
- OnCheckedChangeListener occl = new OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(CompoundButton buttonView,
- boolean isChecked) {
- if (!isChecked) {
- // Ignore non-checked buttons
- return;
- }
-
- if (buttonView == rbutton720p30) {
- prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1280).
- putInt(Game.HEIGHT_PREF_STRING, 720).
- putInt(Game.REFRESH_RATE_PREF_STRING, 30).
- putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_720_30).commit();
- bitrateSlider.setProgress(Game.BITRATE_DEFAULT_720_30);
- }
- else if (buttonView == rbutton720p60) {
- prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1280).
- putInt(Game.HEIGHT_PREF_STRING, 720).
- putInt(Game.REFRESH_RATE_PREF_STRING, 60).
- putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_720_60).commit();
- bitrateSlider.setProgress(Game.BITRATE_DEFAULT_720_60);
- }
- else if (buttonView == rbutton1080p30) {
- prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1920).
- putInt(Game.HEIGHT_PREF_STRING, 1080).
- putInt(Game.REFRESH_RATE_PREF_STRING, 30).
- putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_1080_30).commit();
- bitrateSlider.setProgress(Game.BITRATE_DEFAULT_1080_30);
- }
- else if (buttonView == rbutton1080p60) {
- prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1920).
- putInt(Game.HEIGHT_PREF_STRING, 1080).
- putInt(Game.REFRESH_RATE_PREF_STRING, 60).
- putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_1080_60).commit();
- bitrateSlider.setProgress(Game.BITRATE_DEFAULT_1080_60);
- }
- else if (buttonView == forceSoftDec) {
- prefs.edit().putInt(Game.DECODER_PREF_STRING, Game.FORCE_SOFTWARE_DECODER).commit();
- }
- else if (buttonView == forceHardDec) {
- prefs.edit().putInt(Game.DECODER_PREF_STRING, Game.FORCE_HARDWARE_DECODER).commit();
- }
- else if (buttonView == autoDec) {
- prefs.edit().putInt(Game.DECODER_PREF_STRING, Game.AUTOSELECT_DECODER).commit();
- }
- }
- };
- rbutton720p30.setOnCheckedChangeListener(occl);
- rbutton720p60.setOnCheckedChangeListener(occl);
- rbutton1080p30.setOnCheckedChangeListener(occl);
- rbutton1080p60.setOnCheckedChangeListener(occl);
- forceSoftDec.setOnCheckedChangeListener(occl);
- forceHardDec.setOnCheckedChangeListener(occl);
- autoDec.setOnCheckedChangeListener(occl);
-
- this.bitrateSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
- @Override
- public void onProgressChanged(SeekBar seekBar, int progress,
- boolean fromUser) {
-
- // Verify the user's selection
- if (fromUser) {
- int floor;
- if (rbutton720p30.isChecked()) {
- floor = Game.BITRATE_FLOOR_720_30;
- }
- else if (rbutton720p60.isChecked()){
- floor = Game.BITRATE_FLOOR_720_60;
- }
- else if (rbutton1080p30.isChecked()){
- floor = Game.BITRATE_FLOOR_1080_30;
- }
- else /*if (rbutton1080p60.isChecked())*/ {
- floor = Game.BITRATE_FLOOR_1080_60;
- }
-
- if (progress < floor) {
- seekBar.setProgress(floor);
- return;
- }
- }
-
- updateBitrateLabel();
- }
-
- @Override
- public void onStartTrackingTouch(SeekBar seekBar) {
- }
-
- @Override
- public void onStopTrackingTouch(SeekBar seekBar) {
- }
- });
-
- this.statusButton.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View arg0) {
- if (Connection.this.hostText.getText().length() == 0) {
- Toast.makeText(Connection.this, "Please enter the target PC's IP address in the text box at the top of the screen.", Toast.LENGTH_LONG).show();
- return;
- }
-
- // Ensure that the bitrate preference is up to date before
- // starting the game activity
- prefs.edit().
- putInt(Game.BITRATE_PREF_STRING, bitrateSlider.getProgress()).
- commit();
-
- Intent intent = new Intent(Connection.this, Game.class);
- intent.putExtra("host", Connection.this.hostText.getText().toString());
- Connection.this.startActivity(intent);
- }
- });
-
- this.quitButton.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- if (Connection.this.hostText.getText().length() == 0) {
- Toast.makeText(Connection.this, "Please enter the target PC's IP address in the text box at the top of the screen.", Toast.LENGTH_LONG).show();
- return;
- }
-
- Toast.makeText(Connection.this, "Trying to quit Steam...", Toast.LENGTH_SHORT).show();
- new Thread(new Runnable() {
- @Override
- public void run() {
- String macAddress;
- try {
- macAddress = NvConnection.getMacAddressString();
- } catch (SocketException e) {
- e.printStackTrace();
- return;
- }
-
- if (macAddress == null) {
- LimeLog.severe("Couldn't find a MAC address");
- return;
- }
-
- NvHTTP httpConn;
- String message;
- try {
- httpConn = new NvHTTP(InetAddress.getByName(hostText.getText().toString()),
- macAddress, PlatformBinding.getDeviceName(), PlatformBinding.getCryptoProvider(Connection.this));
- if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
- if (httpConn.getCurrentGame() != 0) {
- if (httpConn.quitApp()) {
- message = "Successfully closed Steam";
- }
- else {
- message = "Failed to close Steam";
- }
- }
- else {
- message = "Steam is not running";
- }
- }
- else {
- message = "Device not paired with computer";
- }
- } catch (UnknownHostException e) {
- message = "Failed to resolve host";
- } catch (FileNotFoundException e) {
- message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
- + "Try rebooting your machine or reinstalling GFE.";
- } catch (Exception e) {
- message = e.getMessage();
- }
-
- final String toastMessage = message;
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- Toast.makeText(Connection.this, toastMessage, Toast.LENGTH_LONG).show();
- }
- });
- }
- }).start();
- }
- });
-
- this.pairButton.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View arg0) {
- if (Connection.this.hostText.getText().length() == 0) {
- Toast.makeText(Connection.this, "Please enter the target PC's IP address in the text box at the top of the screen.", Toast.LENGTH_LONG).show();
- return;
- }
-
- Toast.makeText(Connection.this, "Pairing...", Toast.LENGTH_SHORT).show();
- new Thread(new Runnable() {
- @Override
- public void run() {
- String macAddress;
- try {
- macAddress = NvConnection.getMacAddressString();
- } catch (SocketException e) {
- e.printStackTrace();
- return;
- }
-
- if (macAddress == null) {
- LimeLog.severe("Couldn't find a MAC address");
- return;
- }
-
- NvHTTP httpConn;
- String message;
- try {
- httpConn = new NvHTTP(InetAddress.getByName(hostText.getText().toString()),
- macAddress, PlatformBinding.getDeviceName(), PlatformBinding.getCryptoProvider(Connection.this));
- if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
- message = "Already paired";
- }
- else {
- final String pinStr = PairingManager.generatePinString();
-
- // Spin the dialog off in a thread because it blocks
- Dialog.displayDialog(Connection.this, "Pairing", "Please enter the following PIN on the target PC: "+pinStr, false);
-
- PairingManager.PairState pairState = httpConn.pair(pinStr);
- if (pairState == PairingManager.PairState.PIN_WRONG) {
- message = "Incorrect PIN";
- }
- else if (pairState == PairingManager.PairState.FAILED) {
- message = "Pairing failed";
- }
- else if (pairState == PairingManager.PairState.PAIRED) {
- message = "Paired successfully";
- }
- else {
- // Should be no other values
- message = null;
- }
- }
- } catch (UnknownHostException e) {
- message = "Failed to resolve host";
- } catch (FileNotFoundException e) {
- message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
- + "Try rebooting your machine or reinstalling GFE.";
- } catch (Exception e) {
- message = e.getMessage();
- }
-
- Dialog.closeDialogs();
-
- final String toastMessage = message;
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- Toast.makeText(Connection.this, toastMessage, Toast.LENGTH_LONG).show();
- }
- });
- }
- }).start();
- }
- });
-
- }
-
- private void updateBitrateLabel() {
- bitrateLabel.setText("Max Bitrate: "+bitrateSlider.getProgress()+" Mbps");
- }
-}
diff --git a/src/com/limelight/Game.java b/src/com/limelight/Game.java
index 7ed781bd..c359069c 100644
--- a/src/com/limelight/Game.java
+++ b/src/com/limelight/Game.java
@@ -71,6 +71,10 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
private int drFlags = 0;
+ public static final String EXTRA_HOST = "Host";
+ public static final String EXTRA_APP = "App";
+ public static final String EXTRA_UNIQUEID = "UniqueId";
+
public static final String PREFS_FILE_NAME = "gameprefs";
public static final String WIDTH_PREF_STRING = "ResH";
@@ -172,7 +176,9 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
wifiLock.setReferenceCounted(false);
wifiLock.acquire();
- String host = Game.this.getIntent().getStringExtra("host");
+ String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
+ String app = Game.this.getIntent().getStringExtra(EXTRA_APP);
+ String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
InetAddress addr;
boolean enableLargePackets;
try {
@@ -191,8 +197,8 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
LimeLog.info("Using large packets? "+enableLargePackets);
// Start the connection
- conn = new NvConnection(host, Game.this,
- new StreamConfiguration("Steam", width, height, refreshRate, bitrate * 1000,
+ conn = new NvConnection(host, uniqueId, Game.this,
+ new StreamConfiguration(app, width, height, refreshRate, bitrate * 1000,
enableLargePackets ? 1460 : 1024), PlatformBinding.getCryptoProvider(this));
keybTranslator = new KeyboardTranslator(conn);
controllerHandler = new ControllerHandler(conn);
diff --git a/src/com/limelight/PcView.java b/src/com/limelight/PcView.java
new file mode 100644
index 00000000..dccdff4b
--- /dev/null
+++ b/src/com/limelight/PcView.java
@@ -0,0 +1,537 @@
+package com.limelight;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import com.limelight.binding.PlatformBinding;
+import com.limelight.computers.ComputerManagerListener;
+import com.limelight.computers.ComputerManagerService;
+import com.limelight.nvstream.http.ComputerDetails;
+import com.limelight.nvstream.http.NvHTTP;
+import com.limelight.nvstream.http.PairingManager;
+import com.limelight.nvstream.http.PairingManager.PairState;
+import com.limelight.nvstream.wol.WakeOnLanSender;
+import com.limelight.utils.Dialog;
+
+import android.app.Activity;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnClickListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.Toast;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+
+public class PcView extends Activity {
+ private Button settingsButton;
+ private ListView pcList;
+ private ArrayAdapter pcListAdapter;
+ private ComputerManagerService.ComputerManagerBinder managerBinder;
+ private boolean freezeUpdates, runningPolling;
+ private ServiceConnection serviceConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder binder) {
+ final ComputerManagerService.ComputerManagerBinder localBinder =
+ ((ComputerManagerService.ComputerManagerBinder)binder);
+
+ // Wait in a separate thread to avoid stalling the UI
+ new Thread() {
+ @Override
+ public void run() {
+ // Wait for the binder to be ready
+ localBinder.waitForReady();
+
+ // Now make the binder visible
+ managerBinder = localBinder;
+
+ // Start updates
+ startComputerUpdates();
+ }
+ }.start();
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ managerBinder = null;
+ }
+ };
+
+ private final static int APP_LIST_ID = 1;
+ private final static int PAIR_ID = 2;
+ private final static int UNPAIR_ID = 3;
+ private final static int WOL_ID = 4;
+ private final static int DELETE_ID = 5;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_pc_view);
+
+ // Bind to the computer manager service
+ bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
+ Service.BIND_AUTO_CREATE);
+
+ // Setup the list view
+ settingsButton = (Button)findViewById(R.id.settingsButton);
+ pcList = (ListView)findViewById(R.id.pcListView);
+ pcListAdapter = new ArrayAdapter(this, R.layout.simplerow);
+ pcListAdapter.setNotifyOnChange(false);
+ pcList.setAdapter(pcListAdapter);
+ pcList.setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> arg0, View arg1, int pos,
+ long id) {
+ ComputerObject computer = (ComputerObject) pcListAdapter.getItem(pos);
+ if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
+ // Open the context menu if a PC is offline
+ openContextMenu(arg1);
+ }
+ else if (computer.details.pairState != PairState.PAIRED) {
+ // Pair an unpaired machine by default
+ doPair(computer.details);
+ }
+ else {
+ doAppList(computer.details);
+ }
+ }
+ });
+ registerForContextMenu(pcList);
+ settingsButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startActivity(new Intent(PcView.this, StreamSettings.class));
+ }
+ });
+
+ addListPlaceholder();
+ }
+
+ private void startComputerUpdates() {
+ if (managerBinder != null) {
+ if (runningPolling) {
+ return;
+ }
+
+ freezeUpdates = false;
+ managerBinder.startPolling(new ComputerManagerListener() {
+ @Override
+ public void notifyComputerUpdated(final ComputerDetails details) {
+ if (!freezeUpdates) {
+ PcView.this.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ updateListView(details);
+ }
+ });
+ }
+ }
+ });
+ runningPolling = true;
+ }
+ }
+
+ private void stopComputerUpdates() {
+ freezeUpdates = true;
+ managerBinder.stopPolling();
+ runningPolling = false;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ if (managerBinder != null) {
+ unbindService(serviceConnection);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ startComputerUpdates();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ stopComputerUpdates();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+
+ Dialog.closeDialogs();
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ stopComputerUpdates();
+
+ // Call superclass
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
+ ComputerObject computer = (ComputerObject) pcListAdapter.getItem(info.position);
+ if (computer == null || computer.details == null) {
+ startComputerUpdates();
+ return;
+ }
+
+ // Inflate the context menu
+ if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
+ menu.add(Menu.NONE, WOL_ID, 1, "Send Wake-On-LAN request");
+ menu.add(Menu.NONE, DELETE_ID, 2, "Delete PC");
+ }
+ else if (computer.details.pairState != PairState.PAIRED) {
+ menu.add(Menu.NONE, PAIR_ID, 1, "Pair with PC");
+ if (computer.details.reachability == ComputerDetails.Reachability.REMOTE) {
+ menu.add(Menu.NONE, DELETE_ID, 2, "Delete PC");
+ }
+ }
+ else {
+ menu.add(Menu.NONE, APP_LIST_ID, 1, "View Game List");
+ menu.add(Menu.NONE, UNPAIR_ID, 2, "Unpair");
+ }
+ }
+
+ @Override
+ public void onContextMenuClosed(Menu menu) {
+ startComputerUpdates();
+ }
+
+ private void doPair(final ComputerDetails computer) {
+ if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
+ Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Toast.makeText(PcView.this, "Pairing...", Toast.LENGTH_SHORT).show();
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ NvHTTP httpConn;
+ String message;
+ try {
+ InetAddress addr = null;
+ if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
+ addr = computer.localIp;
+ }
+ else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
+ addr = computer.remoteIp;
+ }
+
+ httpConn = new NvHTTP(addr,
+ managerBinder.getUniqueId(),
+ PlatformBinding.getDeviceName(),
+ PlatformBinding.getCryptoProvider(PcView.this));
+ if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
+ message = "Already paired";
+ }
+ else {
+ final String pinStr = PairingManager.generatePinString();
+
+ // Spin the dialog off in a thread because it blocks
+ Dialog.displayDialog(PcView.this, "Pairing", "Please enter the following PIN on the target PC: "+pinStr, false);
+
+ PairingManager.PairState pairState = httpConn.pair(pinStr);
+ if (pairState == PairingManager.PairState.PIN_WRONG) {
+ message = "Incorrect PIN";
+ }
+ else if (pairState == PairingManager.PairState.FAILED) {
+ message = "Pairing failed";
+ }
+ else if (pairState == PairingManager.PairState.PAIRED) {
+ message = "Paired successfully";
+ }
+ else {
+ // Should be no other values
+ message = null;
+ }
+ }
+ } catch (UnknownHostException e) {
+ message = "Failed to resolve host";
+ } catch (FileNotFoundException e) {
+ message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
+ + "Try rebooting your machine or reinstalling GFE.";
+ } catch (Exception e) {
+ message = e.getMessage();
+ }
+
+ Dialog.closeDialogs();
+
+ final String toastMessage = message;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+ }).start();
+ }
+
+ private void doWakeOnLan(final ComputerDetails computer) {
+ if (computer.reachability != ComputerDetails.Reachability.OFFLINE) {
+ Toast.makeText(PcView.this, "Computer is online", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Toast.makeText(PcView.this, "Waking PC...", Toast.LENGTH_SHORT).show();
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ String message;
+ try {
+ WakeOnLanSender.sendWolPacket(computer);
+ message = "It may take a few seconds for your PC to wake up. " +
+ "If it doesn't, make sure it's configured properly for Wake-On-LAN.";
+ } catch (IOException e) {
+ message = "Failed to send wake-on-lan packets";
+ }
+
+ final String toastMessage = message;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+ }).start();
+ }
+
+ private void doUnpair(final ComputerDetails computer) {
+ if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
+ Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Toast.makeText(PcView.this, "Unpairing...", Toast.LENGTH_SHORT).show();
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ NvHTTP httpConn;
+ String message;
+ try {
+ InetAddress addr = null;
+ if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
+ addr = computer.localIp;
+ }
+ else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
+ addr = computer.remoteIp;
+ }
+
+ httpConn = new NvHTTP(addr,
+ managerBinder.getUniqueId(),
+ PlatformBinding.getDeviceName(),
+ PlatformBinding.getCryptoProvider(PcView.this));
+ if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
+ httpConn.unpair();
+ if (httpConn.getPairState() == PairingManager.PairState.NOT_PAIRED) {
+ message = "Unpaired successfully";
+ }
+ else {
+ message = "Failed to unpair";
+ }
+ }
+ else {
+ message = "Device was not paired";
+ }
+ } catch (UnknownHostException e) {
+ message = "Failed to resolve host";
+ } catch (FileNotFoundException e) {
+ message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
+ + "Try rebooting your machine or reinstalling GFE.";
+ } catch (Exception e) {
+ message = e.getMessage();
+ }
+
+ final String toastMessage = message;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+ }).start();
+ }
+
+ private void doAppList(ComputerDetails computer) {
+ if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
+ Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Intent i = new Intent(this, AppView.class);
+ i.putExtra(AppView.NAME_EXTRA, computer.name);
+ i.putExtra(AppView.UNIQUEID_EXTRA, managerBinder.getUniqueId());
+
+ if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
+ i.putExtra(AppView.ADDRESS_EXTRA, computer.localIp.getAddress());
+ }
+ else {
+ i.putExtra(AppView.ADDRESS_EXTRA, computer.remoteIp.getAddress());
+ }
+ startActivity(i);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
+ ComputerObject computer = (ComputerObject) pcListAdapter.getItem(info.position);
+ switch (item.getItemId())
+ {
+ case PAIR_ID:
+ doPair(computer.details);
+ return true;
+
+ case UNPAIR_ID:
+ doUnpair(computer.details);
+ return true;
+
+ case WOL_ID:
+ doWakeOnLan(computer.details);
+ return true;
+
+ case DELETE_ID:
+ if (managerBinder != null) {
+ managerBinder.removeComputer(computer.details.name);
+ }
+ removeListView(computer.details);
+ return true;
+
+ case APP_LIST_ID:
+ doAppList(computer.details);
+ return true;
+
+ default:
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ private static String generateString(ComputerDetails details) {
+ StringBuilder str = new StringBuilder();
+ str.append(details.name).append(" - ");
+ if (details.state == ComputerDetails.State.ONLINE) {
+ str.append("Online ");
+ if (details.reachability == ComputerDetails.Reachability.LOCAL) {
+ str.append("(Local) - ");
+ }
+ else {
+ str.append("(Remote) - ");
+ }
+ if (details.pairState == PairState.PAIRED) {
+ if (details.runningGameId == 0) {
+ str.append("Available");
+ }
+ else {
+ str.append("In Game");
+ }
+ }
+ else {
+ str.append("Not Paired");
+ }
+ }
+ else {
+ str.append("Offline");
+ }
+ return str.toString();
+ }
+
+ private void addListPlaceholder() {
+ pcListAdapter.add(new ComputerObject("No computers found yet. Make sure your computer is running GFE " +
+ "or add your PC manually on the settings page.", null));
+ }
+
+ private void removeListView(ComputerDetails details) {
+ for (int i = 0; i < pcListAdapter.getCount(); i++) {
+ ComputerObject computer = pcListAdapter.getItem(i);
+
+ if (details.equals(computer.details)) {
+ pcListAdapter.remove(computer);
+ break;
+ }
+ }
+
+ if (pcListAdapter.getCount() == 0) {
+ // Add the placeholder if we're down to 0 computers
+ addListPlaceholder();
+ }
+ }
+
+ private void updateListView(ComputerDetails details) {
+ String computerString = generateString(details);
+ ComputerObject existingEntry = null;
+ boolean placeholderPresent = false;
+
+ for (int i = 0; i < pcListAdapter.getCount(); i++) {
+ ComputerObject computer = pcListAdapter.getItem(i);
+
+ // If there's a placeholder, there's nothing else
+ if (computer.details == null) {
+ placeholderPresent = true;
+ break;
+ }
+
+ if (computer.text.equals(computerString)) {
+ // Already up to date
+ return;
+ }
+
+ // Check if this is the same computer
+ if (details.equals(computer.details)) {
+ existingEntry = computer;
+ break;
+ }
+ }
+
+ if (existingEntry != null) {
+ // Replace the information in the existing entry
+ existingEntry.text = computerString;
+ existingEntry.details = details;
+ }
+ else {
+ // If the placeholder is the only object, remove it
+ if (placeholderPresent) {
+ pcListAdapter.remove(pcListAdapter.getItem(0));
+ }
+
+ // Add a new entry
+ pcListAdapter.add(new ComputerObject(computerString, details));
+ }
+
+ // Notify the view that the data has changed
+ pcListAdapter.notifyDataSetChanged();
+ }
+
+ public class ComputerObject {
+ public String text;
+ public ComputerDetails details;
+
+ public ComputerObject(String text, ComputerDetails details) {
+ this.text = text;
+ this.details = details;
+ }
+
+ @Override
+ public String toString() {
+ return text;
+ }
+ }
+}
diff --git a/src/com/limelight/StreamSettings.java b/src/com/limelight/StreamSettings.java
new file mode 100644
index 00000000..4ff25255
--- /dev/null
+++ b/src/com/limelight/StreamSettings.java
@@ -0,0 +1,123 @@
+package com.limelight;
+
+import com.limelight.utils.Dialog;
+
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.RadioButton;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+
+public class StreamSettings extends Activity {
+ private Button advancedSettingsButton, addComputerButton;
+ private SharedPreferences prefs;
+ private RadioButton rbutton720p30, rbutton720p60, rbutton1080p30, rbutton1080p60;
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+
+ Dialog.closeDialogs();
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_stream_settings);
+
+ this.advancedSettingsButton = (Button) findViewById(R.id.advancedSettingsButton);
+ this.addComputerButton = (Button) findViewById(R.id.manuallyAddPc);
+ this.rbutton720p30 = (RadioButton) findViewById(R.id.config720p30Selected);
+ this.rbutton720p60 = (RadioButton) findViewById(R.id.config720p60Selected);
+ this.rbutton1080p30 = (RadioButton) findViewById(R.id.config1080p30Selected);
+ this.rbutton1080p60 = (RadioButton) findViewById(R.id.config1080p60Selected);
+
+ prefs = getSharedPreferences(Game.PREFS_FILE_NAME, Context.MODE_MULTI_PROCESS);
+
+ boolean res720p = prefs.getInt(Game.HEIGHT_PREF_STRING, Game.DEFAULT_HEIGHT) == 720;
+ boolean fps30 = prefs.getInt(Game.REFRESH_RATE_PREF_STRING, Game.DEFAULT_REFRESH_RATE) == 30;
+
+ rbutton720p30.setChecked(false);
+ rbutton720p60.setChecked(false);
+ rbutton1080p30.setChecked(false);
+ rbutton1080p60.setChecked(false);
+ if (res720p) {
+ if (fps30) {
+ rbutton720p30.setChecked(true);
+ }
+ else {
+ rbutton720p60.setChecked(true);
+ }
+ }
+ else {
+ if (fps30) {
+ rbutton1080p30.setChecked(true);
+ }
+ else {
+ rbutton1080p60.setChecked(true);
+ }
+ }
+
+ OnCheckedChangeListener occl = new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView,
+ boolean isChecked) {
+ if (!isChecked) {
+ // Ignore non-checked buttons
+ return;
+ }
+
+ if (buttonView == rbutton720p30) {
+ prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1280).
+ putInt(Game.HEIGHT_PREF_STRING, 720).
+ putInt(Game.REFRESH_RATE_PREF_STRING, 30).
+ putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_720_30).commit();
+ }
+ else if (buttonView == rbutton720p60) {
+ prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1280).
+ putInt(Game.HEIGHT_PREF_STRING, 720).
+ putInt(Game.REFRESH_RATE_PREF_STRING, 60).
+ putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_720_60).commit();
+ }
+ else if (buttonView == rbutton1080p30) {
+ prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1920).
+ putInt(Game.HEIGHT_PREF_STRING, 1080).
+ putInt(Game.REFRESH_RATE_PREF_STRING, 30).
+ putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_1080_30).commit();
+ }
+ else if (buttonView == rbutton1080p60) {
+ prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1920).
+ putInt(Game.HEIGHT_PREF_STRING, 1080).
+ putInt(Game.REFRESH_RATE_PREF_STRING, 60).
+ putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_1080_60).commit();
+ }
+ }
+ };
+ rbutton720p30.setOnCheckedChangeListener(occl);
+ rbutton720p60.setOnCheckedChangeListener(occl);
+ rbutton1080p30.setOnCheckedChangeListener(occl);
+ rbutton1080p60.setOnCheckedChangeListener(occl);
+
+ advancedSettingsButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent i = new Intent(StreamSettings.this, AdvancedSettings.class);
+ startActivity(i);
+ }
+ });
+ addComputerButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent i = new Intent(StreamSettings.this, AddComputerManually.class);
+ startActivity(i);
+ }
+ });
+ }
+}
diff --git a/src/com/limelight/computers/ComputerDatabaseManager.java b/src/com/limelight/computers/ComputerDatabaseManager.java
new file mode 100644
index 00000000..46bd61ee
--- /dev/null
+++ b/src/com/limelight/computers/ComputerDatabaseManager.java
@@ -0,0 +1,159 @@
+package com.limelight.computers;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import com.limelight.LimeLog;
+import com.limelight.nvstream.http.ComputerDetails;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+
+public class ComputerDatabaseManager {
+ private static final String COMPUTER_DB_NAME = "computers.db";
+ private static final String COMPUTER_TABLE_NAME = "Computers";
+ private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
+ private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
+ private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
+ private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
+ private static final String MAC_COLUMN_NAME = "Mac";
+
+ private SQLiteDatabase computerDb;
+
+ public ComputerDatabaseManager(Context c) {
+ try {
+ // Create or open an existing DB
+ computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
+ } catch (SQLiteException e) {
+ // Delete the DB and try again
+ c.deleteDatabase(COMPUTER_DB_NAME);
+ computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
+ }
+ computerDb.enableWriteAheadLogging();
+ initializeDb();
+ }
+
+ public void close() {
+ computerDb.close();
+ }
+
+ private void initializeDb() {
+ // Create tables if they aren't already there
+ computerDb.execSQL(String.format("CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," +
+ " %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
+ COMPUTER_TABLE_NAME,
+ COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
+ REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME));
+ }
+
+ public void deleteComputer(String name) {
+ computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
+ }
+
+ public boolean updateComputer(ComputerDetails details) {
+ ContentValues values = new ContentValues();
+ values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
+ values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
+ values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
+ values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
+ values.put(MAC_COLUMN_NAME, details.macAddress);
+ return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
+ }
+
+ public List getAllComputers() {
+ Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
+ LinkedList computerList = new LinkedList();
+ while (c.moveToNext()) {
+ ComputerDetails details = new ComputerDetails();
+
+ details.name = c.getString(0);
+
+ String uuidStr = c.getString(1);
+ try {
+ details.uuid = UUID.fromString(uuidStr);
+ } catch (IllegalArgumentException e) {
+ // We'll delete this entry
+ LimeLog.severe("DB: Corrupted UUID for "+details.name);
+ }
+
+ try {
+ details.localIp = InetAddress.getByAddress(c.getBlob(2));
+ } catch (UnknownHostException e) {
+ // We'll delete this entry
+ LimeLog.severe("DB: Corrupted local IP for "+details.name);
+ }
+
+ try {
+ details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
+ } catch (UnknownHostException e) {
+ // We'll delete this entry
+ LimeLog.severe("DB: Corrupted remote IP for "+details.name);
+ }
+
+ details.macAddress = c.getString(4);
+
+ // This signifies we don't have dynamic state (like pair state)
+ details.state = ComputerDetails.State.UNKNOWN;
+
+ // If a field is corrupt or missing, skip the database entry
+ if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
+ details.macAddress == null) {
+ continue;
+ }
+
+ computerList.add(details);
+ }
+
+ return computerList;
+ }
+
+ public ComputerDetails getComputerByName(String name) {
+ Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
+ ComputerDetails details = new ComputerDetails();
+ if (!c.moveToFirst()) {
+ // No matching computer
+ return null;
+ }
+
+ details.name = c.getString(0);
+
+ String uuidStr = c.getString(1);
+ try {
+ details.uuid = UUID.fromString(uuidStr);
+ } catch (IllegalArgumentException e) {
+ // We'll delete this entry
+ LimeLog.severe("DB: Corrupted UUID for "+details.name);
+ }
+
+ try {
+ details.localIp = InetAddress.getByAddress(c.getBlob(2));
+ } catch (UnknownHostException e) {
+ // We'll delete this entry
+ LimeLog.severe("DB: Corrupted local IP for "+details.name);
+ }
+
+ try {
+ details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
+ } catch (UnknownHostException e) {
+ // We'll delete this entry
+ LimeLog.severe("DB: Corrupted remote IP for "+details.name);
+ }
+
+ details.macAddress = c.getString(4);
+
+ // If a field is corrupt or missing, delete the database entry
+ if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
+ details.macAddress == null) {
+ deleteComputer(details.name);
+ return null;
+ }
+
+ return details;
+ }
+}
diff --git a/src/com/limelight/computers/ComputerManagerListener.java b/src/com/limelight/computers/ComputerManagerListener.java
new file mode 100644
index 00000000..51e21a0a
--- /dev/null
+++ b/src/com/limelight/computers/ComputerManagerListener.java
@@ -0,0 +1,7 @@
+package com.limelight.computers;
+
+import com.limelight.nvstream.http.ComputerDetails;
+
+public interface ComputerManagerListener {
+ public void notifyComputerUpdated(ComputerDetails details);
+}
diff --git a/src/com/limelight/computers/ComputerManagerService.java b/src/com/limelight/computers/ComputerManagerService.java
new file mode 100644
index 00000000..3fec22fd
--- /dev/null
+++ b/src/com/limelight/computers/ComputerManagerService.java
@@ -0,0 +1,279 @@
+package com.limelight.computers;
+
+import java.net.InetAddress;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import com.limelight.LimeLog;
+import com.limelight.binding.PlatformBinding;
+import com.limelight.discovery.DiscoveryService;
+import com.limelight.nvstream.http.ComputerDetails;
+import com.limelight.nvstream.http.NvHTTP;
+import com.limelight.nvstream.mdns.MdnsComputer;
+import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
+
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Binder;
+import android.os.IBinder;
+
+public class ComputerManagerService extends Service {
+ private static final int MAX_CONCURRENT_REQUESTS = 4;
+ private static final int POLLING_PERIOD_MS = 5000;
+ private static final int MDNS_QUERY_PERIOD_MS = 1000;
+
+ private ComputerManagerBinder binder = new ComputerManagerBinder();
+
+ private ComputerDatabaseManager dbManager;
+ private IdentityManager idManager;
+ private ThreadPoolExecutor pollingPool;
+ private Timer pollingTimer;
+ private ComputerManagerListener listener = null;
+
+ private DiscoveryService.DiscoveryBinder discoveryBinder;
+ private ServiceConnection discoveryServiceConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder binder) {
+ synchronized (discoveryServiceConnection) {
+ discoveryBinder = ((DiscoveryService.DiscoveryBinder)binder);
+
+ // Set us as the event listener
+ discoveryBinder.setListener(createDiscoveryListener());
+
+ // Signal a possible waiter that we're all setup
+ discoveryServiceConnection.notifyAll();
+ }
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ discoveryBinder = null;
+ }
+ };
+
+ public class ComputerManagerBinder extends Binder {
+ public void startPolling(ComputerManagerListener listener) {
+ // Set the listener
+ ComputerManagerService.this.listener = listener;
+
+ // Start mDNS autodiscovery too
+ discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS);
+
+ // Start polling known machines
+ pollingTimer = new Timer();
+ pollingTimer.schedule(getTimerTask(), 0, POLLING_PERIOD_MS);
+ }
+
+ public void waitForReady() {
+ synchronized (discoveryServiceConnection) {
+ try {
+ while (discoveryBinder == null) {
+ // Wait for the bind notification
+ discoveryServiceConnection.wait(1000);
+ }
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+
+ public boolean addComputerBlocking(InetAddress addr) {
+ return ComputerManagerService.this.addComputerBlocking(addr);
+ }
+
+ public void addComputer(InetAddress addr) {
+ ComputerManagerService.this.addComputer(addr);
+ }
+
+ public void removeComputer(String name) {
+ ComputerManagerService.this.removeComputer(name);
+ }
+
+ public void stopPolling() {
+ // Just call the unbind handler to cleanup
+ ComputerManagerService.this.onUnbind(null);
+ }
+
+ public String getUniqueId() {
+ return idManager.getUniqueId();
+ }
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ // Stop mDNS autodiscovery
+ discoveryBinder.stopDiscovery();
+
+ // Stop polling
+ if (pollingTimer != null) {
+ pollingTimer.cancel();
+ pollingTimer = null;
+ }
+
+ // Remove the listener
+ listener = null;
+
+ return false;
+ }
+
+ private MdnsDiscoveryListener createDiscoveryListener() {
+ return new MdnsDiscoveryListener() {
+ @Override
+ public void notifyComputerAdded(MdnsComputer computer) {
+ LimeLog.severe("Added computer: "+computer.getName());
+ // Kick off a serverinfo poll on this machine
+ addComputer(computer.getAddress());
+ }
+
+ @Override
+ public void notifyComputerRemoved(MdnsComputer computer) {
+ // Nothing to do here
+ }
+
+ @Override
+ public void notifyDiscoveryFailure(Exception e) {
+ LimeLog.severe("mDNS discovery failed");
+ e.printStackTrace();
+ }
+ };
+ }
+
+ public void addComputer(InetAddress addr) {
+ // Setup a placeholder
+ ComputerDetails fakeDetails = new ComputerDetails();
+ fakeDetails.localIp = addr;
+ fakeDetails.remoteIp = addr;
+
+ // Put it in the thread pool to process later
+ pollingPool.execute(getPollingRunnable(fakeDetails));
+ }
+
+ public boolean addComputerBlocking(InetAddress addr) {
+ // Setup a placeholder
+ ComputerDetails fakeDetails = new ComputerDetails();
+ fakeDetails.localIp = addr;
+ fakeDetails.remoteIp = addr;
+
+ // Block while we try to fill the details
+ getPollingRunnable(fakeDetails).run();
+
+ // If the machine is reachable, it was successful
+ return fakeDetails.state == ComputerDetails.State.ONLINE;
+ }
+
+ public void removeComputer(String name) {
+ // Remove it from the database
+ dbManager.deleteComputer(name);
+ }
+
+ private TimerTask getTimerTask() {
+ return new TimerTask() {
+ @Override
+ public void run() {
+ List computerList = dbManager.getAllComputers();
+ for (ComputerDetails computer : computerList) {
+ pollingPool.execute(getPollingRunnable(computer));
+ }
+ }
+ };
+ }
+
+ private Runnable getPollingRunnable(final ComputerDetails details) {
+ return new Runnable() {
+
+ @Override
+ public void run() {
+ boolean newPc = details.name == null;
+
+ try {
+ // Try the local IP first
+ NvHTTP http = new NvHTTP(details.localIp, idManager.getUniqueId(),
+ null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
+
+ ComputerDetails localDetails = http.getComputerDetails();
+
+ // If we got here, it's reachable
+ details.reachability = ComputerDetails.Reachability.LOCAL;
+ details.update(localDetails);
+ } catch (Exception e) {
+ // This isn't horrible yet; we'll try remote
+ try {
+ NvHTTP http = new NvHTTP(details.remoteIp, idManager.getUniqueId(),
+ null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
+
+ ComputerDetails remoteDetails = http.getComputerDetails();
+
+ // If we got here, it's reachable
+ details.reachability = ComputerDetails.Reachability.REMOTE;
+ details.update(remoteDetails);
+ } catch (Exception e1) {
+ // No good, it's offline
+ details.state = ComputerDetails.State.OFFLINE;
+ details.reachability = ComputerDetails.Reachability.OFFLINE;
+ }
+ }
+
+ // If it's online, update our persistent state
+ if (details.state == ComputerDetails.State.ONLINE) {
+ if (!newPc) {
+ // Check if it's in the database because it could have been
+ // removed after this was issued
+ if (dbManager.getComputerByName(details.name) == null) {
+ // It's gone
+ return;
+ }
+ }
+
+ dbManager.updateComputer(details);
+ }
+
+ // Update anyone listening
+ if (listener != null) {
+ listener.notifyComputerUpdated(details);
+ }
+ }
+ };
+ }
+
+ @Override
+ public void onCreate() {
+ // Bind to the discovery service
+ bindService(new Intent(this, DiscoveryService.class),
+ discoveryServiceConnection, Service.BIND_AUTO_CREATE);
+
+ // Create the thread pool for updating computer state
+ pollingPool = new ThreadPoolExecutor(1, MAX_CONCURRENT_REQUESTS, Long.MAX_VALUE, TimeUnit.DAYS,
+ new LinkedBlockingQueue(), new ThreadPoolExecutor.DiscardPolicy());
+
+ // Lookup or generate this device's UID
+ idManager = new IdentityManager(this);
+
+ // Initialize the DB
+ dbManager = new ComputerDatabaseManager(this);
+ }
+
+ @Override
+ public void onDestroy() {
+ if (discoveryBinder != null) {
+ // Unbind from the discovery service
+ unbindService(discoveryServiceConnection);
+ }
+
+ // Stop the thread pool
+ pollingPool.shutdownNow();
+ try {
+ pollingPool.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
+ } catch (InterruptedException e) {}
+
+ // Close the DB
+ dbManager.close();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return binder;
+ }
+}
diff --git a/src/com/limelight/computers/IdentityManager.java b/src/com/limelight/computers/IdentityManager.java
new file mode 100644
index 00000000..db463852
--- /dev/null
+++ b/src/com/limelight/computers/IdentityManager.java
@@ -0,0 +1,85 @@
+package com.limelight.computers;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.util.Random;
+
+import com.limelight.LimeLog;
+
+import android.content.Context;
+
+public class IdentityManager {
+ private static final String UNIQUE_ID_FILE_NAME = "uniqueid";
+ private static final int UID_SIZE_IN_BYTES = 8;
+
+ private String uniqueId;
+
+ public IdentityManager(Context c) {
+ uniqueId = loadUniqueId(c);
+ if (uniqueId == null) {
+ uniqueId = generateNewUniqueId(c);
+ }
+
+ LimeLog.info("UID is now: "+uniqueId);
+ }
+
+ public String getUniqueId() {
+ return uniqueId;
+ }
+
+ private static String loadUniqueId(Context c) {
+ // 2 Hex digits per byte
+ char[] uid = new char[UID_SIZE_IN_BYTES * 2];
+ InputStreamReader reader = null;
+ LimeLog.info("Reading UID from disk");
+ try {
+ reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME));
+ if (reader.read(uid) != UID_SIZE_IN_BYTES * 2)
+ {
+ LimeLog.severe("UID file data is truncated");
+ return null;
+ }
+ return new String(uid);
+ } catch (FileNotFoundException e) {
+ LimeLog.info("No UID file found");
+ return null;
+ } catch (IOException e) {
+ LimeLog.severe("Error while reading UID file");
+ e.printStackTrace();
+ return null;
+ } finally {
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException e) {}
+ }
+ }
+ }
+
+ private static String generateNewUniqueId(Context c) {
+ // Generate a new UID hex string
+ LimeLog.info("Generating new UID");
+ String uidStr = String.format("%016x", new Random().nextLong());
+
+ OutputStreamWriter writer = null;
+ try {
+ writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0));
+ writer.write(uidStr);
+ LimeLog.info("UID written to disk");
+ } catch (IOException e) {
+ LimeLog.severe("Error while writing UID file");
+ e.printStackTrace();
+ } finally {
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException e) {}
+ }
+ }
+
+ // We can return a UID even if I/O fails
+ return uidStr;
+ }
+}
diff --git a/src/com/limelight/discovery/DiscoveryService.java b/src/com/limelight/discovery/DiscoveryService.java
index 6f41a981..108cf8e2 100644
--- a/src/com/limelight/discovery/DiscoveryService.java
+++ b/src/com/limelight/discovery/DiscoveryService.java
@@ -77,6 +77,7 @@ public class DiscoveryService extends Service {
}
});
} catch (IOException e) {
+ e.printStackTrace();
discoveryAgent = null;
}
}