Compare commits

...
Author SHA1 Message Date
Cameron Gutman 5519d92243 Disable the start key shortcut to start the keyboard because the keyboard can't receive input after it's started 2015-02-07 13:58:53 -05:00
Cameron Gutman 3d95ac1f93 Fix keyboard dismissal on Fire TV devices 2015-02-07 13:42:49 -05:00
Cameron Gutman 5c938535be Fix app list focus issues with remotes/gamepads 2015-02-07 13:20:01 -05:00
Cameron Gutman 2fdecc551a Tabs -> Spaces 2015-02-07 11:54:46 -05:00
Cameron Gutman 10204afdb4 Only add PCs to the computer list when they have been polled once to get a UUID for equality comparison. Fix equality comparison in PcView to avoid duplicate PCs enumerated over mDNS. 2015-02-07 11:44:56 -05:00
Cameron Gutman 55c800c2a5 Fade in box art when scrolling 2015-02-07 06:52:28 -05:00
Cameron Gutman 265b3f9963 Use image alpha to make images transparent while loading 2015-02-07 06:23:35 -05:00
Cameron Gutman a8bf2cd1cf Fix UI dropped frames when loading images 2015-02-07 06:08:00 -05:00
Cameron Gutman 4fcd8b3dfe Replace unpair option with delete PC 2015-02-07 05:57:30 -05:00
Cameron Gutman e1a1a6344d Fill the whole height with the list view 2015-02-06 13:38:32 -05:00
Cameron Gutman a095c10a25 Increment version to 3.1 and update build files 2015-02-05 16:15:16 -05:00
Cameron Gutman b1ea487e22 Use the mode (power) button on the Asus Nexus Player Gamepad as a select button 2015-02-05 16:06:55 -05:00
Cameron Gutman 47265d0d10 Add another SELinux policy change needed on Nexus 9 2015-02-05 16:06:22 -05:00
Cameron Gutman 6a41b41a38 Merge pull request #55 from Ansa89/italian-translation
Italian translation: update
2015-02-05 13:34:03 -05:00
Cameron Gutman 2247e43a48 Remove unused imports 2015-02-05 13:23:01 -05:00
Cameron Gutman d3986080a3 Tighten up a bunch of declarations to make Lint happier 2015-02-05 13:21:04 -05:00
Cameron Gutman 07277e1a5b Fix a few Lint warnings 2015-02-05 13:01:35 -05:00
Ansa89 39d7fc748f Italian translation: update 2015-02-03 01:06:36 +01:00
Cameron Gutman 4d3a69cf6a Fix GFE 2.1.x controller regression 2015-02-02 18:10:18 -05:00
Cameron Gutman b806522751 Unassign the controller number when a device is removed 2015-02-02 02:13:27 -05:00
Cameron Gutman 256fa897a7 Fix build issues 2015-02-01 18:31:34 -05:00
Cameron Gutman 5c812eed6c Beta 2 version update 2015-02-01 18:20:55 -05:00
Cameron Gutman f0b22f9119 Don't use small mode on TVs 2015-02-01 18:20:39 -05:00
Cameron Gutman 7e1884acb5 Trap Shield's back button as controller 0 2015-02-01 18:07:03 -05:00
Cameron Gutman 9512521783 Update common 2015-02-01 15:06:27 -05:00
Cameron Gutman da7904a767 Add multiple controller support 2015-02-01 15:06:18 -05:00
46 changed files with 4508 additions and 4404 deletions

No files matched your search

+2
View File
@@ -9,6 +9,7 @@
<facet type="android" name="Android"> <facet type="android" name="Android">
<configuration> <configuration>
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" /> <option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" /> <option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" /> <option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" /> <option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" />
@@ -24,6 +25,7 @@
</component> </component>
<component name="NewModuleRootManager" inherit-compiler-output="false"> <component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" /> <output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/test/nonRoot/debug" />
<exclude-output /> <exclude-output />
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
+10 -10
View File
@@ -11,8 +11,8 @@ android {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 21 targetSdkVersion 21
versionName "3.1-beta1" versionName "3.1"
versionCode = 50 versionCode = 53
} }
productFlavors { productFlavors {
@@ -62,17 +62,17 @@ android {
} }
dependencies { dependencies {
compile group: 'org.jcodec', name: 'jcodec', version: '+' compile group: 'org.jcodec', name: 'jcodec', version: '0.1.9'
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '+' compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '+' compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
compile group: 'com.google.android', name: 'support-v4', version:'+' compile group: 'com.google.android', name: 'support-v4', version:'r7'
compile group: 'com.koushikdutta.ion', name: 'ion', version:'+' compile group: 'com.koushikdutta.ion', name: 'ion', version:'2.0.5'
compile group: 'com.google.code.gson', name: 'gson', version:'+' compile group: 'com.google.code.gson', name: 'gson', version:'2.3.1'
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'+' compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.2.0'
compile group: 'com.squareup.okio', name:'okio', version:'+' compile group: 'com.squareup.okio', name:'okio', version:'1.2.0'
compile files('libs/jmdns-fixed.jar') compile files('libs/jmdns-fixed.jar')
compile files('libs/limelight-common.jar') compile files('libs/limelight-common.jar')
Binary file not shown.
+114 -121
View File
@@ -1,9 +1,6 @@
package com.limelight; package com.limelight;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader; import java.io.StringReader;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
@@ -11,15 +8,11 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.UUID; import java.util.UUID;
import org.xmlpull.v1.XmlPullParserException;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.binding.crypto.AndroidCryptoProvider;
import com.limelight.computers.ComputerManagerListener; import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService; import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.AppGridAdapter; import com.limelight.grid.AppGridAdapter;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.NvApp; import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.NvHTTP;
import com.limelight.preferences.PreferenceConfiguration; import com.limelight.preferences.PreferenceConfiguration;
@@ -48,7 +41,6 @@ import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AbsListView; import android.widget.AbsListView;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.AdapterContextMenuInfo;
@@ -65,16 +57,16 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
private int consecutiveAppListFailures = 0; private int consecutiveAppListFailures = 0;
private final static int CONSECUTIVE_FAILURE_LIMIT = 3; private final static int CONSECUTIVE_FAILURE_LIMIT = 3;
private final static int START_OR_RESUME_ID = 1; private final static int START_OR_RESUME_ID = 1;
private final static int QUIT_ID = 2; private final static int QUIT_ID = 2;
private final static int CANCEL_ID = 3; private final static int CANCEL_ID = 3;
private final static int START_WTIH_QUIT = 4; private final static int START_WTIH_QUIT = 4;
public final static String NAME_EXTRA = "Name"; public final static String NAME_EXTRA = "Name";
public final static String UUID_EXTRA = "UUID"; public final static String UUID_EXTRA = "UUID";
private ComputerManagerService.ComputerManagerBinder managerBinder; private ComputerManagerService.ComputerManagerBinder managerBinder;
private ServiceConnection serviceConnection = new ServiceConnection() { private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) { public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder = final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder); ((ComputerManagerService.ComputerManagerBinder)binder);
@@ -172,7 +164,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
blockingLoadSpinner.dismiss(); blockingLoadSpinner.dismiss();
blockingLoadSpinner = null; blockingLoadSpinner = null;
} }
} catch (Exception e) {} } catch (Exception ignored) {}
} }
}); });
@@ -191,33 +183,33 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
managerBinder.stopPolling(); managerBinder.stopPolling();
} }
} }
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language; String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) { if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration()); Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale); config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics()); getResources().updateConfiguration(config, getResources().getDisplayMetrics());
} }
setContentView(R.layout.activity_app_view); setContentView(R.layout.activity_app_view);
UiHelper.notifyNewRootView(this); UiHelper.notifyNewRootView(this);
uuidString = getIntent().getStringExtra(UUID_EXTRA); uuidString = getIntent().getStringExtra(UUID_EXTRA);
String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA); String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA);
TextView label = (TextView) findViewById(R.id.appListText); TextView label = (TextView) findViewById(R.id.appListText);
setTitle(labelText); setTitle(labelText);
label.setText(labelText); label.setText(labelText);
// Bind to the computer manager service // Bind to the computer manager service
bindService(new Intent(this, ComputerManagerService.class), serviceConnection, bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE); Service.BIND_AUTO_CREATE);
} }
private void populateAppGridWithCache() { private void populateAppGridWithCache() {
try { try {
@@ -241,25 +233,25 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title), blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
getResources().getString(R.string.applist_refresh_msg), true); getResources().getString(R.string.applist_refresh_msg), true);
} }
@Override @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
SpinnerDialog.closeDialogs(this); SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs(); Dialog.closeDialogs();
if (managerBinder != null) { if (managerBinder != null) {
unbindService(serviceConnection); unbindService(serviceConnection);
} }
} }
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
startComputerUpdates(); startComputerUpdates();
} }
@Override @Override
protected void onPause() { protected void onPause() {
@@ -267,49 +259,49 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
stopComputerUpdates(); stopComputerUpdates();
} }
private int getRunningAppId() { private int getRunningAppId() {
int runningAppId = -1; int runningAppId = -1;
for (int i = 0; i < appGridAdapter.getCount(); i++) { for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject app = (AppObject) appGridAdapter.getItem(i); AppObject app = (AppObject) appGridAdapter.getItem(i);
if (app.app == null) { if (app.app == null) {
continue; continue;
} }
if (app.app.getIsRunning()) { if (app.app.getIsRunning()) {
runningAppId = app.app.getAppId(); runningAppId = app.app.getAppId();
break; break;
} }
} }
return runningAppId; return runningAppId;
} }
@Override @Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo); super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position); AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
if (selectedApp == null || selectedApp.app == null) { if (selectedApp == null || selectedApp.app == null) {
return; return;
} }
int runningAppId = getRunningAppId(); int runningAppId = getRunningAppId();
if (runningAppId != -1) { if (runningAppId != -1) {
if (runningAppId == selectedApp.app.getAppId()) { if (runningAppId == selectedApp.app.getAppId()) {
menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume)); menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit)); menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
} }
else { else {
menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start)); menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel)); menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
} }
} }
} }
@Override @Override
public void onContextMenuClosed(Menu menu) { public void onContextMenuClosed(Menu menu) {
} }
private void displayQuitConfirmationDialog(final Runnable onYes, final Runnable onNo) { private void displayQuitConfirmationDialog(final Runnable onYes, final Runnable onNo) {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@@ -422,57 +414,57 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
}); });
} }
private void doStart(NvApp app) { private void doStart(NvApp app) {
Intent intent = new Intent(this, Game.class); Intent intent = new Intent(this, Game.class);
intent.putExtra(Game.EXTRA_HOST, intent.putExtra(Game.EXTRA_HOST,
computer.reachability == ComputerDetails.Reachability.LOCAL ? computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress()); computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
intent.putExtra(Game.EXTRA_APP, app.getAppName()); intent.putExtra(Game.EXTRA_APP, app.getAppName());
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId()); intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
intent.putExtra(Game.EXTRA_STREAMING_REMOTE, intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
computer.reachability != ComputerDetails.Reachability.LOCAL); computer.reachability != ComputerDetails.Reachability.LOCAL);
startActivity(intent); startActivity(intent);
} }
private void doQuit(final NvApp app) { private void doQuit(final NvApp app) {
Toast.makeText(AppView.this, getResources().getString(R.string.applist_quit_app)+" "+app.getAppName()+"...", Toast.LENGTH_SHORT).show(); Toast.makeText(AppView.this, getResources().getString(R.string.applist_quit_app)+" "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
NvHTTP httpConn; NvHTTP httpConn;
String message; String message;
try { try {
httpConn = new NvHTTP(getAddress(), httpConn = new NvHTTP(getAddress(),
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(AppView.this)); managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(AppView.this));
if (httpConn.quitApp()) { if (httpConn.quitApp()) {
message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName(); message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName();
} }
else { else {
message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName(); message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName();
} }
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
message = getResources().getString(R.string.error_unknown_host); message = getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
message = getResources().getString(R.string.error_404); message = getResources().getString(R.string.error_404);
} catch (Exception e) { } catch (Exception e) {
message = e.getMessage(); message = e.getMessage();
} finally { } finally {
// Trigger a poll immediately // Trigger a poll immediately
if (poller != null) { if (poller != null) {
poller.pollNow(); poller.pollNow();
} }
} }
final String toastMessage = message; final String toastMessage = message;
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show(); Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
} }
}); });
} }
}).start(); }).start();
} }
@Override @Override
public int getAdapterFragmentLayoutId() { public int getAdapterFragmentLayoutId() {
@@ -502,18 +494,19 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
} }
}); });
registerForContextMenu(listView); registerForContextMenu(listView);
listView.requestFocus();
} }
public class AppObject { public class AppObject {
public NvApp app; public final NvApp app;
public AppObject(NvApp app) { public AppObject(NvApp app) {
this.app = app; this.app = app;
} }
@Override @Override
public String toString() { public String toString() {
return app.getAppName(); return app.getAppName();
} }
} }
} }
File diff suppressed because it is too large. Load diff
+406 -404
View File
@@ -25,7 +25,6 @@ import com.limelight.utils.Dialog;
import com.limelight.utils.UiHelper; import com.limelight.utils.UiHelper;
import android.app.Activity; import android.app.Activity;
import android.app.FragmentTransaction;
import android.app.Service; import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Intent; import android.content.Intent;
@@ -51,69 +50,69 @@ import android.widget.AdapterView.AdapterContextMenuInfo;
public class PcView extends Activity implements AdapterFragmentCallbacks { public class PcView extends Activity implements AdapterFragmentCallbacks {
private RelativeLayout noPcFoundLayout; private RelativeLayout noPcFoundLayout;
private PcGridAdapter pcGridAdapter; private PcGridAdapter pcGridAdapter;
private ComputerManagerService.ComputerManagerBinder managerBinder; private ComputerManagerService.ComputerManagerBinder managerBinder;
private boolean freezeUpdates, runningPolling; private boolean freezeUpdates, runningPolling;
private ServiceConnection serviceConnection = new ServiceConnection() { private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) { public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder = final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder); ((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();
// Force a keypair to be generated early to avoid discovery delays
new AndroidCryptoProvider(PcView.this).getClientCertificate();
}
}.start();
}
public void onServiceDisconnected(ComponentName className) { // Wait in a separate thread to avoid stalling the UI
managerBinder = null; new Thread() {
} @Override
}; public void run() {
// Wait for the binder to be ready
public void onConfigurationChanged(Configuration newConfig) { localBinder.waitForReady();
super.onConfigurationChanged(newConfig);
// Now make the binder visible
// Reinitialize views just in case orientation changed managerBinder = localBinder;
initializeViews();
} // Start updates
startComputerUpdates();
private final static int APP_LIST_ID = 1;
private final static int PAIR_ID = 2; // Force a keypair to be generated early to avoid discovery delays
private final static int UNPAIR_ID = 3; new AndroidCryptoProvider(PcView.this).getClientCertificate();
private final static int WOL_ID = 4; }
private final static int DELETE_ID = 5; }.start();
}
private void initializeViews() {
setContentView(R.layout.activity_pc_view); public void onServiceDisconnected(ComponentName className) {
managerBinder = null;
}
};
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Reinitialize views just in case orientation changed
initializeViews();
}
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;
private void initializeViews() {
setContentView(R.layout.activity_pc_view);
UiHelper.notifyNewRootView(this); UiHelper.notifyNewRootView(this);
// Set default preferences if we've never been run // Set default preferences if we've never been run
PreferenceManager.setDefaultValues(this, R.xml.preferences, false); PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
// Setup the list view // Setup the list view
ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton); ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton);
ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc); ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc);
settingsButton.setOnClickListener(new OnClickListener() { settingsButton.setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
startActivity(new Intent(PcView.this, StreamSettings.class)); startActivity(new Intent(PcView.this, StreamSettings.class));
} }
}); });
addComputerButton.setOnClickListener(new OnClickListener() { addComputerButton.setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
Intent i = new Intent(PcView.this, AddComputerManually.class); Intent i = new Intent(PcView.this, AddComputerManually.class);
@@ -134,114 +133,114 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
} }
pcGridAdapter.notifyDataSetChanged(); pcGridAdapter.notifyDataSetChanged();
} }
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language; String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) { if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration()); Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale); config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics()); getResources().updateConfiguration(config, getResources().getDisplayMetrics());
} }
// Bind to the computer manager service // Bind to the computer manager service
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection, bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE); Service.BIND_AUTO_CREATE);
pcGridAdapter = new PcGridAdapter(this, pcGridAdapter = new PcGridAdapter(this,
PreferenceConfiguration.readPreferences(this).listMode, PreferenceConfiguration.readPreferences(this).listMode,
PreferenceConfiguration.readPreferences(this).smallIconMode); PreferenceConfiguration.readPreferences(this).smallIconMode);
initializeViews(); initializeViews();
} }
private void startComputerUpdates() { private void startComputerUpdates() {
if (managerBinder != null) { if (managerBinder != null) {
if (runningPolling) { if (runningPolling) {
return; return;
} }
freezeUpdates = false; freezeUpdates = false;
managerBinder.startPolling(new ComputerManagerListener() { managerBinder.startPolling(new ComputerManagerListener() {
@Override @Override
public void notifyComputerUpdated(final ComputerDetails details) { public void notifyComputerUpdated(final ComputerDetails details) {
if (!freezeUpdates) { if (!freezeUpdates) {
PcView.this.runOnUiThread(new Runnable() { PcView.this.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
updateComputer(details); updateComputer(details);
} }
}); });
} }
} }
}); });
runningPolling = true; runningPolling = true;
} }
} }
private void stopComputerUpdates(boolean wait) { private void stopComputerUpdates(boolean wait) {
if (managerBinder != null) { if (managerBinder != null) {
if (!runningPolling) { if (!runningPolling) {
return; return;
} }
freezeUpdates = true; freezeUpdates = true;
managerBinder.stopPolling(); managerBinder.stopPolling();
if (wait) { if (wait) {
managerBinder.waitForPollingStopped(); managerBinder.waitForPollingStopped();
} }
runningPolling = false; runningPolling = false;
} }
} }
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
if (managerBinder != null) { if (managerBinder != null) {
unbindService(serviceConnection); unbindService(serviceConnection);
} }
} }
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
startComputerUpdates(); startComputerUpdates();
} }
@Override @Override
protected void onPause() { protected void onPause() {
super.onPause(); super.onPause();
stopComputerUpdates(false); stopComputerUpdates(false);
} }
@Override @Override
protected void onStop() { protected void onStop() {
super.onStop(); super.onStop();
Dialog.closeDialogs(); Dialog.closeDialogs();
} }
@Override @Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
stopComputerUpdates(false); stopComputerUpdates(false);
// Call superclass // Call superclass
super.onCreateContextMenu(menu, v, menuInfo); super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position); ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
if (computer == null || computer.details == null || if (computer == null || computer.details == null ||
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) { computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
startComputerUpdates(); startComputerUpdates();
return; return;
} }
// Inflate the context menu // Inflate the context menu
@@ -255,97 +254,100 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
} }
else { else {
menu.add(Menu.NONE, APP_LIST_ID, 1, getResources().getString(R.string.pcview_menu_app_list)); menu.add(Menu.NONE, APP_LIST_ID, 1, getResources().getString(R.string.pcview_menu_app_list));
menu.add(Menu.NONE, UNPAIR_ID, 2, getResources().getString(R.string.pcview_menu_unpair_pc));
// FIXME: We used to be able to unpair here but it's been broken since GFE 2.1.x, so I've replaced
// it with delete which actually work
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
} }
} }
@Override @Override
public void onContextMenuClosed(Menu menu) { public void onContextMenuClosed(Menu menu) {
startComputerUpdates(); startComputerUpdates();
} }
private void doPair(final ComputerDetails computer) { private void doPair(final ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) { if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
return; return;
} }
if (computer.runningGameId != 0) { if (computer.runningGameId != 0) {
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_ingame), Toast.LENGTH_LONG).show(); Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_ingame), Toast.LENGTH_LONG).show();
return; return;
} }
if (managerBinder == null) { if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return; return;
} }
Toast.makeText(PcView.this, getResources().getString(R.string.pairing), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, getResources().getString(R.string.pairing), Toast.LENGTH_SHORT).show();
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
NvHTTP httpConn; NvHTTP httpConn;
String message; String message;
boolean success = false; boolean success = false;
try { try {
// Stop updates and wait while pairing // Stop updates and wait while pairing
stopComputerUpdates(true); stopComputerUpdates(true);
InetAddress addr = null; InetAddress addr = null;
if (computer.reachability == ComputerDetails.Reachability.LOCAL) { if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
addr = computer.localIp; addr = computer.localIp;
} }
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) { else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
addr = computer.remoteIp; addr = computer.remoteIp;
} }
httpConn = new NvHTTP(addr, httpConn = new NvHTTP(addr,
managerBinder.getUniqueId(), managerBinder.getUniqueId(),
PlatformBinding.getDeviceName(), PlatformBinding.getDeviceName(),
PlatformBinding.getCryptoProvider(PcView.this)); PlatformBinding.getCryptoProvider(PcView.this));
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) { if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
// Don't display any toast, but open the app list // Don't display any toast, but open the app list
message = null; message = null;
success = true; success = true;
} }
else { else {
final String pinStr = PairingManager.generatePinString(); final String pinStr = PairingManager.generatePinString();
// Spin the dialog off in a thread because it blocks // Spin the dialog off in a thread because it blocks
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title), Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false); getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
PairingManager.PairState pairState = httpConn.pair(pinStr); PairingManager.PairState pairState = httpConn.pair(pinStr);
if (pairState == PairingManager.PairState.PIN_WRONG) { if (pairState == PairingManager.PairState.PIN_WRONG) {
message = getResources().getString(R.string.pair_incorrect_pin); message = getResources().getString(R.string.pair_incorrect_pin);
} }
else if (pairState == PairingManager.PairState.FAILED) { else if (pairState == PairingManager.PairState.FAILED) {
message = getResources().getString(R.string.pair_fail); message = getResources().getString(R.string.pair_fail);
} }
else if (pairState == PairingManager.PairState.PAIRED) { else if (pairState == PairingManager.PairState.PAIRED) {
// Just navigate to the app view without displaying a toast // Just navigate to the app view without displaying a toast
message = null; message = null;
success = true; success = true;
} }
else { else {
// Should be no other values // Should be no other values
message = null; message = null;
} }
} }
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
message = getResources().getString(R.string.error_unknown_host); message = getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
message = getResources().getString(R.string.error_404); message = getResources().getString(R.string.error_404);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
message = e.getMessage(); message = e.getMessage();
} }
Dialog.closeDialogs(); Dialog.closeDialogs();
final String toastMessage = message; final String toastMessage = message;
final boolean toastSuccess = success; final boolean toastSuccess = success;
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
if (toastMessage != null) { if (toastMessage != null) {
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show(); Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
} }
@@ -354,124 +356,124 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
// Open the app list after a successful pairing attemp // Open the app list after a successful pairing attemp
doAppList(computer); doAppList(computer);
} }
} }
}); });
// Start polling again // Start polling again
startComputerUpdates(); startComputerUpdates();
} }
}).start(); }).start();
} }
private void doWakeOnLan(final ComputerDetails computer) { private void doWakeOnLan(final ComputerDetails computer) {
if (computer.reachability != ComputerDetails.Reachability.OFFLINE) { if (computer.reachability != ComputerDetails.Reachability.OFFLINE) {
Toast.makeText(PcView.this, getResources().getString(R.string.wol_pc_online), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, getResources().getString(R.string.wol_pc_online), Toast.LENGTH_SHORT).show();
return; return;
} }
if (computer.macAddress == null) { if (computer.macAddress == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.wol_no_mac), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, getResources().getString(R.string.wol_no_mac), Toast.LENGTH_SHORT).show();
return; return;
} }
Toast.makeText(PcView.this, getResources().getString(R.string.wol_waking_pc), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, getResources().getString(R.string.wol_waking_pc), Toast.LENGTH_SHORT).show();
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
String message; String message;
try { try {
WakeOnLanSender.sendWolPacket(computer); WakeOnLanSender.sendWolPacket(computer);
message = getResources().getString(R.string.wol_waking_msg); message = getResources().getString(R.string.wol_waking_msg);
} catch (IOException e) { } catch (IOException e) {
message = getResources().getString(R.string.wol_fail); message = getResources().getString(R.string.wol_fail);
} }
final String toastMessage = message; final String toastMessage = message;
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show(); Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
} }
}); });
} }
}).start(); }).start();
} }
private void doUnpair(final ComputerDetails computer) { private void doUnpair(final ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) { if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
return; return;
} }
if (managerBinder == null) { if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return; return;
} }
Toast.makeText(PcView.this, getResources().getString(R.string.unpairing), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, getResources().getString(R.string.unpairing), Toast.LENGTH_SHORT).show();
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
NvHTTP httpConn; NvHTTP httpConn;
String message; String message;
try { try {
InetAddress addr = null; InetAddress addr = null;
if (computer.reachability == ComputerDetails.Reachability.LOCAL) { if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
addr = computer.localIp; addr = computer.localIp;
} }
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) { else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
addr = computer.remoteIp; addr = computer.remoteIp;
} }
httpConn = new NvHTTP(addr, httpConn = new NvHTTP(addr,
managerBinder.getUniqueId(), managerBinder.getUniqueId(),
PlatformBinding.getDeviceName(), PlatformBinding.getDeviceName(),
PlatformBinding.getCryptoProvider(PcView.this)); PlatformBinding.getCryptoProvider(PcView.this));
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) { if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
httpConn.unpair(); httpConn.unpair();
if (httpConn.getPairState() == PairingManager.PairState.NOT_PAIRED) { if (httpConn.getPairState() == PairingManager.PairState.NOT_PAIRED) {
message = getResources().getString(R.string.unpair_success); message = getResources().getString(R.string.unpair_success);
} }
else { else {
message = getResources().getString(R.string.unpair_fail); message = getResources().getString(R.string.unpair_fail);
} }
} }
else { else {
message = getResources().getString(R.string.unpair_error); message = getResources().getString(R.string.unpair_error);
} }
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
message = getResources().getString(R.string.error_unknown_host); message = getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
message = getResources().getString(R.string.error_404); message = getResources().getString(R.string.error_404);
} catch (Exception e) { } catch (Exception e) {
message = e.getMessage(); message = e.getMessage();
} }
final String toastMessage = message; final String toastMessage = message;
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show(); Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
} }
}); });
} }
}).start(); }).start();
} }
private void doAppList(ComputerDetails computer) { private void doAppList(ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) { if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
return; return;
} }
if (managerBinder == null) { if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return; return;
} }
Intent i = new Intent(this, AppView.class); Intent i = new Intent(this, AppView.class);
i.putExtra(AppView.NAME_EXTRA, computer.name); i.putExtra(AppView.NAME_EXTRA, computer.name);
i.putExtra(AppView.UUID_EXTRA, computer.uuid.toString()); i.putExtra(AppView.UUID_EXTRA, computer.uuid.toString());
startActivity(i); startActivity(i);
} }
@Override @Override
public boolean onContextItemSelected(MenuItem item) { public boolean onContextItemSelected(MenuItem item) {
@@ -480,75 +482,75 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
switch (item.getItemId()) switch (item.getItemId())
{ {
case PAIR_ID: case PAIR_ID:
doPair(computer.details); doPair(computer.details);
return true; return true;
case UNPAIR_ID: case UNPAIR_ID:
doUnpair(computer.details); doUnpair(computer.details);
return true; return true;
case WOL_ID: case WOL_ID:
doWakeOnLan(computer.details); doWakeOnLan(computer.details);
return true; return true;
case DELETE_ID: case DELETE_ID:
if (managerBinder == null) { if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return true; return true;
} }
managerBinder.removeComputer(computer.details.name); managerBinder.removeComputer(computer.details.name);
removeComputer(computer.details); removeComputer(computer.details);
return true; return true;
case APP_LIST_ID: case APP_LIST_ID:
doAppList(computer.details); doAppList(computer.details);
return true; return true;
default: default:
return super.onContextItemSelected(item); return super.onContextItemSelected(item);
} }
} }
private void removeComputer(ComputerDetails details) { private void removeComputer(ComputerDetails details) {
for (int i = 0; i < pcGridAdapter.getCount(); i++) { for (int i = 0; i < pcGridAdapter.getCount(); i++) {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i); ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
if (details.equals(computer.details)) { if (details.equals(computer.details)) {
pcGridAdapter.removeComputer(computer); pcGridAdapter.removeComputer(computer);
pcGridAdapter.notifyDataSetChanged(); pcGridAdapter.notifyDataSetChanged();
break; break;
} }
} }
} }
private void updateComputer(ComputerDetails details) { private void updateComputer(ComputerDetails details) {
ComputerObject existingEntry = null; ComputerObject existingEntry = null;
for (int i = 0; i < pcGridAdapter.getCount(); i++) { for (int i = 0; i < pcGridAdapter.getCount(); i++) {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i); ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
// Check if this is the same computer // Check if this is the same computer
if (details.equals(computer.details)) { if (details.uuid.equals(computer.details.uuid)) {
existingEntry = computer; existingEntry = computer;
break; break;
} }
} }
if (existingEntry != null) { if (existingEntry != null) {
// Replace the information in the existing entry // Replace the information in the existing entry
existingEntry.details = details; existingEntry.details = details;
} }
else { else {
// Add a new entry // Add a new entry
pcGridAdapter.addComputer(new ComputerObject(details)); pcGridAdapter.addComputer(new ComputerObject(details));
// Remove the "Discovery in progress" view // Remove the "Discovery in progress" view
noPcFoundLayout.setVisibility(View.INVISIBLE); noPcFoundLayout.setVisibility(View.INVISIBLE);
} }
// Notify the view that the data has changed // Notify the view that the data has changed
pcGridAdapter.notifyDataSetChanged(); pcGridAdapter.notifyDataSetChanged();
} }
@Override @Override
public int getAdapterFragmentLayoutId() { public int getAdapterFragmentLayoutId() {
@@ -582,15 +584,15 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
} }
public class ComputerObject { public class ComputerObject {
public ComputerDetails details; public ComputerDetails details;
public ComputerObject(ComputerDetails details) { public ComputerObject(ComputerDetails details) {
this.details = details; this.details = details;
} }
@Override @Override
public String toString() { public String toString() {
return details.name; return details.name;
} }
} }
} }
@@ -8,17 +8,17 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
import com.limelight.nvstream.http.LimelightCryptoProvider; import com.limelight.nvstream.http.LimelightCryptoProvider;
public class PlatformBinding { public class PlatformBinding {
public static String getDeviceName() { public static String getDeviceName() {
String deviceName = android.os.Build.MODEL; String deviceName = android.os.Build.MODEL;
deviceName = deviceName.replace(" ", ""); deviceName = deviceName.replace(" ", "");
return deviceName; return deviceName;
} }
public static AudioRenderer getAudioRenderer() { public static AudioRenderer getAudioRenderer() {
return new AndroidAudioRenderer(); return new AndroidAudioRenderer();
} }
public static LimelightCryptoProvider getCryptoProvider(Context c) { public static LimelightCryptoProvider getCryptoProvider(Context c) {
return new AndroidCryptoProvider(c); return new AndroidCryptoProvider(c);
} }
} }
@@ -9,27 +9,27 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
public class AndroidAudioRenderer implements AudioRenderer { public class AndroidAudioRenderer implements AudioRenderer {
public static final int FRAME_SIZE = 960; private static final int FRAME_SIZE = 960;
private AudioTrack track;
@Override private AudioTrack track;
public boolean streamInitialized(int channelCount, int sampleRate) {
int channelConfig;
int bufferSize;
switch (channelCount) @Override
{ public boolean streamInitialized(int channelCount, int sampleRate) {
case 1: int channelConfig;
channelConfig = AudioFormat.CHANNEL_OUT_MONO; int bufferSize;
break;
case 2: switch (channelCount)
channelConfig = AudioFormat.CHANNEL_OUT_STEREO; {
break; case 1:
default: channelConfig = AudioFormat.CHANNEL_OUT_MONO;
LimeLog.severe("Decoder returned unhandled channel count"); break;
return false; case 2:
} channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
default:
LimeLog.severe("Decoder returned unhandled channel count");
return false;
}
// We're not supposed to request less than the minimum // We're not supposed to request less than the minimum
// buffer size for our buffer, but it appears that we can // buffer size for our buffer, but it appears that we can
@@ -72,26 +72,26 @@ public class AndroidAudioRenderer implements AudioRenderer {
AudioTrack.MODE_STREAM); AudioTrack.MODE_STREAM);
track.play(); track.play();
} }
LimeLog.info("Audio track buffer size: "+bufferSize);
return true; LimeLog.info("Audio track buffer size: "+bufferSize);
}
@Override return true;
public void playDecodedAudio(byte[] audioData, int offset, int length) { }
track.write(audioData, offset, length);
}
@Override @Override
public void streamClosing() { public void playDecodedAudio(byte[] audioData, int offset, int length) {
if (track != null) { track.write(audioData, offset, length);
track.release(); }
}
}
@Override @Override
public int getCapabilities() { public void streamClosing() {
return 0; if (track != null) {
} track.release();
}
}
@Override
public int getCapabilities() {
return 0;
}
} }
@@ -45,239 +45,239 @@ import com.limelight.nvstream.http.LimelightCryptoProvider;
public class AndroidCryptoProvider implements LimelightCryptoProvider { public class AndroidCryptoProvider implements LimelightCryptoProvider {
private File certFile; private final File certFile;
private File keyFile; private final File keyFile;
private X509Certificate cert; private X509Certificate cert;
private RSAPrivateKey key; private RSAPrivateKey key;
private byte[] pemCertBytes; private byte[] pemCertBytes;
private static final Object globalCryptoLock = new Object(); private static final Object globalCryptoLock = new Object();
static { static {
// Install the Bouncy Castle provider // Install the Bouncy Castle provider
Security.addProvider(new BouncyCastleProvider()); Security.addProvider(new BouncyCastleProvider());
} }
public AndroidCryptoProvider(Context c) { public AndroidCryptoProvider(Context c) {
String dataPath = c.getFilesDir().getAbsolutePath(); String dataPath = c.getFilesDir().getAbsolutePath();
certFile = new File(dataPath + File.separator + "client.crt"); certFile = new File(dataPath + File.separator + "client.crt");
keyFile = new File(dataPath + File.separator + "client.key"); keyFile = new File(dataPath + File.separator + "client.key");
} }
private byte[] loadFileToBytes(File f) { private byte[] loadFileToBytes(File f) {
if (!f.exists()) { if (!f.exists()) {
return null; return null;
} }
try { try {
FileInputStream fin = new FileInputStream(f); FileInputStream fin = new FileInputStream(f);
byte[] fileData = new byte[(int) f.length()]; byte[] fileData = new byte[(int) f.length()];
if (fin.read(fileData) != f.length()) { if (fin.read(fileData) != f.length()) {
// Failed to read // Failed to read
fileData = null; fileData = null;
} }
fin.close(); fin.close();
return fileData; return fileData;
} catch (IOException e) { } catch (IOException e) {
return null; return null;
} }
} }
private boolean loadCertKeyPair() {
byte[] certBytes = loadFileToBytes(certFile);
byte[] keyBytes = loadFileToBytes(keyFile);
// If either file was missing, we definitely can't succeed
if (certBytes == null || keyBytes == null) {
LimeLog.info("Missing cert or key; need to generate a new one");
return false;
}
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
pemCertBytes = certBytes;
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
} catch (CertificateException e) {
// May happen if the cert is corrupt
LimeLog.warning("Corrupted certificate");
return false;
} catch (NoSuchAlgorithmException e) {
// Should never happen
e.printStackTrace();
return false;
} catch (InvalidKeySpecException e) {
// May happen if the key is corrupt
LimeLog.warning("Corrupted key");
return false;
} catch (NoSuchProviderException e) {
// Should never happen
e.printStackTrace();
return false;
}
return true;
}
@SuppressLint("TrulyRandom")
private boolean generateCertKeyPair() {
byte[] snBytes = new byte[8];
new SecureRandom().nextBytes(snBytes);
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e1) {
// Should never happen
e1.printStackTrace();
return false;
} catch (NoSuchProviderException e) {
// Should never happen
e.printStackTrace();
return false;
}
Date now = new Date();
// Expires in 20 years
Calendar calendar = Calendar.getInstance();
calendar.setTime(now);
calendar.add(Calendar.YEAR, 20);
Date expirationDate = calendar.getTime();
BigInteger serial = new BigInteger(snBytes).abs();
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
X500Name name = nameBuilder.build();
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
try {
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
key = (RSAPrivateKey) keyPair.getPrivate();
} catch (Exception e) {
// Nothing should go wrong here
e.printStackTrace();
return false;
}
LimeLog.info("Generated a new key pair");
// Save the resulting pair
saveCertKeyPair();
return true;
}
private void saveCertKeyPair() {
try {
FileOutputStream certOut = new FileOutputStream(certFile);
FileOutputStream keyOut = new FileOutputStream(keyFile);
// Write the certificate in OpenSSL PEM format (important for the server)
StringWriter strWriter = new StringWriter();
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
pemWriter.writeObject(cert);
pemWriter.close();
// Line endings MUST be UNIX for the PC to accept the cert properly
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
String pemStr = strWriter.getBuffer().toString();
for (int i = 0; i < pemStr.length(); i++) {
char c = pemStr.charAt(i);
if (c != '\r')
certWriter.append(c);
}
certWriter.close();
// Write the private out in PKCS8 format
keyOut.write(key.getEncoded());
certOut.close();
keyOut.close();
LimeLog.info("Saved generated key pair to disk");
} catch (IOException e) {
// This isn't good because it means we'll have
// to re-pair next time
e.printStackTrace();
}
}
public X509Certificate getClientCertificate() {
// Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time
synchronized (globalCryptoLock) {
// Return a loaded cert if we have one
if (cert != null) {
return cert;
}
// No loaded cert yet, let's see if we have one on disk
if (loadCertKeyPair()) {
// Got one
return cert;
}
// Try to generate a new key pair
if (!generateCertKeyPair()) {
// Failed
return null;
}
// Load the generated pair
loadCertKeyPair();
return cert;
}
}
public RSAPrivateKey getClientPrivateKey() { private boolean loadCertKeyPair() {
// Use a lock here to ensure only one guy will be generating or loading byte[] certBytes = loadFileToBytes(certFile);
// the certificate and key at a time byte[] keyBytes = loadFileToBytes(keyFile);
synchronized (globalCryptoLock) {
// Return a loaded key if we have one
if (key != null) {
return key;
}
// No loaded key yet, let's see if we have one on disk
if (loadCertKeyPair()) {
// Got one
return key;
}
// Try to generate a new key pair
if (!generateCertKeyPair()) {
// Failed
return null;
}
// Load the generated pair
loadCertKeyPair();
return key;
}
}
public byte[] getPemEncodedClientCertificate() {
synchronized (globalCryptoLock) {
// Call our helper function to do the cert loading/generation for us
getClientCertificate();
// Return a cached value if we have it
return pemCertBytes;
}
}
@Override // If either file was missing, we definitely can't succeed
public String encodeBase64String(byte[] data) { if (certBytes == null || keyBytes == null) {
return Base64.encodeToString(data, Base64.NO_WRAP); LimeLog.info("Missing cert or key; need to generate a new one");
} return false;
}
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
pemCertBytes = certBytes;
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
} catch (CertificateException e) {
// May happen if the cert is corrupt
LimeLog.warning("Corrupted certificate");
return false;
} catch (NoSuchAlgorithmException e) {
// Should never happen
e.printStackTrace();
return false;
} catch (InvalidKeySpecException e) {
// May happen if the key is corrupt
LimeLog.warning("Corrupted key");
return false;
} catch (NoSuchProviderException e) {
// Should never happen
e.printStackTrace();
return false;
}
return true;
}
@SuppressLint("TrulyRandom")
private boolean generateCertKeyPair() {
byte[] snBytes = new byte[8];
new SecureRandom().nextBytes(snBytes);
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e1) {
// Should never happen
e1.printStackTrace();
return false;
} catch (NoSuchProviderException e) {
// Should never happen
e.printStackTrace();
return false;
}
Date now = new Date();
// Expires in 20 years
Calendar calendar = Calendar.getInstance();
calendar.setTime(now);
calendar.add(Calendar.YEAR, 20);
Date expirationDate = calendar.getTime();
BigInteger serial = new BigInteger(snBytes).abs();
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
X500Name name = nameBuilder.build();
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
try {
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
key = (RSAPrivateKey) keyPair.getPrivate();
} catch (Exception e) {
// Nothing should go wrong here
e.printStackTrace();
return false;
}
LimeLog.info("Generated a new key pair");
// Save the resulting pair
saveCertKeyPair();
return true;
}
private void saveCertKeyPair() {
try {
FileOutputStream certOut = new FileOutputStream(certFile);
FileOutputStream keyOut = new FileOutputStream(keyFile);
// Write the certificate in OpenSSL PEM format (important for the server)
StringWriter strWriter = new StringWriter();
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
pemWriter.writeObject(cert);
pemWriter.close();
// Line endings MUST be UNIX for the PC to accept the cert properly
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
String pemStr = strWriter.getBuffer().toString();
for (int i = 0; i < pemStr.length(); i++) {
char c = pemStr.charAt(i);
if (c != '\r')
certWriter.append(c);
}
certWriter.close();
// Write the private out in PKCS8 format
keyOut.write(key.getEncoded());
certOut.close();
keyOut.close();
LimeLog.info("Saved generated key pair to disk");
} catch (IOException e) {
// This isn't good because it means we'll have
// to re-pair next time
e.printStackTrace();
}
}
public X509Certificate getClientCertificate() {
// Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time
synchronized (globalCryptoLock) {
// Return a loaded cert if we have one
if (cert != null) {
return cert;
}
// No loaded cert yet, let's see if we have one on disk
if (loadCertKeyPair()) {
// Got one
return cert;
}
// Try to generate a new key pair
if (!generateCertKeyPair()) {
// Failed
return null;
}
// Load the generated pair
loadCertKeyPair();
return cert;
}
}
public RSAPrivateKey getClientPrivateKey() {
// Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time
synchronized (globalCryptoLock) {
// Return a loaded key if we have one
if (key != null) {
return key;
}
// No loaded key yet, let's see if we have one on disk
if (loadCertKeyPair()) {
// Got one
return key;
}
// Try to generate a new key pair
if (!generateCertKeyPair()) {
// Failed
return null;
}
// Load the generated pair
loadCertKeyPair();
return key;
}
}
public byte[] getPemEncodedClientCertificate() {
synchronized (globalCryptoLock) {
// Call our helper function to do the cert loading/generation for us
getClientCertificate();
// Return a cached value if we have it
return pemCertBytes;
}
}
@Override
public String encodeBase64String(byte[] data) {
return Base64.encodeToString(data, Base64.NO_WRAP);
}
} }
File diff suppressed because it is too large. Load diff
@@ -15,7 +15,7 @@ public class KeyboardTranslator extends KeycodeTranslator {
/** /**
* GFE's prefix for every key code * GFE's prefix for every key code
*/ */
public static final short KEY_PREFIX = (short) 0x80; private static final short KEY_PREFIX = (short) 0x80;
public static final int VK_0 = 48; public static final int VK_0 = 48;
public static final int VK_9 = 57; public static final int VK_9 = 57;
@@ -23,8 +23,8 @@ public class KeyboardTranslator extends KeycodeTranslator {
public static final int VK_Z = 90; public static final int VK_Z = 90;
public static final int VK_ALT = 18; public static final int VK_ALT = 18;
public static final int VK_NUMPAD0 = 96; public static final int VK_NUMPAD0 = 96;
public static final int VK_BACK_SLASH = 92; public static final int VK_BACK_SLASH = 92;
public static final int VK_CAPS_LOCK = 20; public static final int VK_CAPS_LOCK = 20;
public static final int VK_CLEAR = 12; public static final int VK_CLEAR = 12;
public static final int VK_COMMA = 44; public static final int VK_COMMA = 44;
public static final int VK_CONTROL = 17; public static final int VK_CONTROL = 17;
@@ -4,94 +4,95 @@ import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.MouseButtonPacket; import com.limelight.nvstream.input.MouseButtonPacket;
public class TouchContext { public class TouchContext {
private int lastTouchX = 0; private int lastTouchX = 0;
private int lastTouchY = 0; private int lastTouchY = 0;
private int originalTouchX = 0; private int originalTouchX = 0;
private int originalTouchY = 0; private int originalTouchY = 0;
private long originalTouchTime = 0; private long originalTouchTime = 0;
private boolean cancelled; private boolean cancelled;
private NvConnection conn; private final NvConnection conn;
private int actionIndex; private final int actionIndex;
private double xFactor, yFactor; private final double xFactor;
private final double yFactor;
private static final int TAP_MOVEMENT_THRESHOLD = 10;
private static final int TAP_TIME_THRESHOLD = 250; private static final int TAP_MOVEMENT_THRESHOLD = 10;
private static final int TAP_TIME_THRESHOLD = 250;
public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor)
{ public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor)
this.conn = conn; {
this.actionIndex = actionIndex; this.conn = conn;
this.actionIndex = actionIndex;
this.xFactor = xFactor; this.xFactor = xFactor;
this.yFactor = yFactor; this.yFactor = yFactor;
} }
public int getActionIndex() public int getActionIndex()
{ {
return actionIndex; return actionIndex;
} }
private boolean isTap() private boolean isTap()
{ {
int xDelta = Math.abs(lastTouchX - originalTouchX); int xDelta = Math.abs(lastTouchX - originalTouchX);
int yDelta = Math.abs(lastTouchY - originalTouchY); int yDelta = Math.abs(lastTouchY - originalTouchY);
long timeDelta = System.currentTimeMillis() - originalTouchTime; long timeDelta = System.currentTimeMillis() - originalTouchTime;
return xDelta <= TAP_MOVEMENT_THRESHOLD && return xDelta <= TAP_MOVEMENT_THRESHOLD &&
yDelta <= TAP_MOVEMENT_THRESHOLD && yDelta <= TAP_MOVEMENT_THRESHOLD &&
timeDelta <= TAP_TIME_THRESHOLD; timeDelta <= TAP_TIME_THRESHOLD;
} }
private byte getMouseButtonIndex() private byte getMouseButtonIndex()
{ {
if (actionIndex == 1) { if (actionIndex == 1) {
return MouseButtonPacket.BUTTON_RIGHT; return MouseButtonPacket.BUTTON_RIGHT;
} }
else { else {
return MouseButtonPacket.BUTTON_LEFT; return MouseButtonPacket.BUTTON_LEFT;
} }
} }
public boolean touchDownEvent(int eventX, int eventY) public boolean touchDownEvent(int eventX, int eventY)
{ {
originalTouchX = lastTouchX = eventX; originalTouchX = lastTouchX = eventX;
originalTouchY = lastTouchY = eventY; originalTouchY = lastTouchY = eventY;
originalTouchTime = System.currentTimeMillis(); originalTouchTime = System.currentTimeMillis();
cancelled = false; cancelled = false;
return true; return true;
} }
public void touchUpEvent(int eventX, int eventY) public void touchUpEvent(int eventX, int eventY)
{ {
if (cancelled) { if (cancelled) {
return; return;
} }
if (isTap()) if (isTap())
{ {
byte buttonIndex = getMouseButtonIndex(); byte buttonIndex = getMouseButtonIndex();
// Lower the mouse button // Lower the mouse button
conn.sendMouseButtonDown(buttonIndex); conn.sendMouseButtonDown(buttonIndex);
// We need to sleep a bit here because some games // We need to sleep a bit here because some games
// do input detection by polling // do input detection by polling
try { try {
Thread.sleep(100); Thread.sleep(100);
} catch (InterruptedException ignored) {} } catch (InterruptedException ignored) {}
// Raise the mouse button // Raise the mouse button
conn.sendMouseButtonUp(buttonIndex); conn.sendMouseButtonUp(buttonIndex);
} }
} }
public boolean touchMoveEvent(int eventX, int eventY) public boolean touchMoveEvent(int eventX, int eventY)
{ {
if (eventX != lastTouchX || eventY != lastTouchY) if (eventX != lastTouchX || eventY != lastTouchY)
{ {
// We only send moves for the primary touch point // We only send moves for the primary touch point
if (actionIndex == 0) { if (actionIndex == 0) {
int deltaX = eventX - lastTouchX; int deltaX = eventX - lastTouchX;
int deltaY = eventY - lastTouchY; int deltaY = eventY - lastTouchY;
@@ -99,15 +100,15 @@ public class TouchContext {
deltaX = (int)Math.round((double)deltaX * xFactor); deltaX = (int)Math.round((double)deltaX * xFactor);
deltaY = (int)Math.round((double)deltaY * yFactor); deltaY = (int)Math.round((double)deltaY * yFactor);
conn.sendMouseMove((short)deltaX, (short)deltaY); conn.sendMouseMove((short)deltaX, (short)deltaY);
} }
lastTouchX = eventX; lastTouchX = eventX;
lastTouchY = eventY; lastTouchY = eventY;
} }
return true; return true;
} }
public void cancelTouch() { public void cancelTouch() {
cancelled = true; cancelled = true;
@@ -1,41 +1,41 @@
package com.limelight.binding.input.evdev; package com.limelight.binding.input.evdev;
public class EvdevEvent { public class EvdevEvent {
public static final int EVDEV_MIN_EVENT_SIZE = 16; public static final int EVDEV_MIN_EVENT_SIZE = 16;
public static final int EVDEV_MAX_EVENT_SIZE = 24; public static final int EVDEV_MAX_EVENT_SIZE = 24;
/* Event types */ /* Event types */
public static final short EV_SYN = 0x00; public static final short EV_SYN = 0x00;
public static final short EV_KEY = 0x01; public static final short EV_KEY = 0x01;
public static final short EV_REL = 0x02; public static final short EV_REL = 0x02;
public static final short EV_MSC = 0x04; public static final short EV_MSC = 0x04;
/* Relative axes */ /* Relative axes */
public static final short REL_X = 0x00; public static final short REL_X = 0x00;
public static final short REL_Y = 0x01; public static final short REL_Y = 0x01;
public static final short REL_WHEEL = 0x08; public static final short REL_WHEEL = 0x08;
/* Buttons */ /* Buttons */
public static final short BTN_LEFT = 0x110; public static final short BTN_LEFT = 0x110;
public static final short BTN_RIGHT = 0x111; public static final short BTN_RIGHT = 0x111;
public static final short BTN_MIDDLE = 0x112; public static final short BTN_MIDDLE = 0x112;
public static final short BTN_SIDE = 0x113; public static final short BTN_SIDE = 0x113;
public static final short BTN_EXTRA = 0x114; public static final short BTN_EXTRA = 0x114;
public static final short BTN_FORWARD = 0x115; public static final short BTN_FORWARD = 0x115;
public static final short BTN_BACK = 0x116; public static final short BTN_BACK = 0x116;
public static final short BTN_TASK = 0x117; public static final short BTN_TASK = 0x117;
public static final short BTN_GAMEPAD = 0x130; public static final short BTN_GAMEPAD = 0x130;
/* Keys */ /* Keys */
public static final short KEY_Q = 16; public static final short KEY_Q = 16;
public short type; public final short type;
public short code; public final short code;
public int value; public final int value;
public EvdevEvent(short type, short code, int value) { public EvdevEvent(short type, short code, int value) {
this.type = type; this.type = type;
this.code = code; this.code = code;
this.value = value; this.value = value;
} }
} }
@@ -7,161 +7,161 @@ import com.limelight.LimeLog;
public class EvdevHandler { public class EvdevHandler {
private String absolutePath; private final String absolutePath;
private EvdevListener listener; private final EvdevListener listener;
private boolean shutdown = false; private boolean shutdown = false;
private int fd = -1; private int fd = -1;
private Thread handlerThread = new Thread() {
@Override
public void run() {
// All the finally blocks here make this code look like a mess
// but it's important that we get this right to avoid causing
// system-wide input problems.
// Open the /dev/input/eventX file
fd = EvdevReader.open(absolutePath);
if (fd == -1) {
LimeLog.warning("Unable to open "+absolutePath);
return;
}
try { private final Thread handlerThread = new Thread() {
// Check if it's a mouse or keyboard, but not a gamepad @Override
if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) || public void run() {
EvdevReader.isGamepad(fd)) { // All the finally blocks here make this code look like a mess
// We only handle keyboards and mice // but it's important that we get this right to avoid causing
return; // system-wide input problems.
}
// Grab it for ourselves // Open the /dev/input/eventX file
if (!EvdevReader.grab(fd)) { fd = EvdevReader.open(absolutePath);
LimeLog.warning("Unable to grab "+absolutePath); if (fd == -1) {
return; LimeLog.warning("Unable to open "+absolutePath);
} return;
}
LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath); try {
// Check if it's a mouse or keyboard, but not a gamepad
if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) ||
EvdevReader.isGamepad(fd)) {
// We only handle keyboards and mice
return;
}
ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder()); // Grab it for ourselves
if (!EvdevReader.grab(fd)) {
LimeLog.warning("Unable to grab "+absolutePath);
return;
}
try { LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath);
int deltaX = 0;
int deltaY = 0;
byte deltaScroll = 0;
while (!isInterrupted() && !shutdown) { ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder());
EvdevEvent event = EvdevReader.read(fd, buffer);
if (event == null) {
return;
}
switch (event.type) try {
{ int deltaX = 0;
case EvdevEvent.EV_SYN: int deltaY = 0;
if (deltaX != 0 || deltaY != 0) { byte deltaScroll = 0;
listener.mouseMove(deltaX, deltaY);
deltaX = deltaY = 0;
}
if (deltaScroll != 0) {
listener.mouseScroll(deltaScroll);
deltaScroll = 0;
}
break;
case EvdevEvent.EV_REL: while (!isInterrupted() && !shutdown) {
switch (event.code) EvdevEvent event = EvdevReader.read(fd, buffer);
{ if (event == null) {
case EvdevEvent.REL_X: return;
deltaX = event.value; }
break;
case EvdevEvent.REL_Y:
deltaY = event.value;
break;
case EvdevEvent.REL_WHEEL:
deltaScroll = (byte) event.value;
break;
}
break;
case EvdevEvent.EV_KEY: switch (event.type)
switch (event.code) {
{ case EvdevEvent.EV_SYN:
case EvdevEvent.BTN_LEFT: if (deltaX != 0 || deltaY != 0) {
listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT, listener.mouseMove(deltaX, deltaY);
event.value != 0); deltaX = deltaY = 0;
break; }
case EvdevEvent.BTN_MIDDLE: if (deltaScroll != 0) {
listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE, listener.mouseScroll(deltaScroll);
event.value != 0); deltaScroll = 0;
break; }
case EvdevEvent.BTN_RIGHT: break;
listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT,
event.value != 0); case EvdevEvent.EV_REL:
break; switch (event.code)
{
case EvdevEvent.BTN_SIDE: case EvdevEvent.REL_X:
case EvdevEvent.BTN_EXTRA: deltaX = event.value;
case EvdevEvent.BTN_FORWARD: break;
case EvdevEvent.BTN_BACK: case EvdevEvent.REL_Y:
case EvdevEvent.BTN_TASK: deltaY = event.value;
// Other unhandled mouse buttons break;
break; case EvdevEvent.REL_WHEEL:
deltaScroll = (byte) event.value;
default: break;
// We got some unrecognized button. This means }
// someone is trying to use the other device in this break;
// "combination" input device. We'll try to handle
// it via keyboard, but we're not going to disconnect case EvdevEvent.EV_KEY:
// if we can't switch (event.code)
short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code); {
if (keyCode != 0) { case EvdevEvent.BTN_LEFT:
listener.keyboardEvent(event.value != 0, keyCode); listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT,
} event.value != 0);
break; break;
} case EvdevEvent.BTN_MIDDLE:
break; listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE,
event.value != 0);
case EvdevEvent.EV_MSC: break;
break; case EvdevEvent.BTN_RIGHT:
} listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT,
} event.value != 0);
} finally { break;
// Release our grab
EvdevReader.ungrab(fd); case EvdevEvent.BTN_SIDE:
} case EvdevEvent.BTN_EXTRA:
} finally { case EvdevEvent.BTN_FORWARD:
// Close the file case EvdevEvent.BTN_BACK:
EvdevReader.close(fd); case EvdevEvent.BTN_TASK:
} // Other unhandled mouse buttons
} break;
};
default:
public EvdevHandler(String absolutePath, EvdevListener listener) { // We got some unrecognized button. This means
this.absolutePath = absolutePath; // someone is trying to use the other device in this
this.listener = listener; // "combination" input device. We'll try to handle
} // it via keyboard, but we're not going to disconnect
// if we can't
public void start() { short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code);
handlerThread.start(); if (keyCode != 0) {
} listener.keyboardEvent(event.value != 0, keyCode);
}
public void stop() { break;
// Close the fd. It doesn't matter if this races }
// with the handler thread. We'll close this out from break;
// under the thread to wake it up
if (fd != -1) { case EvdevEvent.EV_MSC:
EvdevReader.close(fd); break;
} }
}
shutdown = true; } finally {
handlerThread.interrupt(); // Release our grab
EvdevReader.ungrab(fd);
try { }
handlerThread.join(); } finally {
} catch (InterruptedException ignored) {} // Close the file
} EvdevReader.close(fd);
}
public void notifyDeleted() { }
stop(); };
}
public EvdevHandler(String absolutePath, EvdevListener listener) {
this.absolutePath = absolutePath;
this.listener = listener;
}
public void start() {
handlerThread.start();
}
public void stop() {
// Close the fd. It doesn't matter if this races
// with the handler thread. We'll close this out from
// under the thread to wake it up
if (fd != -1) {
EvdevReader.close(fd);
}
shutdown = true;
handlerThread.interrupt();
try {
handlerThread.join();
} catch (InterruptedException ignored) {}
}
public void notifyDeleted() {
stop();
}
} }
@@ -1,12 +1,12 @@
package com.limelight.binding.input.evdev; package com.limelight.binding.input.evdev;
public interface EvdevListener { public interface EvdevListener {
public static final int BUTTON_LEFT = 1; public static final int BUTTON_LEFT = 1;
public static final int BUTTON_MIDDLE = 2; public static final int BUTTON_MIDDLE = 2;
public static final int BUTTON_RIGHT = 3; public static final int BUTTON_RIGHT = 3;
public void mouseMove(int deltaX, int deltaY); public void mouseMove(int deltaX, int deltaY);
public void mouseButtonEvent(int buttonId, boolean down); public void mouseButtonEvent(int buttonId, boolean down);
public void mouseScroll(byte amount); public void mouseScroll(byte amount);
public void keyboardEvent(boolean buttonDown, short keyCode); public void keyboardEvent(boolean buttonDown, short keyCode);
} }
@@ -8,9 +8,9 @@ import java.util.Locale;
import com.limelight.LimeLog; import com.limelight.LimeLog;
public class EvdevReader { public class EvdevReader {
static { static {
System.loadLibrary("evdev_reader"); System.loadLibrary("evdev_reader");
} }
public static void patchSeLinuxPolicies() { public static void patchSeLinuxPolicies() {
// //
@@ -26,80 +26,80 @@ public class EvdevReader {
// 4.4 and later to do live SELinux policy changes. // 4.4 and later to do live SELinux policy changes.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
EvdevShell shell = EvdevShell.getInstance(); EvdevShell shell = EvdevShell.getInstance();
shell.runCommand("supolicy --live \"allow untrusted_app input_device dir { getattr read search }\" " + shell.runCommand("supolicy --live \"allow untrusted_app input_device dir { open getattr read search }\" " +
"\"allow untrusted_app input_device chr_file { open read write ioctl }\""); "\"allow untrusted_app input_device chr_file { open read write ioctl }\"");
} }
} }
// Requires root to chmod /dev/input/eventX // Requires root to chmod /dev/input/eventX
public static void setPermissions(String[] files, int octalPermissions) { public static void setPermissions(String[] files, int octalPermissions) {
EvdevShell shell = EvdevShell.getInstance(); EvdevShell shell = EvdevShell.getInstance();
for (String file : files) { for (String file : files) {
shell.runCommand(String.format((Locale)null, "chmod %o %s", octalPermissions, file)); shell.runCommand(String.format((Locale)null, "chmod %o %s", octalPermissions, file));
} }
} }
// Returns the fd to be passed to other function or -1 on error // Returns the fd to be passed to other function or -1 on error
public static native int open(String fileName); public static native int open(String fileName);
// Prevent other apps (including Android itself) from using the device while "grabbed" // Prevent other apps (including Android itself) from using the device while "grabbed"
public static native boolean grab(int fd); public static native boolean grab(int fd);
public static native boolean ungrab(int fd); public static native boolean ungrab(int fd);
// Used for checking device capabilities // Used for checking device capabilities
public static native boolean hasRelAxis(int fd, short axis); public static native boolean hasRelAxis(int fd, short axis);
public static native boolean hasAbsAxis(int fd, short axis); public static native boolean hasAbsAxis(int fd, short axis);
public static native boolean hasKey(int fd, short key); public static native boolean hasKey(int fd, short key);
public static boolean isMouse(int fd) { public static boolean isMouse(int fd) {
// This is the same check that Android does in EventHub.cpp // This is the same check that Android does in EventHub.cpp
return hasRelAxis(fd, EvdevEvent.REL_X) && return hasRelAxis(fd, EvdevEvent.REL_X) &&
hasRelAxis(fd, EvdevEvent.REL_Y) && hasRelAxis(fd, EvdevEvent.REL_Y) &&
hasKey(fd, EvdevEvent.BTN_LEFT); hasKey(fd, EvdevEvent.BTN_LEFT);
} }
public static boolean isAlphaKeyboard(int fd) { public static boolean isAlphaKeyboard(int fd) {
// This is the same check that Android does in EventHub.cpp // This is the same check that Android does in EventHub.cpp
return hasKey(fd, EvdevEvent.KEY_Q); return hasKey(fd, EvdevEvent.KEY_Q);
} }
public static boolean isGamepad(int fd) { public static boolean isGamepad(int fd) {
return hasKey(fd, EvdevEvent.BTN_GAMEPAD); return hasKey(fd, EvdevEvent.BTN_GAMEPAD);
} }
// Returns the bytes read or -1 on error // Returns the bytes read or -1 on error
private static native int read(int fd, byte[] buffer); private static native int read(int fd, byte[] buffer);
// Takes a byte buffer to use to read the output into. // Takes a byte buffer to use to read the output into.
// This buffer MUST be in native byte order and at least // This buffer MUST be in native byte order and at least
// EVDEV_MAX_EVENT_SIZE bytes long. // EVDEV_MAX_EVENT_SIZE bytes long.
public static EvdevEvent read(int fd, ByteBuffer buffer) { public static EvdevEvent read(int fd, ByteBuffer buffer) {
int bytesRead = read(fd, buffer.array()); int bytesRead = read(fd, buffer.array());
if (bytesRead < 0) { if (bytesRead < 0) {
LimeLog.warning("Failed to read: "+bytesRead); LimeLog.warning("Failed to read: "+bytesRead);
return null; return null;
} }
else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) { else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
LimeLog.warning("Short read: "+bytesRead); LimeLog.warning("Short read: "+bytesRead);
return null; return null;
} }
buffer.limit(bytesRead); buffer.limit(bytesRead);
buffer.rewind(); buffer.rewind();
// Throw away the time stamp // Throw away the time stamp
if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) { if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
buffer.getLong(); buffer.getLong();
buffer.getLong(); buffer.getLong();
} else { } else {
buffer.getInt(); buffer.getInt();
buffer.getInt(); buffer.getInt();
} }
return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt()); return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt());
} }
// Closes the fd from open() // Closes the fd from open()
public static native int close(int fd); public static native int close(int fd);
} }
@@ -4,136 +4,136 @@ import android.view.KeyEvent;
public class EvdevTranslator { public class EvdevTranslator {
public static final short EVDEV_KEY_CODES[] = { private static final short[] EVDEV_KEY_CODES = {
0, //KeyEvent.VK_RESERVED 0, //KeyEvent.VK_RESERVED
KeyEvent.KEYCODE_ESCAPE, KeyEvent.KEYCODE_ESCAPE,
KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_1,
KeyEvent.KEYCODE_2, KeyEvent.KEYCODE_2,
KeyEvent.KEYCODE_3, KeyEvent.KEYCODE_3,
KeyEvent.KEYCODE_4, KeyEvent.KEYCODE_4,
KeyEvent.KEYCODE_5, KeyEvent.KEYCODE_5,
KeyEvent.KEYCODE_6, KeyEvent.KEYCODE_6,
KeyEvent.KEYCODE_7, KeyEvent.KEYCODE_7,
KeyEvent.KEYCODE_8, KeyEvent.KEYCODE_8,
KeyEvent.KEYCODE_9, KeyEvent.KEYCODE_9,
KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_0,
KeyEvent.KEYCODE_MINUS, KeyEvent.KEYCODE_MINUS,
KeyEvent.KEYCODE_EQUALS, KeyEvent.KEYCODE_EQUALS,
KeyEvent.KEYCODE_DEL, KeyEvent.KEYCODE_DEL,
KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_TAB,
KeyEvent.KEYCODE_Q, KeyEvent.KEYCODE_Q,
KeyEvent.KEYCODE_W, KeyEvent.KEYCODE_W,
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_E,
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_R,
KeyEvent.KEYCODE_T, KeyEvent.KEYCODE_T,
KeyEvent.KEYCODE_Y, KeyEvent.KEYCODE_Y,
KeyEvent.KEYCODE_U, KeyEvent.KEYCODE_U,
KeyEvent.KEYCODE_I, KeyEvent.KEYCODE_I,
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_O,
KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_P,
KeyEvent.KEYCODE_LEFT_BRACKET, KeyEvent.KEYCODE_LEFT_BRACKET,
KeyEvent.KEYCODE_RIGHT_BRACKET, KeyEvent.KEYCODE_RIGHT_BRACKET,
KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_ENTER,
KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_CTRL_LEFT,
KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_A,
KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_S,
KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_D,
KeyEvent.KEYCODE_F, KeyEvent.KEYCODE_F,
KeyEvent.KEYCODE_G, KeyEvent.KEYCODE_G,
KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_H,
KeyEvent.KEYCODE_J, KeyEvent.KEYCODE_J,
KeyEvent.KEYCODE_K, KeyEvent.KEYCODE_K,
KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_L,
KeyEvent.KEYCODE_SEMICOLON, KeyEvent.KEYCODE_SEMICOLON,
KeyEvent.KEYCODE_APOSTROPHE, KeyEvent.KEYCODE_APOSTROPHE,
KeyEvent.KEYCODE_GRAVE, KeyEvent.KEYCODE_GRAVE,
KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SHIFT_LEFT,
KeyEvent.KEYCODE_BACKSLASH, KeyEvent.KEYCODE_BACKSLASH,
KeyEvent.KEYCODE_Z, KeyEvent.KEYCODE_Z,
KeyEvent.KEYCODE_X, KeyEvent.KEYCODE_X,
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_C,
KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_V,
KeyEvent.KEYCODE_B, KeyEvent.KEYCODE_B,
KeyEvent.KEYCODE_N, KeyEvent.KEYCODE_N,
KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_M,
KeyEvent.KEYCODE_COMMA, KeyEvent.KEYCODE_COMMA,
KeyEvent.KEYCODE_PERIOD, KeyEvent.KEYCODE_PERIOD,
KeyEvent.KEYCODE_SLASH, KeyEvent.KEYCODE_SLASH,
KeyEvent.KEYCODE_SHIFT_RIGHT, KeyEvent.KEYCODE_SHIFT_RIGHT,
KeyEvent.KEYCODE_NUMPAD_MULTIPLY, KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_ALT_LEFT,
KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_SPACE,
KeyEvent.KEYCODE_CAPS_LOCK, KeyEvent.KEYCODE_CAPS_LOCK,
KeyEvent.KEYCODE_F1, KeyEvent.KEYCODE_F1,
KeyEvent.KEYCODE_F2, KeyEvent.KEYCODE_F2,
KeyEvent.KEYCODE_F3, KeyEvent.KEYCODE_F3,
KeyEvent.KEYCODE_F4, KeyEvent.KEYCODE_F4,
KeyEvent.KEYCODE_F5, KeyEvent.KEYCODE_F5,
KeyEvent.KEYCODE_F6, KeyEvent.KEYCODE_F6,
KeyEvent.KEYCODE_F7, KeyEvent.KEYCODE_F7,
KeyEvent.KEYCODE_F8, KeyEvent.KEYCODE_F8,
KeyEvent.KEYCODE_F9, KeyEvent.KEYCODE_F9,
KeyEvent.KEYCODE_F10, KeyEvent.KEYCODE_F10,
KeyEvent.KEYCODE_NUM_LOCK, KeyEvent.KEYCODE_NUM_LOCK,
KeyEvent.KEYCODE_SCROLL_LOCK, KeyEvent.KEYCODE_SCROLL_LOCK,
KeyEvent.KEYCODE_NUMPAD_7, KeyEvent.KEYCODE_NUMPAD_7,
KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_NUMPAD_8,
KeyEvent.KEYCODE_NUMPAD_9, KeyEvent.KEYCODE_NUMPAD_9,
KeyEvent.KEYCODE_NUMPAD_SUBTRACT, KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_NUMPAD_4,
KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_NUMPAD_5,
KeyEvent.KEYCODE_NUMPAD_6, KeyEvent.KEYCODE_NUMPAD_6,
KeyEvent.KEYCODE_NUMPAD_ADD, KeyEvent.KEYCODE_NUMPAD_ADD,
KeyEvent.KEYCODE_NUMPAD_1, KeyEvent.KEYCODE_NUMPAD_1,
KeyEvent.KEYCODE_NUMPAD_2, KeyEvent.KEYCODE_NUMPAD_2,
KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_NUMPAD_3,
KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_NUMPAD_0,
KeyEvent.KEYCODE_NUMPAD_DOT, KeyEvent.KEYCODE_NUMPAD_DOT,
0, 0,
0, //KeyEvent.VK_ZENKAKUHANKAKU, 0, //KeyEvent.VK_ZENKAKUHANKAKU,
0, //KeyEvent.VK_102ND, 0, //KeyEvent.VK_102ND,
KeyEvent.KEYCODE_F11, KeyEvent.KEYCODE_F11,
KeyEvent.KEYCODE_F12, KeyEvent.KEYCODE_F12,
0, //KeyEvent.VK_RO, 0, //KeyEvent.VK_RO,
0, //KeyEvent.VK_KATAKANA, 0, //KeyEvent.VK_KATAKANA,
0, //KeyEvent.VK_HIRAGANA, 0, //KeyEvent.VK_HIRAGANA,
0, //KeyEvent.VK_HENKAN, 0, //KeyEvent.VK_HENKAN,
0, //KeyEvent.VK_KATAKANAHIRAGANA, 0, //KeyEvent.VK_KATAKANAHIRAGANA,
0, //KeyEvent.VK_MUHENKAN, 0, //KeyEvent.VK_MUHENKAN,
0, //KeyEvent.VK_KPJPCOMMA, 0, //KeyEvent.VK_KPJPCOMMA,
KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER,
KeyEvent.KEYCODE_CTRL_RIGHT, KeyEvent.KEYCODE_CTRL_RIGHT,
KeyEvent.KEYCODE_NUMPAD_DIVIDE, KeyEvent.KEYCODE_NUMPAD_DIVIDE,
KeyEvent.KEYCODE_SYSRQ, KeyEvent.KEYCODE_SYSRQ,
KeyEvent.KEYCODE_ALT_RIGHT, KeyEvent.KEYCODE_ALT_RIGHT,
0, //KeyEvent.VK_LINEFEED, 0, //KeyEvent.VK_LINEFEED,
KeyEvent.KEYCODE_HOME, KeyEvent.KEYCODE_HOME,
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_PAGE_UP, KeyEvent.KEYCODE_PAGE_UP,
KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_MOVE_END, KeyEvent.KEYCODE_MOVE_END,
KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_PAGE_DOWN, KeyEvent.KEYCODE_PAGE_DOWN,
KeyEvent.KEYCODE_INSERT, KeyEvent.KEYCODE_INSERT,
KeyEvent.KEYCODE_FORWARD_DEL, KeyEvent.KEYCODE_FORWARD_DEL,
0, //KeyEvent.VK_MACRO, 0, //KeyEvent.VK_MACRO,
0, //KeyEvent.VK_MUTE, 0, //KeyEvent.VK_MUTE,
0, //KeyEvent.VK_VOLUMEDOWN, 0, //KeyEvent.VK_VOLUMEDOWN,
0, //KeyEvent.VK_VOLUMEUP, 0, //KeyEvent.VK_VOLUMEUP,
0, //KeyEvent.VK_POWER, /* SC System Power Down */ 0, //KeyEvent.VK_POWER, /* SC System Power Down */
KeyEvent.KEYCODE_NUMPAD_EQUALS, KeyEvent.KEYCODE_NUMPAD_EQUALS,
0, //KeyEvent.VK_KPPLUSMINUS, 0, //KeyEvent.VK_KPPLUSMINUS,
KeyEvent.KEYCODE_BREAK, KeyEvent.KEYCODE_BREAK,
0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */ 0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */
}; };
public static short translateEvdevKeyCode(short evdevKeyCode) { public static short translateEvdevKeyCode(short evdevKeyCode) {
if (evdevKeyCode < EVDEV_KEY_CODES.length) { if (evdevKeyCode < EVDEV_KEY_CODES.length) {
return EVDEV_KEY_CODES[evdevKeyCode]; return EVDEV_KEY_CODES[evdevKeyCode];
} }
return 0; return 0;
} }
} }
@@ -10,115 +10,115 @@ import android.os.FileObserver;
@SuppressWarnings("ALL") @SuppressWarnings("ALL")
public class EvdevWatcher { public class EvdevWatcher {
private static final String PATH = "/dev/input"; private static final String PATH = "/dev/input";
private static final String REQUIRED_FILE_PREFIX = "event"; private static final String REQUIRED_FILE_PREFIX = "event";
private final HashMap<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>(); private final HashMap<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>();
private boolean shutdown = false; private boolean shutdown = false;
private boolean init = false; private boolean init = false;
private boolean ungrabbed = false; private boolean ungrabbed = false;
private EvdevListener listener; private EvdevListener listener;
private Thread startThread; private Thread startThread;
private static boolean patchedSeLinuxPolicies = false; private static boolean patchedSeLinuxPolicies = false;
private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) { private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) {
@Override @Override
public void onEvent(int event, String fileName) { public void onEvent(int event, String fileName) {
if (fileName == null) { if (fileName == null) {
return; return;
} }
if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) { if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) {
return; return;
} }
synchronized (handlers) { synchronized (handlers) {
if (shutdown) { if (shutdown) {
return; return;
} }
if ((event & FileObserver.CREATE) != 0) { if ((event & FileObserver.CREATE) != 0) {
LimeLog.info("Starting evdev handler for "+fileName); LimeLog.info("Starting evdev handler for "+fileName);
if (!init) { if (!init) {
// If this a real new device, update permissions again so we can read it // If this a real new device, update permissions again so we can read it
EvdevReader.setPermissions(new String[]{PATH + "/" + fileName}, 0666); EvdevReader.setPermissions(new String[]{PATH + "/" + fileName}, 0666);
} }
EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener); EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener);
// If we're ungrabbed now, don't start the handler // If we're ungrabbed now, don't start the handler
if (!ungrabbed) { if (!ungrabbed) {
handler.start(); handler.start();
} }
handlers.put(fileName, handler); handlers.put(fileName, handler);
} }
if ((event & FileObserver.DELETE) != 0) { if ((event & FileObserver.DELETE) != 0) {
LimeLog.info("Halting evdev handler for "+fileName); LimeLog.info("Halting evdev handler for "+fileName);
EvdevHandler handler = handlers.remove(fileName); EvdevHandler handler = handlers.remove(fileName);
if (handler != null) { if (handler != null) {
handler.notifyDeleted(); handler.notifyDeleted();
} }
} }
} }
} }
}; };
public EvdevWatcher(EvdevListener listener) { public EvdevWatcher(EvdevListener listener) {
this.listener = listener; this.listener = listener;
} }
private File[] rundownWithPermissionsChange(int newPermissions) { private File[] rundownWithPermissionsChange(int newPermissions) {
// Rundown existing files // Rundown existing files
File devInputDir = new File(PATH); File devInputDir = new File(PATH);
File[] files = devInputDir.listFiles(); File[] files = devInputDir.listFiles();
if (files == null) { if (files == null) {
return new File[0]; return new File[0];
} }
// Set desired permissions // Set desired permissions
String[] filePaths = new String[files.length]; String[] filePaths = new String[files.length];
for (int i = 0; i < files.length; i++) { for (int i = 0; i < files.length; i++) {
filePaths[i] = files[i].getAbsolutePath(); filePaths[i] = files[i].getAbsolutePath();
} }
EvdevReader.setPermissions(filePaths, newPermissions); EvdevReader.setPermissions(filePaths, newPermissions);
return files; return files;
} }
public void ungrabAll() { public void ungrabAll() {
synchronized (handlers) { synchronized (handlers) {
// Note that we're ungrabbed for now // Note that we're ungrabbed for now
ungrabbed = true; ungrabbed = true;
// Stop all handlers // Stop all handlers
for (EvdevHandler handler : handlers.values()) { for (EvdevHandler handler : handlers.values()) {
handler.stop(); handler.stop();
} }
} }
} }
public void regrabAll() { public void regrabAll() {
synchronized (handlers) { synchronized (handlers) {
// We're regrabbing everything now // We're regrabbing everything now
ungrabbed = false; ungrabbed = false;
for (Map.Entry<String, EvdevHandler> entry : handlers.entrySet()) { for (Map.Entry<String, EvdevHandler> entry : handlers.entrySet()) {
// We need to recreate each entry since we can't reuse a stopped one // We need to recreate each entry since we can't reuse a stopped one
entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener)); entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener));
entry.getValue().start(); entry.getValue().start();
} }
} }
} }
public void start() { public void start() {
startThread = new Thread() { startThread = new Thread() {
@Override @Override
public void run() { public void run() {
// Initialize the root shell // Initialize the root shell
EvdevShell.getInstance().startShell(); EvdevShell.getInstance().startShell();
@@ -128,61 +128,61 @@ public class EvdevWatcher {
patchedSeLinuxPolicies = true; patchedSeLinuxPolicies = true;
} }
// List all files and allow us access // List all files and allow us access
File[] files = rundownWithPermissionsChange(0666); File[] files = rundownWithPermissionsChange(0666);
init = true; init = true;
for (File f : files) { for (File f : files) {
observer.onEvent(FileObserver.CREATE, f.getName()); observer.onEvent(FileObserver.CREATE, f.getName());
} }
// Done with initial onEvent calls // Done with initial onEvent calls
init = false; init = false;
// Start watching for new files // Start watching for new files
observer.startWatching(); observer.startWatching();
synchronized (startThread) { synchronized (startThread) {
// Wait to be awoken again by shutdown() // Wait to be awoken again by shutdown()
try { try {
startThread.wait(); startThread.wait();
} catch (InterruptedException e) {} } catch (InterruptedException e) {}
} }
// Giveup eventX permissions // Giveup eventX permissions
rundownWithPermissionsChange(0660); rundownWithPermissionsChange(0660);
// Kill the root shell // Kill the root shell
try { try {
EvdevShell.getInstance().stopShell(); EvdevShell.getInstance().stopShell();
} catch (InterruptedException e) {} } catch (InterruptedException e) {}
} }
}; };
startThread.start(); startThread.start();
} }
public void shutdown() { public void shutdown() {
// Let start thread cleanup on it's own sweet time // Let start thread cleanup on it's own sweet time
synchronized (startThread) { synchronized (startThread) {
startThread.notify(); startThread.notify();
} }
// Stop the observer // Stop the observer
observer.stopWatching(); observer.stopWatching();
synchronized (handlers) { synchronized (handlers) {
// Stop creating new handlers // Stop creating new handlers
shutdown = true; shutdown = true;
// If we've already ungrabbed, there's nothing else to do // If we've already ungrabbed, there's nothing else to do
if (ungrabbed) { if (ungrabbed) {
return; return;
} }
// Stop all handlers // Stop all handlers
for (EvdevHandler handler : handlers.values()) { for (EvdevHandler handler : handlers.values()) {
handler.stop(); handler.stop();
} }
} }
} }
} }
@@ -20,140 +20,140 @@ import com.limelight.nvstream.av.video.cpu.AvcDecoder;
@SuppressWarnings("EmptyCatchBlock") @SuppressWarnings("EmptyCatchBlock")
public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer { public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
private Thread rendererThread, decoderThread; private Thread rendererThread, decoderThread;
private int targetFps; private int targetFps;
private static final int DECODER_BUFFER_SIZE = 92*1024;
private ByteBuffer decoderBuffer;
// Only sleep if the difference is above this value
private static final int WAIT_CEILING_MS = 5;
private static final int LOW_PERF = 1;
private static final int MED_PERF = 2;
private static final int HIGH_PERF = 3;
private int totalFrames;
private long totalTimeMs;
private int cpuCount = Runtime.getRuntime().availableProcessors();
@SuppressWarnings("unused")
private int findOptimalPerformanceLevel() {
StringBuilder cpuInfo = new StringBuilder();
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(new File("/proc/cpuinfo")));
for (;;) {
int ch = br.read();
if (ch == -1)
break;
cpuInfo.append((char)ch);
}
// Here we're doing very simple heuristics based on CPU model
String cpuInfoStr = cpuInfo.toString();
// We order them from greatest to least for proper detection private static final int DECODER_BUFFER_SIZE = 92*1024;
// of devices with multiple sets of cores (like Exynos 5 Octa) private ByteBuffer decoderBuffer;
// TODO Make this better (only even kind of works on ARM)
if (Build.FINGERPRINT.contains("generic")) {
// Emulator
return LOW_PERF;
}
else if (cpuInfoStr.contains("0xc0f")) {
// Cortex-A15
return MED_PERF;
}
else if (cpuInfoStr.contains("0xc09")) {
// Cortex-A9
return LOW_PERF;
}
else if (cpuInfoStr.contains("0xc07")) {
// Cortex-A7
return LOW_PERF;
}
else {
// Didn't have anything we're looking for
return MED_PERF;
}
} catch (IOException e) {
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {}
}
}
// Couldn't read cpuinfo, so assume medium
return MED_PERF;
}
@Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.targetFps = redrawRate;
int perfLevel = LOW_PERF; //findOptimalPerformanceLevel();
int threadCount;
int avcFlags = 0;
switch (perfLevel) {
case HIGH_PERF:
// Single threaded low latency decode is ideal but hard to acheive
avcFlags = AvcDecoder.LOW_LATENCY_DECODE;
threadCount = 1;
break;
case LOW_PERF: // Only sleep if the difference is above this value
// Disable the loop filter for performance reasons private static final int WAIT_CEILING_MS = 5;
avcFlags = AvcDecoder.FAST_BILINEAR_FILTERING;
// Use plenty of threads to try to utilize the CPU as best we can
threadCount = cpuCount - 1;
break;
default: private static final int LOW_PERF = 1;
case MED_PERF: private static final int MED_PERF = 2;
avcFlags = AvcDecoder.BILINEAR_FILTERING; private static final int HIGH_PERF = 3;
// Only use 2 threads to minimize frame processing latency private int totalFrames;
threadCount = 2; private long totalTimeMs;
break;
} private final int cpuCount = Runtime.getRuntime().availableProcessors();
// If the user wants quality, we'll remove the low IQ flags @SuppressWarnings("unused")
if ((drFlags & VideoDecoderRenderer.FLAG_PREFER_QUALITY) != 0) { private int findOptimalPerformanceLevel() {
// Make sure the loop filter is enabled StringBuilder cpuInfo = new StringBuilder();
avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER; BufferedReader br = null;
try {
// Disable the non-compliant speed optimizations br = new BufferedReader(new FileReader(new File("/proc/cpuinfo")));
avcFlags &= ~AvcDecoder.FAST_DECODE; for (;;) {
int ch = br.read();
LimeLog.info("Using high quality decoding"); if (ch == -1)
} break;
cpuInfo.append((char)ch);
SurfaceHolder sh = (SurfaceHolder)renderTarget; }
sh.setFormat(PixelFormat.RGBX_8888);
// Here we're doing very simple heuristics based on CPU model
int err = AvcDecoder.init(width, height, avcFlags, threadCount); String cpuInfoStr = cpuInfo.toString();
if (err != 0) {
throw new IllegalStateException("AVC decoder initialization failure: "+err); // We order them from greatest to least for proper detection
} // of devices with multiple sets of cores (like Exynos 5 Octa)
// TODO Make this better (only even kind of works on ARM)
if (!AvcDecoder.setRenderTarget(sh.getSurface())) { if (Build.FINGERPRINT.contains("generic")) {
// Emulator
return LOW_PERF;
}
else if (cpuInfoStr.contains("0xc0f")) {
// Cortex-A15
return MED_PERF;
}
else if (cpuInfoStr.contains("0xc09")) {
// Cortex-A9
return LOW_PERF;
}
else if (cpuInfoStr.contains("0xc07")) {
// Cortex-A7
return LOW_PERF;
}
else {
// Didn't have anything we're looking for
return MED_PERF;
}
} catch (IOException e) {
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {}
}
}
// Couldn't read cpuinfo, so assume medium
return MED_PERF;
}
@Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.targetFps = redrawRate;
int perfLevel = LOW_PERF; //findOptimalPerformanceLevel();
int threadCount;
int avcFlags = 0;
switch (perfLevel) {
case HIGH_PERF:
// Single threaded low latency decode is ideal but hard to acheive
avcFlags = AvcDecoder.LOW_LATENCY_DECODE;
threadCount = 1;
break;
case LOW_PERF:
// Disable the loop filter for performance reasons
avcFlags = AvcDecoder.FAST_BILINEAR_FILTERING;
// Use plenty of threads to try to utilize the CPU as best we can
threadCount = cpuCount - 1;
break;
default:
case MED_PERF:
avcFlags = AvcDecoder.BILINEAR_FILTERING;
// Only use 2 threads to minimize frame processing latency
threadCount = 2;
break;
}
// If the user wants quality, we'll remove the low IQ flags
if ((drFlags & VideoDecoderRenderer.FLAG_PREFER_QUALITY) != 0) {
// Make sure the loop filter is enabled
avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER;
// Disable the non-compliant speed optimizations
avcFlags &= ~AvcDecoder.FAST_DECODE;
LimeLog.info("Using high quality decoding");
}
SurfaceHolder sh = (SurfaceHolder)renderTarget;
sh.setFormat(PixelFormat.RGBX_8888);
int err = AvcDecoder.init(width, height, avcFlags, threadCount);
if (err != 0) {
throw new IllegalStateException("AVC decoder initialization failure: "+err);
}
if (!AvcDecoder.setRenderTarget(sh.getSurface())) {
return false; return false;
} }
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
LimeLog.info("Using software decoding (performance level: "+perfLevel+")");
return true;
}
@Override decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
public boolean start(final VideoDepacketizer depacketizer) {
LimeLog.info("Using software decoding (performance level: "+perfLevel+")");
return true;
}
@Override
public boolean start(final VideoDepacketizer depacketizer) {
decoderThread = new Thread() { decoderThread = new Thread() {
@Override @Override
public void run() { public void run() {
@@ -174,112 +174,112 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
decoderThread.setPriority(Thread.MAX_PRIORITY - 1); decoderThread.setPriority(Thread.MAX_PRIORITY - 1);
decoderThread.start(); decoderThread.start();
rendererThread = new Thread() { rendererThread = new Thread() {
@Override @Override
public void run() { public void run() {
long nextFrameTime = System.currentTimeMillis(); long nextFrameTime = System.currentTimeMillis();
DecodeUnit du; DecodeUnit du;
while (!isInterrupted()) while (!isInterrupted())
{ {
long diff = nextFrameTime - System.currentTimeMillis(); long diff = nextFrameTime - System.currentTimeMillis();
if (diff > WAIT_CEILING_MS) { if (diff > WAIT_CEILING_MS) {
try { try {
Thread.sleep(diff - WAIT_CEILING_MS); Thread.sleep(diff - WAIT_CEILING_MS);
} catch (InterruptedException e) { } catch (InterruptedException e) {
return; return;
} }
continue; continue;
} }
nextFrameTime = computePresentationTimeMs(targetFps); nextFrameTime = computePresentationTimeMs(targetFps);
AvcDecoder.redraw(); AvcDecoder.redraw();
} }
} }
}; };
rendererThread.setName("Video - Renderer (CPU)"); rendererThread.setName("Video - Renderer (CPU)");
rendererThread.setPriority(Thread.MAX_PRIORITY); rendererThread.setPriority(Thread.MAX_PRIORITY);
rendererThread.start(); rendererThread.start();
return true; return true;
} }
private long computePresentationTimeMs(int frameRate) {
return System.currentTimeMillis() + (1000 / frameRate);
}
@Override private long computePresentationTimeMs(int frameRate) {
public void stop() { return System.currentTimeMillis() + (1000 / frameRate);
rendererThread.interrupt(); }
@Override
public void stop() {
rendererThread.interrupt();
decoderThread.interrupt(); decoderThread.interrupt();
try { try {
rendererThread.join(); rendererThread.join();
} catch (InterruptedException e) { } } catch (InterruptedException e) { }
try { try {
decoderThread.join(); decoderThread.join();
} catch (InterruptedException e) { } } catch (InterruptedException e) { }
} }
@Override @Override
public void release() { public void release() {
AvcDecoder.destroy(); AvcDecoder.destroy();
} }
private boolean submitDecodeUnit(DecodeUnit decodeUnit) { private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
byte[] data; byte[] data;
// Use the reserved decoder buffer if this decode unit will fit
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
decoderBuffer.clear();
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
}
data = decoderBuffer.array();
}
else {
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
int offset = 0;
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
offset += bbd.length;
}
}
boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
if (success) {
long timeAfterDecode = System.currentTimeMillis();
// Add delta time to the totals (excluding probable outliers)
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
if (delta >= 0 && delta < 1000) {
totalTimeMs += delta;
totalFrames++;
}
}
return success;
}
@Override // Use the reserved decoder buffer if this decode unit will fit
public int getCapabilities() { if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
return 0; decoderBuffer.clear();
}
@Override for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
public int getAverageDecoderLatency() { decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
return 0; }
}
@Override data = decoderBuffer.array();
public int getAverageEndToEndLatency() { }
if (totalFrames == 0) { else {
return 0; data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
}
return (int)(totalTimeMs / totalFrames); int offset = 0;
} for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
offset += bbd.length;
}
}
boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
if (success) {
long timeAfterDecode = System.currentTimeMillis();
// Add delta time to the totals (excluding probable outliers)
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
if (delta >= 0 && delta < 1000) {
totalTimeMs += delta;
totalFrames++;
}
}
return success;
}
@Override
public int getCapabilities() {
return 0;
}
@Override
public int getAverageDecoderLatency() {
return 0;
}
@Override
public int getAverageEndToEndLatency() {
if (totalFrames == 0) {
return 0;
}
return (int)(totalTimeMs / totalFrames);
}
@Override @Override
public String getDecoderName() { public String getDecoderName() {
@@ -5,75 +5,75 @@ import com.limelight.nvstream.av.video.VideoDepacketizer;
public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer { public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer {
private EnhancedDecoderRenderer decoderRenderer; private EnhancedDecoderRenderer decoderRenderer;
@Override
public void release() {
if (decoderRenderer != null) {
decoderRenderer.release();
}
}
@Override @Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) { public void release() {
if (decoderRenderer == null) { if (decoderRenderer != null) {
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized"); decoderRenderer.release();
} }
return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags); }
}
public void initializeWithFlags(int drFlags) {
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
MediaCodecHelper.findProbableSafeDecoder() != null)) {
decoderRenderer = new MediaCodecDecoderRenderer();
}
else {
decoderRenderer = new AndroidCpuDecoderRenderer();
}
}
public boolean isHardwareAccelerated() {
if (decoderRenderer == null) {
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
}
return (decoderRenderer instanceof MediaCodecDecoderRenderer);
}
@Override @Override
public boolean start(VideoDepacketizer depacketizer) { public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
return decoderRenderer.start(depacketizer); if (decoderRenderer == null) {
} throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
}
return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
}
@Override public void initializeWithFlags(int drFlags) {
public void stop() { if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
decoderRenderer.stop(); ((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
} MediaCodecHelper.findProbableSafeDecoder() != null)) {
decoderRenderer = new MediaCodecDecoderRenderer();
}
else {
decoderRenderer = new AndroidCpuDecoderRenderer();
}
}
@Override public boolean isHardwareAccelerated() {
public int getCapabilities() { if (decoderRenderer == null) {
return decoderRenderer.getCapabilities(); throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
} }
return (decoderRenderer instanceof MediaCodecDecoderRenderer);
}
@Override @Override
public int getAverageDecoderLatency() { public boolean start(VideoDepacketizer depacketizer) {
if (decoderRenderer != null) { return decoderRenderer.start(depacketizer);
return decoderRenderer.getAverageDecoderLatency(); }
}
else {
return 0;
}
}
@Override @Override
public int getAverageEndToEndLatency() { public void stop() {
if (decoderRenderer != null) { decoderRenderer.stop();
return decoderRenderer.getAverageEndToEndLatency(); }
}
else { @Override
return 0; public int getCapabilities() {
} return decoderRenderer.getCapabilities();
} }
@Override
public int getAverageDecoderLatency() {
if (decoderRenderer != null) {
return decoderRenderer.getAverageDecoderLatency();
}
else {
return 0;
}
}
@Override
public int getAverageEndToEndLatency() {
if (decoderRenderer != null) {
return decoderRenderer.getAverageEndToEndLatency();
}
else {
return 0;
}
}
@Override @Override
public String getDecoderName() { public String getDecoderName() {
File diff suppressed because it is too large. Load diff
@@ -20,12 +20,12 @@ import com.limelight.LimeLog;
public class MediaCodecHelper { public class MediaCodecHelper {
public static final List<String> preferredDecoders; private static final List<String> preferredDecoders;
public static final List<String> blacklistedDecoderPrefixes; private static final List<String> blacklistedDecoderPrefixes;
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes; private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
public static final List<String> whitelistedAdaptiveResolutionPrefixes; private static final List<String> whitelistedAdaptiveResolutionPrefixes;
public static final List<String> baselineProfileHackPrefixes; private static final List<String> baselineProfileHackPrefixes;
static { static {
preferredDecoders = new LinkedList<String>(); preferredDecoders = new LinkedList<String>();
@@ -72,8 +72,8 @@ public class MediaCodecHelper {
@TargetApi(Build.VERSION_CODES.KITKAT) @TargetApi(Build.VERSION_CODES.KITKAT)
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) { public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) {
/* /*
FIXME: Intel's decoder on Nexus Player forces the high latency path if adaptive playback is enabled FIXME: Intel's decoder on Nexus Player forces the high latency path if adaptive playback is enabled
so we'll keep it off for now, since we don't know whether other devices also do the same so we'll keep it off for now, since we don't know whether other devices also do the same
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) { if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
LimeLog.info("Adaptive playback supported (whitelist)"); LimeLog.info("Adaptive playback supported (whitelist)");
@@ -146,7 +146,7 @@ public class MediaCodecHelper {
return str; return str;
} }
public static MediaCodecInfo findPreferredDecoder() { private static MediaCodecInfo findPreferredDecoder() {
// This is a different algorithm than the other findXXXDecoder functions, // This is a different algorithm than the other findXXXDecoder functions,
// because we want to evaluate the decoders in our list's order // because we want to evaluate the decoders in our list's order
// rather than MediaCodecList's order // rather than MediaCodecList's order
@@ -217,7 +217,7 @@ public class MediaCodecHelper {
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly // since some bad decoders can throw IllegalArgumentExceptions unexpectedly
// and we want to be sure all callers are handling this possibility // and we want to be sure all callers are handling this possibility
@SuppressWarnings("RedundantThrows") @SuppressWarnings("RedundantThrows")
public static MediaCodecInfo findKnownSafeDecoder() throws Exception { private static MediaCodecInfo findKnownSafeDecoder() throws Exception {
for (MediaCodecInfo codecInfo : getMediaCodecList()) { for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders // Skip encoders
if (codecInfo.isEncoder()) { if (codecInfo.isEncoder()) {
@@ -17,153 +17,153 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteException;
public class ComputerDatabaseManager { public class ComputerDatabaseManager {
private static final String COMPUTER_DB_NAME = "computers.db"; private static final String COMPUTER_DB_NAME = "computers.db";
private static final String COMPUTER_TABLE_NAME = "Computers"; private static final String COMPUTER_TABLE_NAME = "Computers";
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName"; private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID"; private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp"; private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp"; private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
private static final String MAC_COLUMN_NAME = "Mac"; private static final String MAC_COLUMN_NAME = "Mac";
private SQLiteDatabase computerDb; private SQLiteDatabase computerDb;
public ComputerDatabaseManager(Context c) { public ComputerDatabaseManager(Context c) {
try { try {
// Create or open an existing DB // Create or open an existing DB
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null); computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
} catch (SQLiteException e) { } catch (SQLiteException e) {
// Delete the DB and try again // Delete the DB and try again
c.deleteDatabase(COMPUTER_DB_NAME); c.deleteDatabase(COMPUTER_DB_NAME);
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null); computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
} }
initializeDb(); initializeDb();
} }
public void close() { public void close() {
computerDb.close(); computerDb.close();
} }
private void initializeDb() { private void initializeDb() {
// Create tables if they aren't already there // Create tables if they aren't already there
computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," + computerDb.execSQL(String.format((Locale)null, "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)", " %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
COMPUTER_TABLE_NAME, COMPUTER_TABLE_NAME,
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME)); REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME));
} }
public void deleteComputer(String name) { public void deleteComputer(String name) {
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null); computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
} }
public boolean updateComputer(ComputerDetails details) { public boolean updateComputer(ComputerDetails details) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(COMPUTER_NAME_COLUMN_NAME, details.name); values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString()); values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress()); values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress()); values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
values.put(MAC_COLUMN_NAME, details.macAddress); values.put(MAC_COLUMN_NAME, details.macAddress);
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE); return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
} }
public List<ComputerDetails> getAllComputers() { public List<ComputerDetails> getAllComputers() {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null); Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>(); LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>();
while (c.moveToNext()) { while (c.moveToNext()) {
ComputerDetails details = new ComputerDetails(); ComputerDetails details = new ComputerDetails();
details.name = c.getString(0); details.name = c.getString(0);
String uuidStr = c.getString(1); String uuidStr = c.getString(1);
try { try {
details.uuid = UUID.fromString(uuidStr); details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
// We'll delete this entry // We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name); LimeLog.severe("DB: Corrupted UUID for "+details.name);
} }
try { try {
details.localIp = InetAddress.getByAddress(c.getBlob(2)); details.localIp = InetAddress.getByAddress(c.getBlob(2));
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
// We'll delete this entry // We'll delete this entry
LimeLog.severe("DB: Corrupted local IP for "+details.name); LimeLog.severe("DB: Corrupted local IP for "+details.name);
} }
try { try {
details.remoteIp = InetAddress.getByAddress(c.getBlob(3)); details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
// We'll delete this entry // We'll delete this entry
LimeLog.severe("DB: Corrupted remote IP for "+details.name); LimeLog.severe("DB: Corrupted remote IP for "+details.name);
} }
details.macAddress = c.getString(4); details.macAddress = c.getString(4);
// This signifies we don't have dynamic state (like pair state) // This signifies we don't have dynamic state (like pair state)
details.state = ComputerDetails.State.UNKNOWN; details.state = ComputerDetails.State.UNKNOWN;
details.reachability = ComputerDetails.Reachability.UNKNOWN; details.reachability = ComputerDetails.Reachability.UNKNOWN;
// If a field is corrupt or missing, skip the database entry // If a field is corrupt or missing, skip the database entry
if (details.uuid == null || details.localIp == null || details.remoteIp == null || if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
details.macAddress == null) { details.macAddress == null) {
continue; continue;
} }
computerList.add(details); computerList.add(details);
} }
c.close();
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
c.close();
return null;
}
details.name = c.getString(0); c.close();
String uuidStr = c.getString(1); return computerList;
try { }
details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) { public ComputerDetails getComputerByName(String name) {
// We'll delete this entry Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
LimeLog.severe("DB: Corrupted UUID for "+details.name); ComputerDetails details = new ComputerDetails();
} if (!c.moveToFirst()) {
// No matching computer
try { c.close();
details.localIp = InetAddress.getByAddress(c.getBlob(2)); return null;
} catch (UnknownHostException e) { }
// We'll delete this entry
LimeLog.severe("DB: Corrupted local IP for "+details.name); details.name = c.getString(0);
}
String uuidStr = c.getString(1);
try { try {
details.remoteIp = InetAddress.getByAddress(c.getBlob(3)); details.uuid = UUID.fromString(uuidStr);
} catch (UnknownHostException e) { } catch (IllegalArgumentException e) {
// We'll delete this entry // We'll delete this entry
LimeLog.severe("DB: Corrupted remote IP for "+details.name); LimeLog.severe("DB: Corrupted UUID for "+details.name);
} }
details.macAddress = c.getString(4); try {
details.localIp = InetAddress.getByAddress(c.getBlob(2));
c.close(); } 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);
c.close();
details.state = ComputerDetails.State.UNKNOWN; details.state = ComputerDetails.State.UNKNOWN;
details.reachability = ComputerDetails.Reachability.UNKNOWN; details.reachability = ComputerDetails.Reachability.UNKNOWN;
// If a field is corrupt or missing, delete the database entry // If a field is corrupt or missing, delete the database entry
if (details.uuid == null || details.localIp == null || details.remoteIp == null || if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
details.macAddress == null) { details.macAddress == null) {
deleteComputer(details.name); deleteComputer(details.name);
return null; return null;
} }
return details; return details;
} }
} }
@@ -3,5 +3,5 @@ package com.limelight.computers;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
public interface ComputerManagerListener { public interface ComputerManagerListener {
public void notifyComputerUpdated(ComputerDetails details); public void notifyComputerUpdated(ComputerDetails details);
} }
@@ -1,6 +1,5 @@
package com.limelight.computers; package com.limelight.computers;
import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.StringReader; import java.io.StringReader;
@@ -30,45 +29,43 @@ import android.os.IBinder;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
public class ComputerManagerService extends Service { public class ComputerManagerService extends Service {
private static final int POLLING_PERIOD_MS = 3000; private static final int POLLING_PERIOD_MS = 3000;
private static final int MDNS_QUERY_PERIOD_MS = 1000; private static final int MDNS_QUERY_PERIOD_MS = 1000;
private ComputerManagerBinder binder = new ComputerManagerBinder(); private final ComputerManagerBinder binder = new ComputerManagerBinder();
private ComputerDatabaseManager dbManager; private ComputerDatabaseManager dbManager;
private AtomicInteger dbRefCount = new AtomicInteger(0); private final AtomicInteger dbRefCount = new AtomicInteger(0);
private IdentityManager idManager; private IdentityManager idManager;
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>(); private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
private ComputerManagerListener listener = null; private ComputerManagerListener listener = null;
private AtomicInteger activePolls = new AtomicInteger(0); private final AtomicInteger activePolls = new AtomicInteger(0);
private boolean pollingActive = false; private boolean pollingActive = false;
private DiscoveryService.DiscoveryBinder discoveryBinder; private DiscoveryService.DiscoveryBinder discoveryBinder;
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() { private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) { public void onServiceConnected(ComponentName className, IBinder binder) {
synchronized (discoveryServiceConnection) { synchronized (discoveryServiceConnection) {
DiscoveryService.DiscoveryBinder privateBinder = ((DiscoveryService.DiscoveryBinder)binder); DiscoveryService.DiscoveryBinder privateBinder = ((DiscoveryService.DiscoveryBinder)binder);
// Set us as the event listener
privateBinder.setListener(createDiscoveryListener());
// Signal a possible waiter that we're all setup
discoveryBinder = privateBinder;
discoveryServiceConnection.notifyAll();
}
}
public void onServiceDisconnected(ComponentName className) { // Set us as the event listener
discoveryBinder = null; privateBinder.setListener(createDiscoveryListener());
}
}; // Signal a possible waiter that we're all setup
discoveryBinder = privateBinder;
discoveryServiceConnection.notifyAll();
}
}
public void onServiceDisconnected(ComponentName className) {
discoveryBinder = null;
}
};
// Returns true if the details object was modified // Returns true if the details object was modified
private boolean runPoll(ComputerDetails details) private boolean runPoll(ComputerDetails details, boolean newPc)
{ {
boolean newPc = details.name.isEmpty();
if (!getLocalDatabaseReference()) { if (!getLocalDatabaseReference()) {
return false; return false;
} }
@@ -113,7 +110,7 @@ public class ComputerManagerService extends Service {
public void run() { public void run() {
while (!isInterrupted() && pollingActive) { while (!isInterrupted() && pollingActive) {
// Check if this poll has modified the details // Check if this poll has modified the details
runPoll(details); runPoll(details, false);
// Wait until the next polling interval // Wait until the next polling interval
try { try {
@@ -127,17 +124,17 @@ public class ComputerManagerService extends Service {
t.setName("Polling thread for "+details.localIp.getHostAddress()); t.setName("Polling thread for "+details.localIp.getHostAddress());
return t; return t;
} }
public class ComputerManagerBinder extends Binder { public class ComputerManagerBinder extends Binder {
public void startPolling(ComputerManagerListener listener) { public void startPolling(ComputerManagerListener listener) {
// Polling is active // Polling is active
pollingActive = true; pollingActive = true;
// Set the listener // Set the listener
ComputerManagerService.this.listener = listener; ComputerManagerService.this.listener = listener;
// Start mDNS autodiscovery too // Start mDNS autodiscovery too
discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS); discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS);
synchronized (pollingTuples) { synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) { for (PollingTuple tuple : pollingTuples) {
@@ -151,52 +148,48 @@ public class ComputerManagerService extends Service {
} }
} }
} }
} }
public void waitForReady() { public void waitForReady() {
synchronized (discoveryServiceConnection) { synchronized (discoveryServiceConnection) {
try { try {
while (discoveryBinder == null) { while (discoveryBinder == null) {
// Wait for the bind notification // Wait for the bind notification
discoveryServiceConnection.wait(1000); discoveryServiceConnection.wait(1000);
} }
} catch (InterruptedException ignored) { } catch (InterruptedException ignored) {
} }
} }
} }
public void waitForPollingStopped() { public void waitForPollingStopped() {
while (activePolls.get() != 0) { while (activePolls.get() != 0) {
try { try {
Thread.sleep(250); Thread.sleep(250);
} catch (InterruptedException ignored) {} } catch (InterruptedException ignored) {}
} }
} }
public boolean addComputerBlocking(InetAddress addr) { public boolean addComputerBlocking(InetAddress addr) {
return ComputerManagerService.this.addComputerBlocking(addr); return ComputerManagerService.this.addComputerBlocking(addr);
} }
public void addComputer(InetAddress addr) { public void removeComputer(String name) {
ComputerManagerService.this.addComputer(addr); ComputerManagerService.this.removeComputer(name);
} }
public void removeComputer(String name) { public void stopPolling() {
ComputerManagerService.this.removeComputer(name); // Just call the unbind handler to cleanup
} ComputerManagerService.this.onUnbind(null);
}
public void stopPolling() {
// Just call the unbind handler to cleanup
ComputerManagerService.this.onUnbind(null);
}
public ApplistPoller createAppListPoller(ComputerDetails computer) { public ApplistPoller createAppListPoller(ComputerDetails computer) {
return new ApplistPoller(computer); return new ApplistPoller(computer);
} }
public String getUniqueId() { public String getUniqueId() {
return idManager.getUniqueId(); return idManager.getUniqueId();
} }
public ComputerDetails getComputer(UUID uuid) { public ComputerDetails getComputer(UUID uuid) {
synchronized (pollingTuples) { synchronized (pollingTuples) {
@@ -209,14 +202,14 @@ public class ComputerManagerService extends Service {
return null; return null;
} }
} }
@Override @Override
public boolean onUnbind(Intent intent) { public boolean onUnbind(Intent intent) {
// Stop mDNS autodiscovery // Stop mDNS autodiscovery
discoveryBinder.stopDiscovery(); discoveryBinder.stopDiscovery();
// Stop polling // Stop polling
pollingActive = false; pollingActive = false;
synchronized (pollingTuples) { synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) { for (PollingTuple tuple : pollingTuples) {
@@ -227,56 +220,39 @@ public class ComputerManagerService extends Service {
} }
} }
} }
// Remove the listener
listener = null;
return false;
}
private MdnsDiscoveryListener createDiscoveryListener() {
return new MdnsDiscoveryListener() {
@Override
public void notifyComputerAdded(MdnsComputer computer) {
// Kick off a serverinfo poll on this machine
addComputer(computer.getAddress());
}
@Override // Remove the listener
public void notifyComputerRemoved(MdnsComputer computer) { listener = null;
// Nothing to do here
}
@Override return false;
public void notifyDiscoveryFailure(Exception e) { }
LimeLog.severe("mDNS discovery failed");
e.printStackTrace();
}
};
}
public void addComputer(InetAddress addr) { private MdnsDiscoveryListener createDiscoveryListener() {
// Setup a placeholder return new MdnsDiscoveryListener() {
ComputerDetails fakeDetails = new ComputerDetails(); @Override
fakeDetails.localIp = addr; public void notifyComputerAdded(MdnsComputer computer) {
fakeDetails.remoteIp = addr; // Kick off a serverinfo poll on this machine
fakeDetails.name = ""; addComputerBlocking(computer.getAddress());
}
addTuple(fakeDetails); @Override
} public void notifyComputerRemoved(MdnsComputer computer) {
// Nothing to do here
}
@Override
public void notifyDiscoveryFailure(Exception e) {
LimeLog.severe("mDNS discovery failed");
e.printStackTrace();
}
};
}
private void addTuple(ComputerDetails details) { private void addTuple(ComputerDetails details) {
synchronized (pollingTuples) { synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) { for (PollingTuple tuple : pollingTuples) {
// Check if this is the same computer // Check if this is the same computer
if (tuple.computer == details || if (tuple.computer.uuid.equals(details.uuid)) {
// If there's no name on one of these computers, compare with the local IP
((details.name.isEmpty() || tuple.computer.name.isEmpty()) &&
tuple.computer.localIp.equals(details.localIp)) ||
// If there is a name on both computers, compare with name
((!details.name.isEmpty() && !tuple.computer.name.isEmpty()) &&
tuple.computer.name.equals(details.name))) {
// Update details anyway in case this machine has been re-added by IP // Update details anyway in case this machine has been re-added by IP
// after not being reachable by our existing information // after not being reachable by our existing information
tuple.computer.localIp = details.localIp; tuple.computer.localIp = details.localIp;
@@ -302,18 +278,19 @@ public class ComputerManagerService extends Service {
} }
} }
public boolean addComputerBlocking(InetAddress addr) { public boolean addComputerBlocking(InetAddress addr) {
// Setup a placeholder // Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails(); ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr; fakeDetails.localIp = addr;
fakeDetails.remoteIp = addr; fakeDetails.remoteIp = addr;
fakeDetails.name = "";
// Block while we try to fill the details
// Block while we try to fill the details runPoll(fakeDetails, true);
runPoll(fakeDetails);
// If the machine is reachable, it was successful
// If the machine is reachable, it was successful if (fakeDetails.state == ComputerDetails.State.ONLINE) {
if (fakeDetails.state == ComputerDetails.State.ONLINE) { LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
// Start a polling thread for this machine // Start a polling thread for this machine
addTuple(fakeDetails); addTuple(fakeDetails);
return true; return true;
@@ -321,15 +298,15 @@ public class ComputerManagerService extends Service {
else { else {
return false; return false;
} }
} }
public void removeComputer(String name) { public void removeComputer(String name) {
if (!getLocalDatabaseReference()) { if (!getLocalDatabaseReference()) {
return; return;
} }
// Remove it from the database // Remove it from the database
dbManager.deleteComputer(name); dbManager.deleteComputer(name);
synchronized (pollingTuples) { synchronized (pollingTuples) {
// Remove the computer from the computer list // Remove the computer from the computer list
@@ -344,31 +321,31 @@ public class ComputerManagerService extends Service {
} }
} }
} }
releaseLocalDatabaseReference();
}
private boolean getLocalDatabaseReference() {
if (dbRefCount.get() == 0) {
return false;
}
dbRefCount.incrementAndGet();
return true;
}
private void releaseLocalDatabaseReference() {
if (dbRefCount.decrementAndGet() == 0) {
dbManager.close();
}
}
private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) {
try {
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
ComputerDetails newDetails = http.getComputerDetails(); releaseLocalDatabaseReference();
}
private boolean getLocalDatabaseReference() {
if (dbRefCount.get() == 0) {
return false;
}
dbRefCount.incrementAndGet();
return true;
}
private void releaseLocalDatabaseReference() {
if (dbRefCount.decrementAndGet() == 0) {
dbManager.close();
}
}
private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) {
try {
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
ComputerDetails newDetails = http.getComputerDetails();
// Check if this is the PC we expected // Check if this is the PC we expected
if (details.uuid != null && newDetails.uuid != null && if (details.uuid != null && newDetails.uuid != null &&
@@ -379,58 +356,58 @@ public class ComputerManagerService extends Service {
} }
return newDetails; return newDetails;
} catch (Exception e) { } catch (Exception e) {
return null; return null;
} }
} }
private boolean pollComputer(ComputerDetails details, boolean localFirst) { private boolean pollComputer(ComputerDetails details, boolean localFirst) {
ComputerDetails polledDetails; ComputerDetails polledDetails;
// If the local address is routable across the Internet, // If the local address is routable across the Internet,
// always consider this PC remote to be conservative // always consider this PC remote to be conservative
if (details.localIp.equals(details.remoteIp)) { if (details.localIp.equals(details.remoteIp)) {
localFirst = false; localFirst = false;
} }
if (localFirst) { if (localFirst) {
polledDetails = tryPollIp(details, details.localIp); polledDetails = tryPollIp(details, details.localIp);
} }
else { else {
polledDetails = tryPollIp(details, details.remoteIp); polledDetails = tryPollIp(details, details.remoteIp);
} }
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) { if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
// Failed, so let's try the fallback // Failed, so let's try the fallback
if (!localFirst) { if (!localFirst) {
polledDetails = tryPollIp(details, details.localIp); polledDetails = tryPollIp(details, details.localIp);
} }
else { else {
polledDetails = tryPollIp(details, details.remoteIp); polledDetails = tryPollIp(details, details.remoteIp);
} }
// The fallback poll worked // The fallback poll worked
if (polledDetails != null) { if (polledDetails != null) {
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL : polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL :
ComputerDetails.Reachability.REMOTE; ComputerDetails.Reachability.REMOTE;
} }
} }
else if (polledDetails != null) { else if (polledDetails != null) {
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL : polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
ComputerDetails.Reachability.REMOTE; ComputerDetails.Reachability.REMOTE;
} }
// Machine was unreachable both tries // Machine was unreachable both tries
if (polledDetails == null) { if (polledDetails == null) {
return false; return false;
} }
// If we got here, it's reachable // If we got here, it's reachable
details.update(polledDetails); details.update(polledDetails);
return true; return true;
} }
private boolean doPollMachine(ComputerDetails details) { private boolean doPollMachine(ComputerDetails details) {
if (details.reachability == ComputerDetails.Reachability.UNKNOWN || if (details.reachability == ComputerDetails.Reachability.UNKNOWN ||
details.reachability == ComputerDetails.Reachability.OFFLINE) { details.reachability == ComputerDetails.Reachability.OFFLINE) {
// Always try local first to avoid potential UDP issues when // Always try local first to avoid potential UDP issues when
@@ -443,20 +420,20 @@ public class ComputerManagerService extends Service {
// always try that one first // always try that one first
return pollComputer(details, details.reachability == ComputerDetails.Reachability.LOCAL); return pollComputer(details, details.reachability == ComputerDetails.Reachability.LOCAL);
} }
} }
@Override @Override
public void onCreate() { public void onCreate() {
// Bind to the discovery service // Bind to the discovery service
bindService(new Intent(this, DiscoveryService.class), bindService(new Intent(this, DiscoveryService.class),
discoveryServiceConnection, Service.BIND_AUTO_CREATE); discoveryServiceConnection, Service.BIND_AUTO_CREATE);
// Lookup or generate this device's UID // Lookup or generate this device's UID
idManager = new IdentityManager(this); idManager = new IdentityManager(this);
// Initialize the DB // Initialize the DB
dbManager = new ComputerDatabaseManager(this); dbManager = new ComputerDatabaseManager(this);
dbRefCount.set(1); dbRefCount.set(1);
// Grab known machines into our computer list // Grab known machines into our computer list
if (!getLocalDatabaseReference()) { if (!getLocalDatabaseReference()) {
@@ -469,30 +446,30 @@ public class ComputerManagerService extends Service {
} }
releaseLocalDatabaseReference(); releaseLocalDatabaseReference();
} }
@Override @Override
public void onDestroy() { public void onDestroy() {
if (discoveryBinder != null) { if (discoveryBinder != null) {
// Unbind from the discovery service // Unbind from the discovery service
unbindService(discoveryServiceConnection); unbindService(discoveryServiceConnection);
} }
// FIXME: Should await termination here but we have timeout issues in HttpURLConnection // FIXME: Should await termination here but we have timeout issues in HttpURLConnection
// Remove the initial DB reference // Remove the initial DB reference
releaseLocalDatabaseReference(); releaseLocalDatabaseReference();
} }
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
return binder; return binder;
} }
public class ApplistPoller { public class ApplistPoller {
private Thread thread; private Thread thread;
private ComputerDetails computer; private final ComputerDetails computer;
private Object pollEvent = new Object(); private final Object pollEvent = new Object();
public ApplistPoller(ComputerDetails computer) { public ApplistPoller(ComputerDetails computer) {
this.computer = computer; this.computer = computer;
@@ -593,7 +570,7 @@ public class ComputerManagerService extends Service {
class PollingTuple { class PollingTuple {
public Thread thread; public Thread thread;
public ComputerDetails computer; public final ComputerDetails computer;
public PollingTuple(ComputerDetails computer, Thread thread) { public PollingTuple(ComputerDetails computer, Thread thread) {
this.computer = computer; this.computer = computer;
@@ -12,75 +12,75 @@ import com.limelight.LimeLog;
import android.content.Context; import android.content.Context;
public class IdentityManager { public class IdentityManager {
private static final String UNIQUE_ID_FILE_NAME = "uniqueid"; private static final String UNIQUE_ID_FILE_NAME = "uniqueid";
private static final int UID_SIZE_IN_BYTES = 8; private static final int UID_SIZE_IN_BYTES = 8;
private String uniqueId; private String uniqueId;
public IdentityManager(Context c) { public IdentityManager(Context c) {
uniqueId = loadUniqueId(c); uniqueId = loadUniqueId(c);
if (uniqueId == null) { if (uniqueId == null) {
uniqueId = generateNewUniqueId(c); uniqueId = generateNewUniqueId(c);
} }
LimeLog.info("UID is now: "+uniqueId); LimeLog.info("UID is now: "+uniqueId);
} }
public String getUniqueId() { public String getUniqueId() {
return uniqueId; return uniqueId;
} }
private static String loadUniqueId(Context c) { private static String loadUniqueId(Context c) {
// 2 Hex digits per byte // 2 Hex digits per byte
char[] uid = new char[UID_SIZE_IN_BYTES * 2]; char[] uid = new char[UID_SIZE_IN_BYTES * 2];
InputStreamReader reader = null; InputStreamReader reader = null;
LimeLog.info("Reading UID from disk"); LimeLog.info("Reading UID from disk");
try { try {
reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME)); reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME));
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2) if (reader.read(uid) != UID_SIZE_IN_BYTES * 2)
{ {
LimeLog.severe("UID file data is truncated"); LimeLog.severe("UID file data is truncated");
return null; return null;
} }
return new String(uid); return new String(uid);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
LimeLog.info("No UID file found"); LimeLog.info("No UID file found");
return null; return null;
} catch (IOException e) { } catch (IOException e) {
LimeLog.severe("Error while reading UID file"); LimeLog.severe("Error while reading UID file");
e.printStackTrace(); e.printStackTrace();
return null; return null;
} finally { } finally {
if (reader != null) { if (reader != null) {
try { try {
reader.close(); reader.close();
} catch (IOException ignored) {} } catch (IOException ignored) {}
} }
} }
} }
private static String generateNewUniqueId(Context c) { private static String generateNewUniqueId(Context c) {
// Generate a new UID hex string // Generate a new UID hex string
LimeLog.info("Generating new UID"); LimeLog.info("Generating new UID");
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong()); String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
OutputStreamWriter writer = null; OutputStreamWriter writer = null;
try { try {
writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0)); writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0));
writer.write(uidStr); writer.write(uidStr);
LimeLog.info("UID written to disk"); LimeLog.info("UID written to disk");
} catch (IOException e) { } catch (IOException e) {
LimeLog.severe("Error while writing UID file"); LimeLog.severe("Error while writing UID file");
e.printStackTrace(); e.printStackTrace();
} finally { } finally {
if (writer != null) { if (writer != null) {
try { try {
writer.close(); writer.close();
} catch (IOException ignored) {} } catch (IOException ignored) {}
} }
} }
// We can return a UID even if I/O fails // We can return a UID even if I/O fails
return uidStr; return uidStr;
} }
} }
@@ -15,76 +15,76 @@ import android.os.Binder;
import android.os.IBinder; import android.os.IBinder;
public class DiscoveryService extends Service { public class DiscoveryService extends Service {
private MdnsDiscoveryAgent discoveryAgent;
private MdnsDiscoveryListener boundListener;
private MulticastLock multicastLock;
public class DiscoveryBinder extends Binder {
public void setListener(MdnsDiscoveryListener listener) {
boundListener = listener;
}
public void startDiscovery(int queryIntervalMs) {
multicastLock.acquire();
discoveryAgent.startDiscovery(queryIntervalMs);
}
public void stopDiscovery() {
discoveryAgent.stopDiscovery();
multicastLock.release();
}
public List<MdnsComputer> getComputerSet() {
return discoveryAgent.getComputerSet();
}
}
@Override
public void onCreate() {
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
multicastLock.setReferenceCounted(false);
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
@Override
public void notifyComputerAdded(MdnsComputer computer) {
if (boundListener != null) {
boundListener.notifyComputerAdded(computer);
}
}
@Override private MdnsDiscoveryAgent discoveryAgent;
public void notifyComputerRemoved(MdnsComputer computer) { private MdnsDiscoveryListener boundListener;
if (boundListener != null) { private MulticastLock multicastLock;
boundListener.notifyComputerRemoved(computer);
}
}
@Override public class DiscoveryBinder extends Binder {
public void notifyDiscoveryFailure(Exception e) { public void setListener(MdnsDiscoveryListener listener) {
if (boundListener != null) { boundListener = listener;
boundListener.notifyDiscoveryFailure(e); }
}
} public void startDiscovery(int queryIntervalMs) {
}); multicastLock.acquire();
} discoveryAgent.startDiscovery(queryIntervalMs);
}
private DiscoveryBinder binder = new DiscoveryBinder();
public void stopDiscovery() {
@Override discoveryAgent.stopDiscovery();
public IBinder onBind(Intent intent) { multicastLock.release();
return binder; }
}
public List<MdnsComputer> getComputerSet() {
@Override return discoveryAgent.getComputerSet();
public boolean onUnbind(Intent intent) { }
// Stop any discovery session }
discoveryAgent.stopDiscovery();
multicastLock.release(); @Override
public void onCreate() {
// Unbind the listener WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
boundListener = null; multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
return false; multicastLock.setReferenceCounted(false);
}
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
@Override
public void notifyComputerAdded(MdnsComputer computer) {
if (boundListener != null) {
boundListener.notifyComputerAdded(computer);
}
}
@Override
public void notifyComputerRemoved(MdnsComputer computer) {
if (boundListener != null) {
boundListener.notifyComputerRemoved(computer);
}
}
@Override
public void notifyDiscoveryFailure(Exception e) {
if (boundListener != null) {
boundListener.notifyDiscoveryFailure(e);
}
}
});
}
private final DiscoveryBinder binder = new DiscoveryBinder();
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public boolean onUnbind(Intent intent) {
// Stop any discovery session
discoveryAgent.stopDiscovery();
multicastLock.release();
// Unbind the listener
boundListener = null;
return false;
}
} }
@@ -4,12 +4,10 @@ import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import com.koushikdutta.async.future.FutureCallback; import com.koushikdutta.async.future.FutureCallback;
import com.koushikdutta.ion.ImageViewBitmapInfo;
import com.koushikdutta.ion.Ion; import com.koushikdutta.ion.Ion;
import com.limelight.AppView; import com.limelight.AppView;
import com.limelight.LimeLog; import com.limelight.LimeLog;
@@ -19,10 +17,6 @@ import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.LimelightCryptoProvider; import com.limelight.nvstream.http.LimelightCryptoProvider;
import com.limelight.utils.CacheHelper; import com.limelight.utils.CacheHelper;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@@ -36,7 +30,6 @@ import java.security.SecureRandom;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.Future; import java.util.concurrent.Future;
@@ -51,10 +44,10 @@ import java.security.cert.X509Certificate;
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> { public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
private ComputerDetails computer; private final ComputerDetails computer;
private String uniqueId; private final String uniqueId;
private LimelightCryptoProvider cryptoProvider; private final LimelightCryptoProvider cryptoProvider;
private SSLContext sslContext; private final SSLContext sslContext;
private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>(); private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>();
public AppGridAdapter(Context context, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException { public AppGridAdapter(Context context, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException {
@@ -69,7 +62,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom()); sslContext.init(ourKeyman, trustAllCerts, new SecureRandom());
} }
TrustManager[] trustAllCerts = new TrustManager[] { private final TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() { new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() { public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0]; return new X509Certificate[0];
@@ -78,7 +71,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
public void checkServerTrusted(X509Certificate[] certs, String authType) {} public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}}; }};
KeyManager[] ourKeyman = new KeyManager[] { private final KeyManager[] ourKeyman = new KeyManager[] {
new X509KeyManager() { new X509KeyManager() {
public String chooseClientAlias(String[] keyTypes, public String chooseClientAlias(String[] keyTypes,
Principal[] issuers, Socket socket) { Principal[] issuers, Socket socket) {
@@ -172,8 +165,8 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
@Override @Override
public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) { public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) {
// Hide the image view while we're loading the image from disk cache // Clear existing contents of the image view
imgView.setVisibility(View.INVISIBLE); imgView.setAlpha(0.0f);
// Check the on-disk cache // Check the on-disk cache
new ImageCacheRequest(imgView, obj.app.getAppId()).execute(); new ImageCacheRequest(imgView, obj.app.getAppId()).execute();
@@ -203,8 +196,8 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
} }
private class ImageCacheRequest extends AsyncTask<Void, Void, Bitmap> { private class ImageCacheRequest extends AsyncTask<Void, Void, Bitmap> {
private ImageView view; private final ImageView view;
private int appId; private final int appId;
public ImageCacheRequest(ImageView view, int appId) { public ImageCacheRequest(ImageView view, int appId) {
this.view = view; this.view = view;
@@ -223,19 +216,23 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
if (in != null) { if (in != null) {
try { try {
in.close(); in.close();
} catch (IOException e) {} } catch (IOException ignored) {}
} }
} }
return null; return null;
} }
private void fadeInImage(ImageView view) {
view.animate().alpha(1.0f).setDuration(250).start();
}
@Override @Override
protected void onPostExecute(Bitmap result) { protected void onPostExecute(Bitmap result) {
if (result != null) { if (result != null) {
// Disk cache was read successfully // Disk cache was read successfully
LimeLog.info("Image disk cache hit for ("+computer.uuid+", "+appId+")"); LimeLog.info("Image disk cache hit for (" + computer.uuid + ", " + appId + ")");
view.setImageBitmap(result); view.setImageBitmap(result);
view.setVisibility(View.VISIBLE); fadeInImage(view);
} }
else { else {
LimeLog.info("Image disk cache miss for ("+computer.uuid+", "+appId+")"); LimeLog.info("Image disk cache miss for ("+computer.uuid+", "+appId+")");
@@ -244,11 +241,12 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
// Load the placeholder image // Load the placeholder image
view.setImageResource(defaultImageRes); view.setImageResource(defaultImageRes);
view.setVisibility(View.VISIBLE); fadeInImage(view);
// Set SSL contexts correctly to allow us to authenticate // Set SSL contexts correctly to allow us to authenticate
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts); Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts);
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext); Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext);
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setHostnameVerifier(hv);
// Kick off the deferred image load // Kick off the deferred image load
synchronized (pendingRequests) { synchronized (pendingRequests) {
@@ -258,7 +256,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
.asBitmap() .asBitmap()
.setCallback(new FutureCallback<Bitmap>() { .setCallback(new FutureCallback<Bitmap>() {
@Override @Override
public void onCompleted(Exception e, Bitmap result) { public void onCompleted(Exception e, final Bitmap result) {
synchronized (pendingRequests) { synchronized (pendingRequests) {
pendingRequests.remove(view); pendingRequests.remove(view);
} }
@@ -266,10 +264,17 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
if (result != null) { if (result != null) {
// Make the view visible now // Make the view visible now
view.setImageBitmap(result); view.setImageBitmap(result);
view.setVisibility(View.VISIBLE); fadeInImage(view);
// Populate the disk cache if we got an image back // Populate the disk cache if we got an image back.
populateBitmapCache(computer.uuid, appId, result); // We do it in a new thread because it can be very expensive, especially
// when we do the initial load where lots of disk I/O is happening at once.
new Thread() {
@Override
public void run() {
populateBitmapCache(computer.uuid, appId, result);
}
}.start();
} }
else { else {
// Leave the loading icon as is (probably should change this eventually...) // Leave the loading icon as is (probably should change this eventually...)
@@ -280,5 +285,5 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
} }
} }
} }
}; }
} }
@@ -13,11 +13,11 @@ import com.limelight.R;
import java.util.ArrayList; import java.util.ArrayList;
public abstract class GenericGridAdapter<T> extends BaseAdapter { public abstract class GenericGridAdapter<T> extends BaseAdapter {
protected Context context; protected final Context context;
protected int defaultImageRes; protected final int defaultImageRes;
protected int layoutId; protected final int layoutId;
protected ArrayList<T> itemList = new ArrayList<T>(); protected final ArrayList<T> itemList = new ArrayList<T>();
protected LayoutInflater inflater; protected final LayoutInflater inflater;
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) { public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
this.context = context; this.context = context;
@@ -1,44 +1,44 @@
package com.limelight.nvstream.av.video.cpu; package com.limelight.nvstream.av.video.cpu;
public class AvcDecoder { public class AvcDecoder {
static { static {
// FFMPEG dependencies // FFMPEG dependencies
System.loadLibrary("avutil-52"); System.loadLibrary("avutil-52");
System.loadLibrary("swresample-0"); System.loadLibrary("swresample-0");
System.loadLibrary("swscale-2"); System.loadLibrary("swscale-2");
System.loadLibrary("avcodec-55"); System.loadLibrary("avcodec-55");
System.loadLibrary("avformat-55"); System.loadLibrary("avformat-55");
System.loadLibrary("nv_avc_dec");
}
/** Disables the deblocking filter at the cost of image quality */
public static final int DISABLE_LOOP_FILTER = 0x1;
/** Uses the low latency decode flag (disables multithreading) */
public static final int LOW_LATENCY_DECODE = 0x2;
/** Threads process each slice, rather than each frame */
public static final int SLICE_THREADING = 0x4;
/** Uses nonstandard speedup tricks */
public static final int FAST_DECODE = 0x8;
/** Uses bilinear filtering instead of bicubic */
public static final int BILINEAR_FILTERING = 0x10;
/** Uses a faster bilinear filtering with lower image quality */
public static final int FAST_BILINEAR_FILTERING = 0x20;
/** Disables color conversion (output is NV21) */
public static final int NO_COLOR_CONVERSION = 0x40;
public static native int init(int width, int height, int perflvl, int threadcount);
public static native void destroy();
// Rendering API when NO_COLOR_CONVERSION == 0
public static native boolean setRenderTarget(Object androidSurface);
public static native boolean getRgbFrameInt(int[] rgbFrame, int bufferSize);
public static native boolean getRgbFrame(byte[] rgbFrame, int bufferSize);
public static native boolean redraw();
// Rendering API when NO_COLOR_CONVERSION == 1 System.loadLibrary("nv_avc_dec");
public static native boolean getRawFrame(byte[] yuvFrame, int bufferSize); }
public static native int getInputPaddingSize(); /** Disables the deblocking filter at the cost of image quality */
public static native int decode(byte[] indata, int inoff, int inlen); public static final int DISABLE_LOOP_FILTER = 0x1;
/** Uses the low latency decode flag (disables multithreading) */
public static final int LOW_LATENCY_DECODE = 0x2;
/** Threads process each slice, rather than each frame */
public static final int SLICE_THREADING = 0x4;
/** Uses nonstandard speedup tricks */
public static final int FAST_DECODE = 0x8;
/** Uses bilinear filtering instead of bicubic */
public static final int BILINEAR_FILTERING = 0x10;
/** Uses a faster bilinear filtering with lower image quality */
public static final int FAST_BILINEAR_FILTERING = 0x20;
/** Disables color conversion (output is NV21) */
public static final int NO_COLOR_CONVERSION = 0x40;
public static native int init(int width, int height, int perflvl, int threadcount);
public static native void destroy();
// Rendering API when NO_COLOR_CONVERSION == 0
public static native boolean setRenderTarget(Object androidSurface);
public static native boolean getRgbFrameInt(int[] rgbFrame, int bufferSize);
public static native boolean getRgbFrame(byte[] rgbFrame, int bufferSize);
public static native boolean redraw();
// Rendering API when NO_COLOR_CONVERSION == 1
public static native boolean getRawFrame(byte[] yuvFrame, int bufferSize);
public static native int getInputPaddingSize();
public static native int decode(byte[] indata, int inoff, int inlen);
} }
@@ -14,6 +14,7 @@ import com.limelight.utils.UiHelper;
import android.app.Activity; import android.app.Activity;
import android.app.Service; import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.content.res.Configuration; import android.content.res.Configuration;
@@ -21,131 +22,132 @@ import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
public class AddComputerManually extends Activity { public class AddComputerManually extends Activity {
private TextView hostText; private TextView hostText;
private ComputerManagerService.ComputerManagerBinder managerBinder; private ComputerManagerService.ComputerManagerBinder managerBinder;
private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>(); private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
private Thread addThread; private Thread addThread;
private ServiceConnection serviceConnection = new ServiceConnection() { private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, final IBinder binder) { public void onServiceConnected(ComponentName className, final IBinder binder) {
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder); managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
startAddThread(); startAddThread();
} }
public void onServiceDisconnected(ComponentName className) { public void onServiceDisconnected(ComponentName className) {
joinAddThread(); joinAddThread();
managerBinder = null; managerBinder = null;
} }
}; };
private void doAddPc(String host) { private void doAddPc(String host) {
String msg; String msg;
boolean finish = false; boolean finish = false;
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc), SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
getResources().getString(R.string.msg_add_pc), false); getResources().getString(R.string.msg_add_pc), false);
try { try {
InetAddress addr = InetAddress.getByName(host); InetAddress addr = InetAddress.getByName(host);
if (!managerBinder.addComputerBlocking(addr)){ if (!managerBinder.addComputerBlocking(addr)){
msg = getResources().getString(R.string.addpc_fail); msg = getResources().getString(R.string.addpc_fail);
} }
else { else {
msg = getResources().getString(R.string.addpc_success); msg = getResources().getString(R.string.addpc_success);
finish = true; finish = true;
} }
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
msg = getResources().getString(R.string.addpc_unknown_host); msg = getResources().getString(R.string.addpc_unknown_host);
} }
dialog.dismiss(); dialog.dismiss();
final boolean toastFinish = finish; final boolean toastFinish = finish;
final String toastMsg = msg; final String toastMsg = msg;
AddComputerManually.this.runOnUiThread(new Runnable() { AddComputerManually.this.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show(); Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
if (toastFinish && !isFinishing()) {
// Close the activity
AddComputerManually.this.finish();
}
}
});
}
private void startAddThread() {
addThread = new Thread() {
@Override
public void run() {
while (!isInterrupted()) {
String computer;
try {
computer = computersToAdd.take();
} catch (InterruptedException e) {
return;
}
doAddPc(computer);
}
}
};
addThread.setName("UI - AddComputerManually");
addThread.start();
}
private void joinAddThread() {
if (addThread != null) {
addThread.interrupt();
try {
addThread.join();
} catch (InterruptedException ignored) {}
addThread = null;
}
}
@Override if (toastFinish && !isFinishing()) {
protected void onStop() { // Close the activity
super.onStop(); AddComputerManually.this.finish();
}
Dialog.closeDialogs(); }
});
}
private void startAddThread() {
addThread = new Thread() {
@Override
public void run() {
while (!isInterrupted()) {
String computer;
try {
computer = computersToAdd.take();
} catch (InterruptedException e) {
return;
}
doAddPc(computer);
}
}
};
addThread.setName("UI - AddComputerManually");
addThread.start();
}
private void joinAddThread() {
if (addThread != null) {
addThread.interrupt();
try {
addThread.join();
} catch (InterruptedException ignored) {}
addThread = null;
}
}
@Override
protected void onStop() {
super.onStop();
Dialog.closeDialogs();
SpinnerDialog.closeDialogs(this); SpinnerDialog.closeDialogs(this);
} }
@Override @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
if (managerBinder != null) { if (managerBinder != null) {
joinAddThread(); joinAddThread();
unbindService(serviceConnection); unbindService(serviceConnection);
} }
} }
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language; String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) { if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration()); Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale); config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics()); getResources().updateConfiguration(config, getResources().getDisplayMetrics());
} }
setContentView(R.layout.activity_add_computer_manually); setContentView(R.layout.activity_add_computer_manually);
UiHelper.notifyNewRootView(this); UiHelper.notifyNewRootView(this);
this.hostText = (TextView) findViewById(R.id.hostTextView); this.hostText = (TextView) findViewById(R.id.hostTextView);
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE); hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() { hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override @Override
@@ -161,13 +163,19 @@ public class AddComputerManually extends Activity {
computersToAdd.add(hostText.getText().toString().trim()); computersToAdd.add(hostText.getText().toString().trim());
} }
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
// This is how the Fire TV dismisses the keyboard
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(hostText.getWindowToken(), 0);
return false;
}
return false; return false;
} }
}); });
// Bind to the ComputerManager service // Bind to the ComputerManager service
bindService(new Intent(AddComputerManually.this, bindService(new Intent(AddComputerManually.this,
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE); ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
} }
} }
@@ -2,6 +2,7 @@ package com.limelight.preferences;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
public class PreferenceConfiguration { public class PreferenceConfiguration {
@@ -16,6 +17,7 @@ public class PreferenceConfiguration {
private static final String LANGUAGE_PREF_STRING = "list_languages"; private static final String LANGUAGE_PREF_STRING = "list_languages";
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode"; private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode"; private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
private static final int BITRATE_DEFAULT_720_30 = 5; private static final int BITRATE_DEFAULT_720_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10; private static final int BITRATE_DEFAULT_720_60 = 10;
@@ -32,6 +34,7 @@ public class PreferenceConfiguration {
private static final int DEFAULT_DEADZONE = 15; private static final int DEFAULT_DEADZONE = 15;
public static final String DEFAULT_LANGUAGE = "default"; public static final String DEFAULT_LANGUAGE = "default";
private static final boolean DEFAULT_LIST_MODE = false; private static final boolean DEFAULT_LIST_MODE = false;
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
public static final int FORCE_HARDWARE_DECODER = -1; public static final int FORCE_HARDWARE_DECODER = -1;
public static final int AUTOSELECT_DECODER = 0; public static final int AUTOSELECT_DECODER = 0;
@@ -43,7 +46,7 @@ public class PreferenceConfiguration {
public int deadzonePercentage; public int deadzonePercentage;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings; public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language; public String language;
public boolean listMode, smallIconMode; public boolean listMode, smallIconMode, multiController;
public static int getDefaultBitrate(String resFpsString) { public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("720p30")) { if (resFpsString.equals("720p30")) {
@@ -65,6 +68,12 @@ public class PreferenceConfiguration {
} }
public static boolean getDefaultSmallMode(Context context) { public static boolean getDefaultSmallMode(Context context) {
PackageManager manager = context.getPackageManager();
if (manager != null && manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
// TVs shouldn't use small mode by default
return false;
}
// Use small mode on anything smaller than a 7" tablet // Use small mode on anything smaller than a 7" tablet
return context.getResources().getConfiguration().smallestScreenWidthDp < 600; return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
} }
@@ -156,6 +165,7 @@ public class PreferenceConfiguration {
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO); config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE); config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context)); config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
return config; return config;
} }
@@ -20,10 +20,14 @@ public class SeekBarPreference extends DialogPreference
private SeekBar seekBar; private SeekBar seekBar;
private TextView valueText; private TextView valueText;
private Context context; private final Context context;
private String dialogMessage, suffix; private final String dialogMessage;
private int defaultValue, maxValue, minValue, currentValue; private final String suffix;
private final int defaultValue;
private final int maxValue;
private final int minValue;
private int currentValue;
public SeekBarPreference(Context context, AttributeSet attrs) { public SeekBarPreference(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
@@ -127,13 +131,6 @@ public class SeekBarPreference extends DialogPreference
} }
} }
public void setMax(int max) {
this.maxValue = max;
}
public int getMax() {
return this.maxValue;
}
public void setProgress(int progress) { public void setProgress(int progress) {
this.currentValue = progress; this.currentValue = progress;
if (seekBar != null) { if (seekBar != null) {
@@ -9,8 +9,6 @@ import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.Reader; import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.Scanner;
public class CacheHelper { public class CacheHelper {
private static File openPath(boolean createPath, File root, String... path) { private static File openPath(boolean createPath, File root, String... path) {
@@ -7,70 +7,71 @@ import android.app.AlertDialog;
import android.content.DialogInterface; import android.content.DialogInterface;
public class Dialog implements Runnable { public class Dialog implements Runnable {
private String title, message; private final String title;
private Activity activity; private final String message;
private boolean endAfterDismiss; private final Activity activity;
private final boolean endAfterDismiss;
private AlertDialog alert;
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
public Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
{
this.activity = activity;
this.title = title;
this.message = message;
this.endAfterDismiss = endAfterDismiss;
}
public static void closeDialogs()
{
synchronized (rundownDialogs) {
for (Dialog d : rundownDialogs) {
if (d.alert.isShowing()) {
d.alert.dismiss();
}
}
rundownDialogs.clear();
}
}
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
{
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
}
@Override
public void run() {
// If we're dying, don't bother creating a dialog
if (activity.isFinishing())
return;
alert = new AlertDialog.Builder(activity).create();
alert.setTitle(title); private AlertDialog alert;
alert.setMessage(message);
alert.setCancelable(false); private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
alert.setCanceledOnTouchOutside(false);
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
{
this.activity = activity;
this.title = title;
this.message = message;
this.endAfterDismiss = endAfterDismiss;
}
public static void closeDialogs()
{
synchronized (rundownDialogs) {
for (Dialog d : rundownDialogs) {
if (d.alert.isShowing()) {
d.alert.dismiss();
}
}
rundownDialogs.clear();
}
}
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
{
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
}
@Override
public void run() {
// If we're dying, don't bother creating a dialog
if (activity.isFinishing())
return;
alert = new AlertDialog.Builder(activity).create();
alert.setTitle(title);
alert.setMessage(message);
alert.setCancelable(false);
alert.setCanceledOnTouchOutside(false);
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() { alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
rundownDialogs.remove(Dialog.this); rundownDialogs.remove(Dialog.this);
alert.dismiss(); alert.dismiss();
} }
if (endAfterDismiss) { if (endAfterDismiss) {
activity.finish(); activity.finish();
} }
} }
}); });
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
rundownDialogs.add(this); rundownDialogs.add(this);
alert.show(); alert.show();
} }
} }
} }
@@ -9,111 +9,112 @@ import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener; import android.content.DialogInterface.OnCancelListener;
public class SpinnerDialog implements Runnable,OnCancelListener { public class SpinnerDialog implements Runnable,OnCancelListener {
private String title, message; private final String title;
private Activity activity; private final String message;
private ProgressDialog progress; private final Activity activity;
private boolean finish; private ProgressDialog progress;
private final boolean finish;
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
public SpinnerDialog(Activity activity, String title, String message, boolean finish)
{
this.activity = activity;
this.title = title;
this.message = message;
this.progress = null;
this.finish = finish;
}
public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish)
{
SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish);
activity.runOnUiThread(spinner);
return spinner;
}
public static void closeDialogs(Activity activity)
{
synchronized (rundownDialogs) {
Iterator<SpinnerDialog> i = rundownDialogs.iterator();
while (i.hasNext()) {
SpinnerDialog dialog = i.next();
if (dialog.activity == activity) {
i.remove();
if (dialog.progress.isShowing()) {
dialog.progress.dismiss();
}
}
}
}
}
public void dismiss()
{
// Running again with progress != null will destroy it
activity.runOnUiThread(this);
}
public void setMessage(final String message)
{
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
progress.setMessage(message);
}
});
}
@Override
public void run() {
// If we're dying, don't bother doing anything private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
if (activity.isFinishing()) {
return;
}
if (progress == null)
{
progress = new ProgressDialog(activity);
progress.setTitle(title);
progress.setMessage(message);
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progress.setOnCancelListener(this);
// If we want to finish the activity when this is killed, make it cancellable
if (finish)
{
progress.setCancelable(true);
progress.setCanceledOnTouchOutside(false);
}
else
{
progress.setCancelable(false);
}
synchronized (rundownDialogs) {
rundownDialogs.add(this);
progress.show();
}
}
else
{
synchronized (rundownDialogs) {
if (rundownDialogs.remove(this) && progress.isShowing()) {
progress.dismiss();
}
}
}
}
@Override private SpinnerDialog(Activity activity, String title, String message, boolean finish)
public void onCancel(DialogInterface dialog) { {
synchronized (rundownDialogs) { this.activity = activity;
rundownDialogs.remove(this); this.title = title;
} this.message = message;
this.progress = null;
// This will only be called if finish was true, so we don't need to check again this.finish = finish;
activity.finish(); }
}
public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish)
{
SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish);
activity.runOnUiThread(spinner);
return spinner;
}
public static void closeDialogs(Activity activity)
{
synchronized (rundownDialogs) {
Iterator<SpinnerDialog> i = rundownDialogs.iterator();
while (i.hasNext()) {
SpinnerDialog dialog = i.next();
if (dialog.activity == activity) {
i.remove();
if (dialog.progress.isShowing()) {
dialog.progress.dismiss();
}
}
}
}
}
public void dismiss()
{
// Running again with progress != null will destroy it
activity.runOnUiThread(this);
}
public void setMessage(final String message)
{
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
progress.setMessage(message);
}
});
}
@Override
public void run() {
// If we're dying, don't bother doing anything
if (activity.isFinishing()) {
return;
}
if (progress == null)
{
progress = new ProgressDialog(activity);
progress.setTitle(title);
progress.setMessage(message);
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progress.setOnCancelListener(this);
// If we want to finish the activity when this is killed, make it cancellable
if (finish)
{
progress.setCancelable(true);
progress.setCanceledOnTouchOutside(false);
}
else
{
progress.setCancelable(false);
}
synchronized (rundownDialogs) {
rundownDialogs.add(this);
progress.show();
}
}
else
{
synchronized (rundownDialogs) {
if (rundownDialogs.remove(this) && progress.isShowing()) {
progress.dismiss();
}
}
}
}
@Override
public void onCancel(DialogInterface dialog) {
synchronized (rundownDialogs) {
rundownDialogs.remove(this);
}
// This will only be called if finish was true, so we don't need to check again
activity.finish();
}
} }
@@ -9,5 +9,7 @@
android:numColumns="auto_fit" android:numColumns="auto_fit"
android:columnWidth="160dp" android:columnWidth="160dp"
android:stretchMode="spacingWidth" android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/> android:gravity="center"/>
</LinearLayout> </LinearLayout>
@@ -9,5 +9,7 @@
android:numColumns="auto_fit" android:numColumns="auto_fit"
android:columnWidth="105dp" android:columnWidth="105dp"
android:stretchMode="spacingWidth" android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/> android:gravity="center"/>
</LinearLayout> </LinearLayout>
+3 -1
View File
@@ -6,10 +6,12 @@
<ListView <ListView
android:id="@+id/fragmentView" android:id="@+id/fragmentView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:background="@drawable/list_view_unselected" android:background="@drawable/list_view_unselected"
android:fastScrollEnabled="true" android:fastScrollEnabled="true"
android:longClickable="false" android:longClickable="false"
android:focusable="true"
android:focusableInTouchMode="true"
android:stackFromBottom="false" > android:stackFromBottom="false" >
</ListView> </ListView>
+2
View File
@@ -8,5 +8,7 @@
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:numColumns="auto_fit" android:numColumns="auto_fit"
android:columnWidth="160dp" android:columnWidth="160dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/> android:gravity="center"/>
</LinearLayout> </LinearLayout>
@@ -8,5 +8,7 @@
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:numColumns="auto_fit" android:numColumns="auto_fit"
android:columnWidth="105dp" android:columnWidth="105dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/> android:gravity="center"/>
</LinearLayout> </LinearLayout>
+2
View File
@@ -95,6 +95,8 @@
<string name="summary_checkbox_disable_warnings">Disabilita i messaggi di warning sullo schermo durante lo streaming</string> <string name="summary_checkbox_disable_warnings">Disabilita i messaggi di warning sullo schermo durante lo streaming</string>
<string name="category_gamepad_settings">Impostazioni Gamepad</string> <string name="category_gamepad_settings">Impostazioni Gamepad</string>
<string name="title_checkbox_multi_controller">Supporto controller multipli</string>
<string name="summary_checkbox_multi_controller">Quando disabilitato, tutti i controllers appaiono come uno solo</string>
<string name="title_seekbar_deadzone">Aggiusta deadzone degli stick analogici</string> <string name="title_seekbar_deadzone">Aggiusta deadzone degli stick analogici</string>
<string name="suffix_seekbar_deadzone">%</string> <string name="suffix_seekbar_deadzone">%</string>
+2
View File
@@ -95,6 +95,8 @@
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string> <string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
<string name="category_gamepad_settings">Gamepad Settings</string> <string name="category_gamepad_settings">Gamepad Settings</string>
<string name="title_checkbox_multi_controller">Multiple controller support</string>
<string name="summary_checkbox_multi_controller">When unchecked, all controllers appear as one</string>
<string name="title_seekbar_deadzone">Adjust analog stick deadzone</string> <string name="title_seekbar_deadzone">Adjust analog stick deadzone</string>
<string name="suffix_seekbar_deadzone">%</string> <string name="suffix_seekbar_deadzone">%</string>
+9 -4
View File
@@ -26,14 +26,19 @@
android:summary="@string/summary_checkbox_disable_warnings" android:summary="@string/summary_checkbox_disable_warnings"
android:defaultValue="false" /> android:defaultValue="false" />
</PreferenceCategory> </PreferenceCategory>
<!--PreferenceCategory android:title="@string/category_gamepad_settings"> <PreferenceCategory android:title="@string/category_gamepad_settings">
<com.limelight.preferences.SeekBarPreference <!--com.limelight.preferences.SeekBarPreference
android:key="seekbar_deadzone" android:key="seekbar_deadzone"
android:defaultValue="15" android:defaultValue="15"
android:max="50" android:max="50"
android:text="@string/suffix_seekbar_deadzone" android:text="@string/suffix_seekbar_deadzone"
android:title="@string/title_seekbar_deadzone"/> android:title="@string/title_seekbar_deadzone"/-->
</PreferenceCategory--> <CheckBoxPreference
android:key="checkbox_multi_controller"
android:title="@string/title_checkbox_multi_controller"
android:summary="@string/summary_checkbox_multi_controller"
android:defaultValue="true" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_host_settings"> <PreferenceCategory android:title="@string/category_host_settings">
<CheckBoxPreference <CheckBoxPreference
android:key="checkbox_enable_sops" android:key="checkbox_enable_sops"
+1 -1
View File
@@ -4,7 +4,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:1.0.0' classpath 'com.android.tools.build:gradle:1.0.1'
} }
} }
+1 -3
View File
@@ -7,9 +7,7 @@
</configuration> </configuration>
</facet> </facet>
</component> </component>
<component name="NewModuleRootManager" inherit-compiler-output="false"> <component name="NewModuleRootManager" inherit-compiler-output="true">
<output url="file://$MODULE_DIR$/build/classes/main" />
<output-test url="file://$MODULE_DIR$/build/classes/test" />
<exclude-output /> <exclude-output />
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" /> <excludeFolder url="file://$MODULE_DIR$/.gradle" />