Compare commits

..
2 Commits
Author SHA1 Message Date
Cameron Gutman 2ba3c0e477 Update common again 2014-11-11 01:09:45 -08:00
Cameron Gutman fe27957000 Update 2.9 series to support GFE 2.1.4 2014-11-10 22:09:56 -08:00
92 changed files with 4582 additions and 6767 deletions

No files matched your search

-2
View File
@@ -8,8 +8,6 @@ whether in your own home or over the internet.
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development. [Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development.
Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for more detailed information or a troubleshooting guide.
##Features ##Features
* Streams any of your games from your PC to your Android device * Streams any of your games from your PC to your Android device
+3 -11
View File
@@ -9,7 +9,6 @@
<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" />
@@ -25,7 +24,6 @@
</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" />
@@ -33,7 +31,6 @@
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/nonRoot/debug" isTestSource="false" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/nonRoot/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/nonRoot/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" />
@@ -46,7 +43,6 @@
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/nonRoot/debug" isTestSource="true" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/nonRoot/debug" type="java-test-resource" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/test/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
@@ -105,18 +101,14 @@
</content> </content>
<orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" /> <orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="okhttp-2.1.0-RC1" level="project" />
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" /> <orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="androidasync-2.0.5" level="project" />
<orderEntry type="library" exported="" name="gson-2.3.1" level="project" />
<orderEntry type="library" exported="" name="support-v4-r7" level="project" />
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" /> <orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
<orderEntry type="library" exported="" name="jcodec-0.1.6-3" level="project" />
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" /> <orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="tinyrtsp" level="project" /> <orderEntry type="library" exported="" name="tinyrtsp" level="project" />
<orderEntry type="library" exported="" name="limelight-common" level="project" /> <orderEntry type="library" exported="" name="limelight-common" level="project" />
<orderEntry type="library" exported="" name="ion-2.0.5" level="project" /> <orderEntry type="library" exported="" name="okio-1.0.1" level="project" />
<orderEntry type="library" exported="" name="okhttp-2.2.0" level="project" />
<orderEntry type="library" exported="" name="jcodec-0.1.9" level="project" />
<orderEntry type="library" exported="" name="okio-1.2.0" level="project" />
</component> </component>
</module> </module>
+7 -14
View File
@@ -5,14 +5,14 @@ apply plugin: 'com.android.application'
android { android {
compileSdkVersion 21 compileSdkVersion 21
buildToolsVersion "21.1.2" buildToolsVersion "21.0.2"
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 21 targetSdkVersion 21
versionName "3.1" versionName "2.9.0.1"
versionCode = 53 versionCode = 40
} }
productFlavors { productFlavors {
@@ -27,7 +27,7 @@ android {
buildTypes { buildTypes {
release { release {
minifyEnabled false runProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
} }
} }
@@ -62,18 +62,11 @@ android {
} }
dependencies { dependencies {
compile group: 'org.jcodec', name: 'jcodec', version: '0.1.9' compile group: 'org.jcodec', name: 'jcodec', version: '0.1.6-3'
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51' compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51' compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.1.0-RC1'
compile group: 'com.google.android', name: 'support-v4', version:'r7' compile group: 'com.squareup.okio', name:'okio', version:'1.0.1'
compile group: 'com.koushikdutta.ion', name: 'ion', version:'2.0.5'
compile group: 'com.google.code.gson', name: 'gson', version:'2.3.1'
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.2.0'
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')
compile files('libs/tinyrtsp.jar') compile files('libs/tinyrtsp.jar')
Binary file not shown.
+1 -2
View File
@@ -64,11 +64,10 @@
<activity <activity
android:name=".Game" android:name=".Game"
android:screenOrientation="sensorLandscape" android:screenOrientation="sensorLandscape"
android:theme="@style/StreamTheme"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" > android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.AppView" /> android:value="com.limelight.Connection" />
</activity> </activity>
<service <service
android:name=".discovery.DiscoveryService" android:name=".discovery.DiscoveryService"
+216 -433
View File
@@ -1,512 +1,295 @@
package com.limelight; package com.limelight;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.StringReader; import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.UUID; import org.xmlpull.v1.XmlPullParserException;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.computers.ComputerManagerListener; import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.AppGridAdapter;
import com.limelight.nvstream.http.ComputerDetails;
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.R;
import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.CacheHelper;
import com.limelight.utils.Dialog; import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog; import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog;
import android.app.Service;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
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.ArrayAdapter;
import android.widget.ListView;
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;
public class AppView extends Activity implements AdapterFragmentCallbacks { public class AppView extends Activity {
private AppGridAdapter appGridAdapter; private ListView appList;
private String uuidString; private ArrayAdapter<AppObject> appListAdapter;
private InetAddress ipAddress;
private String uniqueId;
private boolean remote;
private ComputerDetails computer; private final static int RESUME_ID = 1;
private ComputerManagerService.ApplistPoller poller; private final static int QUIT_ID = 2;
private SpinnerDialog blockingLoadSpinner; private final static int CANCEL_ID = 3;
private String lastRawApplist;
private int consecutiveAppListFailures = 0; public final static String ADDRESS_EXTRA = "Address";
private final static int CONSECUTIVE_FAILURE_LIMIT = 3; public final static String UNIQUEID_EXTRA = "UniqueId";
public final static String NAME_EXTRA = "Name";
public final static String REMOTE_EXTRA = "Remote";
private final static int START_OR_RESUME_ID = 1; @Override
private final static int QUIT_ID = 2; protected void onCreate(Bundle savedInstanceState) {
private final static int CANCEL_ID = 3; super.onCreate(savedInstanceState);
private final static int START_WTIH_QUIT = 4; setContentView(R.layout.activity_app_view);
public final static String NAME_EXTRA = "Name"; byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA);
public final static String UUID_EXTRA = "UUID"; uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA);
remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false);
if (address == null || uniqueId == null) {
return;
}
private ComputerManagerService.ComputerManagerBinder managerBinder; String labelText = "App List for "+getIntent().getStringExtra(NAME_EXTRA);
private final ServiceConnection serviceConnection = new ServiceConnection() { TextView label = (TextView) findViewById(R.id.appListText);
public void onServiceConnected(ComponentName className, IBinder binder) { setTitle(labelText);
final ComputerManagerService.ComputerManagerBinder localBinder = label.setText(labelText);
((ComputerManagerService.ComputerManagerBinder)binder);
// Wait in a separate thread to avoid stalling the UI try {
new Thread() { ipAddress = InetAddress.getByAddress(address);
@Override } catch (UnknownHostException e) {
public void run() { return;
// Wait for the binder to be ready }
localBinder.waitForReady();
// Now make the binder visible // Setup the list view
managerBinder = localBinder; appList = (ListView)findViewById(R.id.pcListView);
appListAdapter = new ArrayAdapter<AppObject>(this, R.layout.simplerow, R.id.rowTextView);
appListAdapter.setNotifyOnChange(false);
appList.setAdapter(appListAdapter);
appList.setItemsCanFocus(true);
appList.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
AppObject app = appListAdapter.getItem(pos);
if (app == null || app.app == null) {
return;
}
// Get the computer object // Only open the context menu if something is running, otherwise start it
computer = managerBinder.getComputer(UUID.fromString(uuidString)); if (getRunningAppId() != -1) {
openContextMenu(arg1);
}
else {
doStart(app.app);
}
}
});
registerForContextMenu(appList);
}
try { @Override
appGridAdapter = new AppGridAdapter(AppView.this, protected void onDestroy() {
PreferenceConfiguration.readPreferences(AppView.this).listMode, super.onDestroy();
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
computer, managerBinder.getUniqueId());
} catch (Exception e) {
e.printStackTrace();
finish();
return;
}
// Start updates SpinnerDialog.closeDialogs(this);
startComputerUpdates(); Dialog.closeDialogs();
}
// Load the app grid with cached data (if possible) @Override
populateAppGridWithCache(); protected void onResume() {
super.onResume();
getFragmentManager().beginTransaction() updateAppList();
.replace(R.id.appFragmentContainer, new AdapterFragment()) }
.commitAllowingStateLoss();
}
}.start();
}
public void onServiceDisconnected(ComponentName className) { private int getRunningAppId() {
managerBinder = null;
}
};
private InetAddress getAddress() {
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp : computer.remoteIp;
}
private void startComputerUpdates() {
if (managerBinder == null) {
return;
}
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(ComputerDetails details) {
// Don't care about other computers
if (details != computer) {
return;
}
if (details.state != ComputerDetails.State.ONLINE) {
consecutiveAppListFailures++;
if (consecutiveAppListFailures >= CONSECUTIVE_FAILURE_LIMIT) {
// The PC is unreachable now
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
// Display a toast to the user and quit the activity
Toast.makeText(AppView.this, getResources().getText(R.string.lost_connection), Toast.LENGTH_SHORT).show();
finish();
}
});
}
return;
}
consecutiveAppListFailures = 0;
// App list is the same or empty; nothing to do
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
return;
}
try {
lastRawApplist = details.rawAppList;
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
} catch (Exception ignored) {}
}
});
if (poller == null) {
poller = managerBinder.createAppListPoller(computer);
}
poller.start();
}
private void stopComputerUpdates() {
if (poller != null) {
poller.stop();
}
if (managerBinder != null) {
managerBinder.stopPolling();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
setContentView(R.layout.activity_app_view);
UiHelper.notifyNewRootView(this);
uuidString = getIntent().getStringExtra(UUID_EXTRA);
String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA);
TextView label = (TextView) findViewById(R.id.appListText);
setTitle(labelText);
label.setText(labelText);
// Bind to the computer manager service
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
}
private void populateAppGridWithCache() {
try {
// Try to load from cache
lastRawApplist = CacheHelper.readInputStreamToString(CacheHelper.openCacheFileForInput(getCacheDir(), "applist", uuidString));
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
updateUiWithAppList(applist);
LimeLog.info("Loaded applist from cache");
} catch (Exception e) {
if (lastRawApplist != null) {
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
e.printStackTrace();
}
LimeLog.info("Loading applist from the network");
// We'll need to load from the network
loadAppsBlocking();
}
}
private void loadAppsBlocking() {
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
getResources().getString(R.string.applist_refresh_msg), true);
}
@Override
protected void onDestroy() {
super.onDestroy();
SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs();
if (managerBinder != null) {
unbindService(serviceConnection);
}
}
@Override
protected void onResume() {
super.onResume();
startComputerUpdates();
}
@Override
protected void onPause() {
super.onPause();
stopComputerUpdates();
}
private int getRunningAppId() {
int runningAppId = -1; int runningAppId = -1;
for (int i = 0; i < appGridAdapter.getCount(); i++) { for (int i = 0; i < appListAdapter.getCount(); i++) {
AppObject app = (AppObject) appGridAdapter.getItem(i); AppObject app = appListAdapter.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 = appListAdapter.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, RESUME_ID, 1, "Resume Session");
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit)); menu.add(Menu.NONE, QUIT_ID, 2, "Quit Session");
} }
else { else {
menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start)); menu.add(Menu.NONE, RESUME_ID, 1, "Quit Current Game and Start");
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel)); menu.add(Menu.NONE, CANCEL_ID, 2, "Cancel");
} }
} }
} }
@Override @Override
public void onContextMenuClosed(Menu menu) { public void onContextMenuClosed(Menu menu) {
} }
private void displayQuitConfirmationDialog(final Runnable onYes, final Runnable onNo) {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which){
case DialogInterface.BUTTON_POSITIVE:
if (onYes != null) {
onYes.run();
}
break;
case DialogInterface.BUTTON_NEGATIVE:
if (onNo != null) {
onNo.run();
}
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(getResources().getString(R.string.applist_quit_confirmation))
.setPositiveButton(getResources().getString(R.string.yes), dialogClickListener)
.setNegativeButton(getResources().getString(R.string.no), dialogClickListener)
.show();
}
@Override @Override
public boolean onContextItemSelected(MenuItem item) { public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
final AppObject app = (AppObject) appGridAdapter.getItem(info.position); AppObject app = appListAdapter.getItem(info.position);
switch (item.getItemId()) { switch (item.getItemId())
case START_WTIH_QUIT: {
// Display a confirmation dialog first case RESUME_ID:
displayQuitConfirmationDialog(new Runnable() { // Resume is the same as start for us
@Override doStart(app.app);
public void run() { return true;
doStart(app.app);
}
}, null);
return true;
case START_OR_RESUME_ID: case QUIT_ID:
// Resume is the same as start for us doQuit(app.app);
doStart(app.app); return true;
return true;
case QUIT_ID: case CANCEL_ID:
// Display a confirmation dialog first return true;
displayQuitConfirmationDialog(new Runnable() {
@Override
public void run() {
doQuit(app.app);
}
}, null);
return true;
case CANCEL_ID: default:
return true; return super.onContextItemSelected(item);
default:
return super.onContextItemSelected(item);
} }
} }
private void updateUiWithAppList(final List<NvApp> appList) { private static String generateString(NvApp app) {
AppView.this.runOnUiThread(new Runnable() { StringBuilder str = new StringBuilder();
@Override str.append(app.getAppName());
public void run() { if (app.getIsRunning()) {
boolean updated = false; str.append(" - Running");
}
for (NvApp app : appList) { return str.toString();
boolean foundExistingApp = false;
// Try to update an existing app in the list first
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
if (existingApp.app == null) {
continue;
}
if (existingApp.app.getAppId() == app.getAppId()) {
// Found the app; update its properties
if (existingApp.app.getIsRunning() != app.getIsRunning()) {
existingApp.app.setIsRunningBoolean(app.getIsRunning());
updated = true;
}
if (!existingApp.app.getAppName().equals(app.getAppName())) {
existingApp.app.setAppName(app.getAppName());
updated = true;
}
foundExistingApp = true;
break;
}
}
if (!foundExistingApp) {
// This app must be new
appGridAdapter.addApp(new AppObject(app));
updated = true;
}
}
if (updated) {
appGridAdapter.notifyDataSetChanged();
}
}
});
} }
private void doStart(NvApp app) { private void addListPlaceholder() {
Intent intent = new Intent(this, Game.class); appListAdapter.add(new AppObject("No apps found. Try rescanning for games in GeForce Experience.", null));
intent.putExtra(Game.EXTRA_HOST,
computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
intent.putExtra(Game.EXTRA_APP, app.getAppName());
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
computer.reachability != ComputerDetails.Reachability.LOCAL);
startActivity(intent);
} }
private void doQuit(final NvApp app) { private void updateAppList() {
Toast.makeText(AppView.this, getResources().getString(R.string.applist_quit_app)+" "+app.getAppName()+"...", Toast.LENGTH_SHORT).show(); final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, "App List", "Refreshing app list...", true);
new Thread(new Runnable() { new Thread() {
@Override @Override
public void run() { public void run() {
NvHTTP httpConn; NvHTTP httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
String message;
try {
httpConn = new NvHTTP(getAddress(),
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(AppView.this));
if (httpConn.quitApp()) {
message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName();
}
else {
message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName();
}
} catch (UnknownHostException e) {
message = getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) {
message = getResources().getString(R.string.error_404);
} catch (Exception e) {
message = e.getMessage();
} finally {
// Trigger a poll immediately
if (poller != null) {
poller.pollNow();
}
}
final String toastMessage = message; try {
runOnUiThread(new Runnable() { final List<NvApp> appList = httpConn.getAppList();
@Override
public void run() { AppView.this.runOnUiThread(new Runnable() {
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show(); @Override
} public void run() {
}); appListAdapter.clear();
} if (appList.isEmpty()) {
}).start(); addListPlaceholder();
}
else {
for (NvApp app : appList) {
appListAdapter.add(new AppObject(generateString(app), app));
}
}
appListAdapter.notifyDataSetChanged();
}
});
// Success case
return;
} catch (GfeHttpResponseException ignored) {
} catch (IOException ignored) {
} catch (XmlPullParserException ignored) {
} finally {
spinner.dismiss();
}
Dialog.displayDialog(AppView.this, "Error", "Failed to get app list", true);
}
}.start();
} }
@Override private void doStart(NvApp app) {
public int getAdapterFragmentLayoutId() { Intent intent = new Intent(this, Game.class);
return PreferenceConfiguration.readPreferences(this).listMode ? intent.putExtra(Game.EXTRA_HOST, ipAddress.getHostAddress());
R.layout.list_view : (PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ? intent.putExtra(Game.EXTRA_APP, app.getAppName());
R.layout.app_grid_view_small : R.layout.app_grid_view); intent.putExtra(Game.EXTRA_UNIQUEID, uniqueId);
} intent.putExtra(Game.EXTRA_STREAMING_REMOTE, remote);
startActivity(intent);
}
@Override private void doQuit(final NvApp app) {
public void receiveAbsListView(AbsListView listView) { Toast.makeText(AppView.this, "Quitting "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
listView.setAdapter(appGridAdapter); new Thread(new Runnable() {
listView.setOnItemClickListener(new OnItemClickListener() { @Override
@Override public void run() {
public void onItemClick(AdapterView<?> arg0, View arg1, int pos, NvHTTP httpConn;
long id) { String message;
AppObject app = (AppObject) appGridAdapter.getItem(pos); try {
if (app == null || app.app == null) { httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
return; if (httpConn.quitApp()) {
} message = "Successfully quit "+app.getAppName();
}
else {
message = "Failed to quit "+app.getAppName();
}
updateAppList();
} catch (UnknownHostException e) {
message = "Failed to resolve host";
} catch (FileNotFoundException e) {
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
+ "Try rebooting your machine or reinstalling GFE.";
} catch (Exception e) {
message = e.getMessage();
}
// Only open the context menu if something is running, otherwise start it final String toastMessage = message;
if (getRunningAppId() != -1) { runOnUiThread(new Runnable() {
openContextMenu(arg1); @Override
} else { public void run() {
doStart(app.app); Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
} }
} });
}); }
registerForContextMenu(listView); }).start();
listView.requestFocus(); }
}
public class AppObject { public class AppObject {
public final NvApp app; public String text;
public NvApp app;
public AppObject(NvApp app) { public AppObject(String text, NvApp app) {
this.app = app; this.text = text;
} this.app = app;
}
@Override @Override
public String toString() { public String toString() {
return app.getAppName(); return text;
} }
} }
} }
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -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,89 +9,62 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
public class AndroidAudioRenderer implements AudioRenderer { public class AndroidAudioRenderer implements AudioRenderer {
private static final int FRAME_SIZE = 960; public static final int FRAME_SIZE = 960;
private AudioTrack track; private AudioTrack track;
@Override @Override
public boolean streamInitialized(int channelCount, int sampleRate) { public boolean streamInitialized(int channelCount, int sampleRate) {
int channelConfig; int channelConfig;
int bufferSize; int bufferSize;
switch (channelCount) switch (channelCount)
{ {
case 1: case 1:
channelConfig = AudioFormat.CHANNEL_OUT_MONO; channelConfig = AudioFormat.CHANNEL_OUT_MONO;
break; break;
case 2: case 2:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO; channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break; break;
default: default:
LimeLog.severe("Decoder returned unhandled channel count"); LimeLog.severe("Decoder returned unhandled channel count");
return false; return false;
} }
// We're not supposed to request less than the minimum bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
// buffer size for our buffer, but it appears that we can channelConfig,
// do this on many devices and it lowers audio latency. AudioFormat.ENCODING_PCM_16BIT),
// We'll try the small buffer size first and if it fails, FRAME_SIZE * 2);
// use the recommended larger buffer size.
try {
// Buffer two frames of audio if possible
bufferSize = FRAME_SIZE * 2;
track = new AudioTrack(AudioManager.STREAM_MUSIC, // Round to next frame
sampleRate, bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE);
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
track.play();
} catch (Exception e) {
// Try to release the AudioTrack if we got far enough
try {
if (track != null) {
track.release();
}
} catch (Exception ignored) {}
// Now try the larger buffer size LimeLog.info("Audio track buffer size: "+bufferSize);
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate, track = new AudioTrack(AudioManager.STREAM_MUSIC,
channelConfig, sampleRate,
AudioFormat.ENCODING_PCM_16BIT), channelConfig,
FRAME_SIZE * 2); AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
// Round to next frame track.play();
bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE); return true;
}
track = new AudioTrack(AudioManager.STREAM_MUSIC, @Override
sampleRate, public void playDecodedAudio(byte[] audioData, int offset, int length) {
channelConfig, track.write(audioData, offset, length);
AudioFormat.ENCODING_PCM_16BIT, }
bufferSize,
AudioTrack.MODE_STREAM);
track.play();
}
LimeLog.info("Audio track buffer size: "+bufferSize); @Override
public void streamClosing() {
if (track != null) {
track.release();
}
}
return true; @Override
} public int getCapabilities() {
return 0;
@Override }
public void playDecodedAudio(byte[] audioData, int offset, int length) {
track.write(audioData, offset, length);
}
@Override
public void streamClosing() {
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 final File certFile; private File certFile;
private final File keyFile; private 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() { private boolean loadCertKeyPair() {
byte[] certBytes = loadFileToBytes(certFile); byte[] certBytes = loadFileToBytes(certFile);
byte[] keyBytes = loadFileToBytes(keyFile); byte[] keyBytes = loadFileToBytes(keyFile);
// If either file was missing, we definitely can't succeed // If either file was missing, we definitely can't succeed
if (certBytes == null || keyBytes == null) { if (certBytes == null || keyBytes == null) {
LimeLog.info("Missing cert or key; need to generate a new one"); LimeLog.info("Missing cert or key; need to generate a new one");
return false; return false;
} }
try { try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC"); CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes)); cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
pemCertBytes = certBytes; pemCertBytes = certBytes;
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC"); KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
} catch (CertificateException e) { } catch (CertificateException e) {
// May happen if the cert is corrupt // May happen if the cert is corrupt
LimeLog.warning("Corrupted certificate"); LimeLog.warning("Corrupted certificate");
return false; return false;
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
// Should never happen // Should never happen
e.printStackTrace(); e.printStackTrace();
return false; return false;
} catch (InvalidKeySpecException e) { } catch (InvalidKeySpecException e) {
// May happen if the key is corrupt // May happen if the key is corrupt
LimeLog.warning("Corrupted key"); LimeLog.warning("Corrupted key");
return false; return false;
} catch (NoSuchProviderException e) { } catch (NoSuchProviderException e) {
// Should never happen // Should never happen
e.printStackTrace(); e.printStackTrace();
return false; return false;
} }
return true; return true;
} }
@SuppressLint("TrulyRandom") @SuppressLint("TrulyRandom")
private boolean generateCertKeyPair() { private boolean generateCertKeyPair() {
byte[] snBytes = new byte[8]; byte[] snBytes = new byte[8];
new SecureRandom().nextBytes(snBytes); new SecureRandom().nextBytes(snBytes);
KeyPair keyPair; KeyPair keyPair;
try { try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
keyPairGenerator.initialize(2048); keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair(); keyPair = keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e1) { } catch (NoSuchAlgorithmException e1) {
// Should never happen // Should never happen
e1.printStackTrace(); e1.printStackTrace();
return false; return false;
} catch (NoSuchProviderException e) { } catch (NoSuchProviderException e) {
// Should never happen // Should never happen
e.printStackTrace(); e.printStackTrace();
return false; return false;
} }
Date now = new Date(); Date now = new Date();
// Expires in 20 years // Expires in 20 years
Calendar calendar = Calendar.getInstance(); Calendar calendar = Calendar.getInstance();
calendar.setTime(now); calendar.setTime(now);
calendar.add(Calendar.YEAR, 20); calendar.add(Calendar.YEAR, 20);
Date expirationDate = calendar.getTime(); Date expirationDate = calendar.getTime();
BigInteger serial = new BigInteger(snBytes).abs(); BigInteger serial = new BigInteger(snBytes).abs();
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE); X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client"); nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
X500Name name = nameBuilder.build(); X500Name name = nameBuilder.build();
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name, X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded())); SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
try { try {
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate()); ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen)); cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
key = (RSAPrivateKey) keyPair.getPrivate(); key = (RSAPrivateKey) keyPair.getPrivate();
} catch (Exception e) { } catch (Exception e) {
// Nothing should go wrong here // Nothing should go wrong here
e.printStackTrace(); e.printStackTrace();
return false; return false;
} }
LimeLog.info("Generated a new key pair"); LimeLog.info("Generated a new key pair");
// Save the resulting pair // Save the resulting pair
saveCertKeyPair(); saveCertKeyPair();
return true; return true;
} }
private void saveCertKeyPair() { private void saveCertKeyPair() {
try { try {
FileOutputStream certOut = new FileOutputStream(certFile); FileOutputStream certOut = new FileOutputStream(certFile);
FileOutputStream keyOut = new FileOutputStream(keyFile); FileOutputStream keyOut = new FileOutputStream(keyFile);
// Write the certificate in OpenSSL PEM format (important for the server) // Write the certificate in OpenSSL PEM format (important for the server)
StringWriter strWriter = new StringWriter(); StringWriter strWriter = new StringWriter();
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter); JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
pemWriter.writeObject(cert); pemWriter.writeObject(cert);
pemWriter.close(); pemWriter.close();
// Line endings MUST be UNIX for the PC to accept the cert properly // Line endings MUST be UNIX for the PC to accept the cert properly
OutputStreamWriter certWriter = new OutputStreamWriter(certOut); OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
String pemStr = strWriter.getBuffer().toString(); String pemStr = strWriter.getBuffer().toString();
for (int i = 0; i < pemStr.length(); i++) { for (int i = 0; i < pemStr.length(); i++) {
char c = pemStr.charAt(i); char c = pemStr.charAt(i);
if (c != '\r') if (c != '\r')
certWriter.append(c); certWriter.append(c);
} }
certWriter.close(); certWriter.close();
// Write the private out in PKCS8 format // Write the private out in PKCS8 format
keyOut.write(key.getEncoded()); keyOut.write(key.getEncoded());
certOut.close(); certOut.close();
keyOut.close(); keyOut.close();
LimeLog.info("Saved generated key pair to disk"); LimeLog.info("Saved generated key pair to disk");
} catch (IOException e) { } catch (IOException e) {
// This isn't good because it means we'll have // This isn't good because it means we'll have
// to re-pair next time // to re-pair next time
e.printStackTrace(); e.printStackTrace();
} }
} }
public X509Certificate getClientCertificate() { public X509Certificate getClientCertificate() {
// Use a lock here to ensure only one guy will be generating or loading // Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time // the certificate and key at a time
synchronized (globalCryptoLock) { synchronized (globalCryptoLock) {
// Return a loaded cert if we have one // Return a loaded cert if we have one
if (cert != null) { if (cert != null) {
return cert; return cert;
} }
// No loaded cert yet, let's see if we have one on disk // No loaded cert yet, let's see if we have one on disk
if (loadCertKeyPair()) { if (loadCertKeyPair()) {
// Got one // Got one
return cert; return cert;
} }
// Try to generate a new key pair // Try to generate a new key pair
if (!generateCertKeyPair()) { if (!generateCertKeyPair()) {
// Failed // Failed
return null; return null;
} }
// Load the generated pair // Load the generated pair
loadCertKeyPair(); loadCertKeyPair();
return cert; return cert;
} }
} }
public RSAPrivateKey getClientPrivateKey() { public RSAPrivateKey getClientPrivateKey() {
// Use a lock here to ensure only one guy will be generating or loading // Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time // the certificate and key at a time
synchronized (globalCryptoLock) { synchronized (globalCryptoLock) {
// Return a loaded key if we have one // Return a loaded key if we have one
if (key != null) { if (key != null) {
return key; return key;
} }
// No loaded key yet, let's see if we have one on disk // No loaded key yet, let's see if we have one on disk
if (loadCertKeyPair()) { if (loadCertKeyPair()) {
// Got one // Got one
return key; return key;
} }
// Try to generate a new key pair // Try to generate a new key pair
if (!generateCertKeyPair()) { if (!generateCertKeyPair()) {
// Failed // Failed
return null; return null;
} }
// Load the generated pair // Load the generated pair
loadCertKeyPair(); loadCertKeyPair();
return key; return key;
} }
} }
public byte[] getPemEncodedClientCertificate() { public byte[] getPemEncodedClientCertificate() {
synchronized (globalCryptoLock) { synchronized (globalCryptoLock) {
// Call our helper function to do the cert loading/generation for us // Call our helper function to do the cert loading/generation for us
getClientCertificate(); getClientCertificate();
// Return a cached value if we have it // Return a cached value if we have it
return pemCertBytes; return pemCertBytes;
} }
} }
@Override @Override
public String encodeBase64String(byte[] data) { public String encodeBase64String(byte[] data) {
return Base64.encodeToString(data, Base64.NO_WRAP); 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
*/ */
private static final short KEY_PREFIX = (short) 0x80; public 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,117 +4,90 @@ 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 final NvConnection conn; private NvConnection conn;
private final int actionIndex; private int actionIndex;
private final double xFactor;
private final double yFactor;
private static final int TAP_MOVEMENT_THRESHOLD = 10; private static final int TAP_MOVEMENT_THRESHOLD = 10;
private static final int TAP_TIME_THRESHOLD = 250; private static final int TAP_TIME_THRESHOLD = 250;
public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor) public TouchContext(NvConnection conn, int actionIndex)
{ {
this.conn = conn; this.conn = conn;
this.actionIndex = actionIndex; this.actionIndex = actionIndex;
this.xFactor = xFactor; }
this.yFactor = yFactor;
}
public int getActionIndex() private boolean isTap()
{ {
return actionIndex; int xDelta = Math.abs(lastTouchX - originalTouchX);
} int yDelta = Math.abs(lastTouchY - originalTouchY);
long timeDelta = System.currentTimeMillis() - originalTouchTime;
private boolean isTap() return xDelta <= TAP_MOVEMENT_THRESHOLD &&
{ yDelta <= TAP_MOVEMENT_THRESHOLD &&
int xDelta = Math.abs(lastTouchX - originalTouchX); timeDelta <= TAP_TIME_THRESHOLD;
int yDelta = Math.abs(lastTouchY - originalTouchY); }
long timeDelta = System.currentTimeMillis() - originalTouchTime;
return xDelta <= TAP_MOVEMENT_THRESHOLD && private byte getMouseButtonIndex()
yDelta <= TAP_MOVEMENT_THRESHOLD && {
timeDelta <= TAP_TIME_THRESHOLD; if (actionIndex == 1) {
} return MouseButtonPacket.BUTTON_RIGHT;
}
else {
return MouseButtonPacket.BUTTON_LEFT;
}
}
private byte getMouseButtonIndex() public boolean touchDownEvent(int eventX, int eventY)
{ {
if (actionIndex == 1) { originalTouchX = lastTouchX = eventX;
return MouseButtonPacket.BUTTON_RIGHT; originalTouchY = lastTouchY = eventY;
} originalTouchTime = System.currentTimeMillis();
else {
return MouseButtonPacket.BUTTON_LEFT;
}
}
public boolean touchDownEvent(int eventX, int eventY) return true;
{ }
originalTouchX = lastTouchX = eventX;
originalTouchY = lastTouchY = eventY;
originalTouchTime = System.currentTimeMillis();
cancelled = false;
return true; public void touchUpEvent(int eventX, int eventY)
} {
if (isTap())
{
byte buttonIndex = getMouseButtonIndex();
public void touchUpEvent(int eventX, int eventY) // Lower the mouse button
{ conn.sendMouseButtonDown(buttonIndex);
if (cancelled) {
return;
}
if (isTap()) // We need to sleep a bit here because some games
{ // do input detection by polling
byte buttonIndex = getMouseButtonIndex(); try {
Thread.sleep(100);
} catch (InterruptedException ignored) {}
// Lower the mouse button // Raise the mouse button
conn.sendMouseButtonDown(buttonIndex); conn.sendMouseButtonUp(buttonIndex);
}
}
// We need to sleep a bit here because some games public boolean touchMoveEvent(int eventX, int eventY)
// do input detection by polling {
try { if (eventX != lastTouchX || eventY != lastTouchY)
Thread.sleep(100); {
} catch (InterruptedException ignored) {} // We only send moves for the primary touch point
if (actionIndex == 0) {
conn.sendMouseMove((short)(eventX - lastTouchX),
(short)(eventY - lastTouchY));
}
// Raise the mouse button lastTouchX = eventX;
conn.sendMouseButtonUp(buttonIndex); lastTouchY = eventY;
}
}
public boolean touchMoveEvent(int eventX, int eventY) return true;
{ }
if (eventX != lastTouchX || eventY != lastTouchY)
{
// We only send moves for the primary touch point
if (actionIndex == 0) {
int deltaX = eventX - lastTouchX;
int deltaY = eventY - lastTouchY;
// Scale the deltas based on the factors passed to our constructor return false;
deltaX = (int)Math.round((double)deltaX * xFactor); }
deltaY = (int)Math.round((double)deltaY * yFactor);
conn.sendMouseMove((short)deltaX, (short)deltaY);
}
lastTouchX = eventX;
lastTouchY = eventY;
}
return true;
}
public void cancelTouch() {
cancelled = true;
}
public boolean isCancelled() {
return cancelled;
}
} }
@@ -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 final short type; public short type;
public final short code; public short code;
public final int value; public 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 final String absolutePath; private String absolutePath;
private final EvdevListener listener; private EvdevListener listener;
private boolean shutdown = false; private boolean shutdown = false;
private int fd = -1; private int fd = -1;
private final Thread handlerThread = new Thread() { private Thread handlerThread = new Thread() {
@Override @Override
public void run() { public void run() {
// All the finally blocks here make this code look like a mess // All the finally blocks here make this code look like a mess
// but it's important that we get this right to avoid causing // but it's important that we get this right to avoid causing
// system-wide input problems. // system-wide input problems.
// Open the /dev/input/eventX file // Open the /dev/input/eventX file
fd = EvdevReader.open(absolutePath); fd = EvdevReader.open(absolutePath);
if (fd == -1) { if (fd == -1) {
LimeLog.warning("Unable to open "+absolutePath); LimeLog.warning("Unable to open "+absolutePath);
return; return;
} }
try { try {
// Check if it's a mouse or keyboard, but not a gamepad // Check if it's a mouse or keyboard, but not a gamepad
if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) || if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) ||
EvdevReader.isGamepad(fd)) { EvdevReader.isGamepad(fd)) {
// We only handle keyboards and mice // We only handle keyboards and mice
return; return;
} }
// Grab it for ourselves // Grab it for ourselves
if (!EvdevReader.grab(fd)) { if (!EvdevReader.grab(fd)) {
LimeLog.warning("Unable to grab "+absolutePath); LimeLog.warning("Unable to grab "+absolutePath);
return; return;
} }
LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath); LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath);
ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder()); ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder());
try { try {
int deltaX = 0; int deltaX = 0;
int deltaY = 0; int deltaY = 0;
byte deltaScroll = 0; byte deltaScroll = 0;
while (!isInterrupted() && !shutdown) { while (!isInterrupted() && !shutdown) {
EvdevEvent event = EvdevReader.read(fd, buffer); EvdevEvent event = EvdevReader.read(fd, buffer);
if (event == null) { if (event == null) {
return; return;
} }
switch (event.type) switch (event.type)
{ {
case EvdevEvent.EV_SYN: case EvdevEvent.EV_SYN:
if (deltaX != 0 || deltaY != 0) { if (deltaX != 0 || deltaY != 0) {
listener.mouseMove(deltaX, deltaY); listener.mouseMove(deltaX, deltaY);
deltaX = deltaY = 0; deltaX = deltaY = 0;
} }
if (deltaScroll != 0) { if (deltaScroll != 0) {
listener.mouseScroll(deltaScroll); listener.mouseScroll(deltaScroll);
deltaScroll = 0; deltaScroll = 0;
} }
break; break;
case EvdevEvent.EV_REL: case EvdevEvent.EV_REL:
switch (event.code) switch (event.code)
{ {
case EvdevEvent.REL_X: case EvdevEvent.REL_X:
deltaX = event.value; deltaX = event.value;
break; break;
case EvdevEvent.REL_Y: case EvdevEvent.REL_Y:
deltaY = event.value; deltaY = event.value;
break; break;
case EvdevEvent.REL_WHEEL: case EvdevEvent.REL_WHEEL:
deltaScroll = (byte) event.value; deltaScroll = (byte) event.value;
break; break;
} }
break; break;
case EvdevEvent.EV_KEY: case EvdevEvent.EV_KEY:
switch (event.code) switch (event.code)
{ {
case EvdevEvent.BTN_LEFT: case EvdevEvent.BTN_LEFT:
listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT, listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT,
event.value != 0); event.value != 0);
break; break;
case EvdevEvent.BTN_MIDDLE: case EvdevEvent.BTN_MIDDLE:
listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE, listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE,
event.value != 0); event.value != 0);
break; break;
case EvdevEvent.BTN_RIGHT: case EvdevEvent.BTN_RIGHT:
listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT, listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT,
event.value != 0); event.value != 0);
break; break;
case EvdevEvent.BTN_SIDE: case EvdevEvent.BTN_SIDE:
case EvdevEvent.BTN_EXTRA: case EvdevEvent.BTN_EXTRA:
case EvdevEvent.BTN_FORWARD: case EvdevEvent.BTN_FORWARD:
case EvdevEvent.BTN_BACK: case EvdevEvent.BTN_BACK:
case EvdevEvent.BTN_TASK: case EvdevEvent.BTN_TASK:
// Other unhandled mouse buttons // Other unhandled mouse buttons
break; break;
default: default:
// We got some unrecognized button. This means // We got some unrecognized button. This means
// someone is trying to use the other device in this // someone is trying to use the other device in this
// "combination" input device. We'll try to handle // "combination" input device. We'll try to handle
// it via keyboard, but we're not going to disconnect // it via keyboard, but we're not going to disconnect
// if we can't // if we can't
short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code); short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code);
if (keyCode != 0) { if (keyCode != 0) {
listener.keyboardEvent(event.value != 0, keyCode); listener.keyboardEvent(event.value != 0, keyCode);
} }
break; break;
} }
break; break;
case EvdevEvent.EV_MSC: case EvdevEvent.EV_MSC:
break; break;
} }
} }
} finally { } finally {
// Release our grab // Release our grab
EvdevReader.ungrab(fd); EvdevReader.ungrab(fd);
} }
} finally { } finally {
// Close the file // Close the file
EvdevReader.close(fd); EvdevReader.close(fd);
} }
} }
}; };
public EvdevHandler(String absolutePath, EvdevListener listener) { public EvdevHandler(String absolutePath, EvdevListener listener) {
this.absolutePath = absolutePath; this.absolutePath = absolutePath;
this.listener = listener; this.listener = listener;
} }
public void start() { public void start() {
handlerThread.start(); handlerThread.start();
} }
public void stop() { public void stop() {
// Close the fd. It doesn't matter if this races // Close the fd. It doesn't matter if this races
// with the handler thread. We'll close this out from // with the handler thread. We'll close this out from
// under the thread to wake it up // under the thread to wake it up
if (fd != -1) { if (fd != -1) {
EvdevReader.close(fd); EvdevReader.close(fd);
} }
shutdown = true; shutdown = true;
handlerThread.interrupt(); handlerThread.interrupt();
try { try {
handlerThread.join(); handlerThread.join();
} catch (InterruptedException ignored) {} } catch (InterruptedException ignored) {}
} }
public void notifyDeleted() { public void notifyDeleted() {
stop(); 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);
} }
@@ -1,105 +1,103 @@
package com.limelight.binding.input.evdev; package com.limelight.binding.input.evdev;
import android.os.Build; import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Locale; 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() { // Requires root to chmod /dev/input/eventX
// public static boolean setPermissions(String[] files, int octalPermissions) {
// FIXME: We REALLY shouldn't being changing permissions on the input devices like this. ProcessBuilder builder = new ProcessBuilder("su");
// We should probably do something clever with a separate daemon and talk via a localhost
// socket. We don't return the SELinux policies back to default after we're done which I feel
// bad about, but we do chmod the input devices back so I don't think any additional attack surface
// remains opened after streaming other than listing the /dev/input directory which you wouldn't
// normally be able to do with SELinux enforcing on Lollipop.
//
// We need to modify SELinux policies to allow us to capture input devices on Lollipop and possibly other
// more restrictive ROMs. Per Chainfire's SuperSU documentation, the supolicy binary is provided on
// 4.4 and later to do live SELinux policy changes.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
EvdevShell shell = EvdevShell.getInstance();
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 }\"");
}
}
// Requires root to chmod /dev/input/eventX try {
public static void setPermissions(String[] files, int octalPermissions) { Process p = builder.start();
EvdevShell shell = EvdevShell.getInstance();
for (String file : files) { OutputStream stdin = p.getOutputStream();
shell.runCommand(String.format((Locale)null, "chmod %o %s", octalPermissions, file)); for (String file : files) {
} stdin.write(String.format((Locale)null, "chmod %o %s\n", octalPermissions, file).getBytes("UTF-8"));
} }
stdin.write("exit\n".getBytes("UTF-8"));
stdin.flush();
// Returns the fd to be passed to other function or -1 on error p.waitFor();
public static native int open(String fileName); p.destroy();
return true;
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Prevent other apps (including Android itself) from using the device while "grabbed" return false;
public static native boolean grab(int fd); }
public static native boolean ungrab(int fd);
// Used for checking device capabilities // Returns the fd to be passed to other function or -1 on error
public static native boolean hasRelAxis(int fd, short axis); public static native int open(String fileName);
public static native boolean hasAbsAxis(int fd, short axis);
public static native boolean hasKey(int fd, short key);
public static boolean isMouse(int fd) { // Prevent other apps (including Android itself) from using the device while "grabbed"
// This is the same check that Android does in EventHub.cpp public static native boolean grab(int fd);
return hasRelAxis(fd, EvdevEvent.REL_X) && public static native boolean ungrab(int fd);
hasRelAxis(fd, EvdevEvent.REL_Y) &&
hasKey(fd, EvdevEvent.BTN_LEFT);
}
public static boolean isAlphaKeyboard(int fd) { // Used for checking device capabilities
// This is the same check that Android does in EventHub.cpp public static native boolean hasRelAxis(int fd, short axis);
return hasKey(fd, EvdevEvent.KEY_Q); public static native boolean hasAbsAxis(int fd, short axis);
} public static native boolean hasKey(int fd, short key);
public static boolean isGamepad(int fd) { public static boolean isMouse(int fd) {
return hasKey(fd, EvdevEvent.BTN_GAMEPAD); // This is the same check that Android does in EventHub.cpp
} return hasRelAxis(fd, EvdevEvent.REL_X) &&
hasRelAxis(fd, EvdevEvent.REL_Y) &&
hasKey(fd, EvdevEvent.BTN_LEFT);
}
// Returns the bytes read or -1 on error public static boolean isAlphaKeyboard(int fd) {
private static native int read(int fd, byte[] buffer); // This is the same check that Android does in EventHub.cpp
return hasKey(fd, EvdevEvent.KEY_Q);
}
// Takes a byte buffer to use to read the output into. public static boolean isGamepad(int fd) {
// This buffer MUST be in native byte order and at least return hasKey(fd, EvdevEvent.BTN_GAMEPAD);
// EVDEV_MAX_EVENT_SIZE bytes long. }
public static EvdevEvent read(int fd, ByteBuffer buffer) {
int bytesRead = read(fd, buffer.array());
if (bytesRead < 0) {
LimeLog.warning("Failed to read: "+bytesRead);
return null;
}
else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
LimeLog.warning("Short read: "+bytesRead);
return null;
}
buffer.limit(bytesRead); // Returns the bytes read or -1 on error
buffer.rewind(); private static native int read(int fd, byte[] buffer);
// Throw away the time stamp // Takes a byte buffer to use to read the output into.
if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) { // This buffer MUST be in native byte order and at least
buffer.getLong(); // EVDEV_MAX_EVENT_SIZE bytes long.
buffer.getLong(); public static EvdevEvent read(int fd, ByteBuffer buffer) {
} else { int bytesRead = read(fd, buffer.array());
buffer.getInt(); if (bytesRead < 0) {
buffer.getInt(); LimeLog.warning("Failed to read: "+bytesRead);
} return null;
}
else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
LimeLog.warning("Short read: "+bytesRead);
return null;
}
return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt()); buffer.limit(bytesRead);
} buffer.rewind();
// Closes the fd from open() // Throw away the time stamp
public static native int close(int fd); if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
buffer.getLong();
buffer.getLong();
} else {
buffer.getInt();
buffer.getInt();
}
return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt());
}
// Closes the fd from open()
public static native int close(int fd);
} }
@@ -1,116 +0,0 @@
package com.limelight.binding.input.evdev;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Scanner;
import java.util.UUID;
public class EvdevShell {
private OutputStream stdin;
private InputStream stdout;
private Process shell;
private final String uuidString = UUID.randomUUID().toString();
private static final EvdevShell globalShell = new EvdevShell();
public static EvdevShell getInstance() {
return globalShell;
}
public void startShell() {
ProcessBuilder builder = new ProcessBuilder("su");
try {
// Redirect stderr to stdout
builder.redirectErrorStream(true);
shell = builder.start();
stdin = shell.getOutputStream();
stdout = shell.getInputStream();
} catch (IOException e) {
// This is unexpected
e.printStackTrace();
// Kill the shell if it spawned
if (stdin != null) {
try {
stdin.close();
} catch (IOException e1) {
e1.printStackTrace();
} finally {
stdin = null;
}
}
if (stdout != null) {
try {
stdout.close();
} catch (IOException e1) {
e1.printStackTrace();
} finally {
stdout = null;
}
}
if (shell != null) {
shell.destroy();
shell = null;
}
}
}
public void runCommand(String command) {
if (shell == null) {
// Shell never started
return;
}
try {
// Write the command followed by an echo with our UUID
stdin.write((command+'\n').getBytes("UTF-8"));
stdin.write(("echo "+uuidString+'\n').getBytes("UTF-8"));
stdin.flush();
// This is the only command in flight so we can use a scanner
// without worrying about it eating too many characters
Scanner scanner = new Scanner(stdout);
while (scanner.hasNext()) {
if (scanner.next().contains(uuidString)) {
// Our command ran
return;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void stopShell() throws InterruptedException {
boolean exitWritten = false;
if (shell == null) {
// Shell never started
return;
}
try {
stdin.write("exit\n".getBytes("UTF-8"));
exitWritten = true;
} catch (IOException e) {
// We'll destroy the process without
// waiting for it to terminate since
// we don't know whether our exit command made it
e.printStackTrace();
}
if (exitWritten) {
try {
shell.waitFor();
} finally {
shell.destroy();
}
}
else {
shell.destroy();
}
}
}
@@ -4,136 +4,136 @@ import android.view.KeyEvent;
public class EvdevTranslator { public class EvdevTranslator {
private static final short[] EVDEV_KEY_CODES = { public 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,179 +10,163 @@ 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 FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) {
@Override
public void onEvent(int event, String fileName) {
if (fileName == null) {
return;
}
private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) { if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) {
@Override return;
public void onEvent(int event, String fileName) { }
if (fileName == null) {
return;
}
if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) { synchronized (handlers) {
return; if (shutdown) {
} return;
}
synchronized (handlers) { if ((event & FileObserver.CREATE) != 0) {
if (shutdown) { LimeLog.info("Starting evdev handler for "+fileName);
return;
}
if ((event & FileObserver.CREATE) != 0) { if (!init) {
LimeLog.info("Starting evdev handler for "+fileName); // If this a real new device, update permissions again so we can read it
EvdevReader.setPermissions(new String[]{PATH + "/" + fileName}, 0666);
}
if (!init) { EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener);
// If this a real new device, update permissions again so we can read it
EvdevReader.setPermissions(new String[]{PATH + "/" + fileName}, 0666);
}
EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener); // If we're ungrabbed now, don't start the handler
if (!ungrabbed) {
handler.start();
}
// If we're ungrabbed now, don't start the handler handlers.put(fileName, handler);
if (!ungrabbed) { }
handler.start();
}
handlers.put(fileName, handler); if ((event & FileObserver.DELETE) != 0) {
} LimeLog.info("Halting evdev handler for "+fileName);
if ((event & FileObserver.DELETE) != 0) { EvdevHandler handler = handlers.remove(fileName);
LimeLog.info("Halting evdev handler for "+fileName); if (handler != null) {
handler.notifyDeleted();
}
}
}
}
};
EvdevHandler handler = handlers.remove(fileName); public EvdevWatcher(EvdevListener listener) {
if (handler != null) { this.listener = listener;
handler.notifyDeleted(); }
}
}
}
}
};
public EvdevWatcher(EvdevListener listener) { private File[] rundownWithPermissionsChange(int newPermissions) {
this.listener = listener; // Rundown existing files
} File devInputDir = new File(PATH);
File[] files = devInputDir.listFiles();
if (files == null) {
return new File[0];
}
private File[] rundownWithPermissionsChange(int newPermissions) { // Set desired permissions
// Rundown existing files String[] filePaths = new String[files.length];
File devInputDir = new File(PATH); for (int i = 0; i < files.length; i++) {
File[] files = devInputDir.listFiles(); filePaths[i] = files[i].getAbsolutePath();
if (files == null) { }
return new File[0]; EvdevReader.setPermissions(filePaths, newPermissions);
}
// Set desired permissions return files;
String[] filePaths = new String[files.length]; }
for (int i = 0; i < files.length; i++) {
filePaths[i] = files[i].getAbsolutePath();
}
EvdevReader.setPermissions(filePaths, newPermissions);
return files; public void ungrabAll() {
} synchronized (handlers) {
// Note that we're ungrabbed for now
ungrabbed = true;
public void ungrabAll() { // Stop all handlers
synchronized (handlers) { for (EvdevHandler handler : handlers.values()) {
// Note that we're ungrabbed for now handler.stop();
ungrabbed = true; }
}
}
// Stop all handlers public void regrabAll() {
for (EvdevHandler handler : handlers.values()) { synchronized (handlers) {
handler.stop(); // We're regrabbing everything now
} ungrabbed = false;
}
}
public void regrabAll() { for (Map.Entry<String, EvdevHandler> entry : handlers.entrySet()) {
synchronized (handlers) { // We need to recreate each entry since we can't reuse a stopped one
// We're regrabbing everything now entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener));
ungrabbed = false; entry.getValue().start();
}
}
}
for (Map.Entry<String, EvdevHandler> entry : handlers.entrySet()) { public void start() {
// We need to recreate each entry since we can't reuse a stopped one startThread = new Thread() {
entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener)); @Override
entry.getValue().start(); public void run() {
} // List all files and allow us access
} File[] files = rundownWithPermissionsChange(0666);
}
public void start() { init = true;
startThread = new Thread() { for (File f : files) {
@Override observer.onEvent(FileObserver.CREATE, f.getName());
public void run() { }
// Initialize the root shell
EvdevShell.getInstance().startShell();
// Patch SELinux policies (if needed) // Done with initial onEvent calls
if (!patchedSeLinuxPolicies) { init = false;
EvdevReader.patchSeLinuxPolicies();
patchedSeLinuxPolicies = true;
}
// List all files and allow us access // Start watching for new files
File[] files = rundownWithPermissionsChange(0666); observer.startWatching();
init = true; synchronized (startThread) {
for (File f : files) { // Wait to be awoken again by shutdown()
observer.onEvent(FileObserver.CREATE, f.getName()); try {
} startThread.wait();
} catch (InterruptedException e) {}
}
// Done with initial onEvent calls // Giveup eventX permissions
init = false; rundownWithPermissionsChange(066);
}
};
startThread.start();
}
// Start watching for new files public void shutdown() {
observer.startWatching(); // Let start thread cleanup on it's own sweet time
synchronized (startThread) {
startThread.notify();
}
synchronized (startThread) { // Stop the observer
// Wait to be awoken again by shutdown() observer.stopWatching();
try {
startThread.wait();
} catch (InterruptedException e) {}
}
// Giveup eventX permissions synchronized (handlers) {
rundownWithPermissionsChange(0660); // Stop creating new handlers
shutdown = true;
// Kill the root shell // If we've already ungrabbed, there's nothing else to do
try { if (ungrabbed) {
EvdevShell.getInstance().stopShell(); return;
} catch (InterruptedException e) {} }
}
};
startThread.start();
}
public void shutdown() { // Stop all handlers
// Let start thread cleanup on it's own sweet time for (EvdevHandler handler : handlers.values()) {
synchronized (startThread) { handler.stop();
startThread.notify(); }
} }
}
// Stop the observer
observer.stopWatching();
synchronized (handlers) {
// Stop creating new handlers
shutdown = true;
// If we've already ungrabbed, there's nothing else to do
if (ungrabbed) {
return;
}
// Stop all handlers
for (EvdevHandler handler : handlers.values()) {
handler.stop();
}
}
}
} }
@@ -5,6 +5,7 @@ import java.io.File;
import java.io.FileReader; import java.io.FileReader;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.concurrent.locks.LockSupport;
import android.graphics.PixelFormat; import android.graphics.PixelFormat;
import android.os.Build; import android.os.Build;
@@ -18,271 +19,245 @@ import com.limelight.nvstream.av.video.VideoDepacketizer;
import com.limelight.nvstream.av.video.cpu.AvcDecoder; import com.limelight.nvstream.av.video.cpu.AvcDecoder;
@SuppressWarnings("EmptyCatchBlock") @SuppressWarnings("EmptyCatchBlock")
public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer { public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
private Thread rendererThread, decoderThread; private Thread rendererThread;
private int targetFps; private int targetFps;
private static final int DECODER_BUFFER_SIZE = 92*1024; private static final int DECODER_BUFFER_SIZE = 92*1024;
private ByteBuffer decoderBuffer; private ByteBuffer decoderBuffer;
// Only sleep if the difference is above this value // Only sleep if the difference is above this value
private static final int WAIT_CEILING_MS = 5; private static final int WAIT_CEILING_MS = 8;
private static final int LOW_PERF = 1; private static final int LOW_PERF = 1;
private static final int MED_PERF = 2; private static final int MED_PERF = 2;
private static final int HIGH_PERF = 3; private static final int HIGH_PERF = 3;
private int totalFrames; private int totalFrames;
private long totalTimeMs; private long totalTimeMs;
private final int cpuCount = Runtime.getRuntime().availableProcessors(); private int cpuCount = Runtime.getRuntime().availableProcessors();
@SuppressWarnings("unused") @SuppressWarnings("unused")
private int findOptimalPerformanceLevel() { private int findOptimalPerformanceLevel() {
StringBuilder cpuInfo = new StringBuilder(); StringBuilder cpuInfo = new StringBuilder();
BufferedReader br = null; BufferedReader br = null;
try { try {
br = new BufferedReader(new FileReader(new File("/proc/cpuinfo"))); br = new BufferedReader(new FileReader(new File("/proc/cpuinfo")));
for (;;) { for (;;) {
int ch = br.read(); int ch = br.read();
if (ch == -1) if (ch == -1)
break; break;
cpuInfo.append((char)ch); cpuInfo.append((char)ch);
} }
// Here we're doing very simple heuristics based on CPU model // Here we're doing very simple heuristics based on CPU model
String cpuInfoStr = cpuInfo.toString(); String cpuInfoStr = cpuInfo.toString();
// We order them from greatest to least for proper detection // We order them from greatest to least for proper detection
// of devices with multiple sets of cores (like Exynos 5 Octa) // of devices with multiple sets of cores (like Exynos 5 Octa)
// TODO Make this better (only even kind of works on ARM) // TODO Make this better (only even kind of works on ARM)
if (Build.FINGERPRINT.contains("generic")) { if (Build.FINGERPRINT.contains("generic")) {
// Emulator // Emulator
return LOW_PERF; return LOW_PERF;
} }
else if (cpuInfoStr.contains("0xc0f")) { else if (cpuInfoStr.contains("0xc0f")) {
// Cortex-A15 // Cortex-A15
return MED_PERF; return MED_PERF;
} }
else if (cpuInfoStr.contains("0xc09")) { else if (cpuInfoStr.contains("0xc09")) {
// Cortex-A9 // Cortex-A9
return LOW_PERF; return LOW_PERF;
} }
else if (cpuInfoStr.contains("0xc07")) { else if (cpuInfoStr.contains("0xc07")) {
// Cortex-A7 // Cortex-A7
return LOW_PERF; return LOW_PERF;
} }
else { else {
// Didn't have anything we're looking for // Didn't have anything we're looking for
return MED_PERF; return MED_PERF;
} }
} catch (IOException e) { } catch (IOException e) {
} finally { } finally {
if (br != null) { if (br != null) {
try { try {
br.close(); br.close();
} catch (IOException e) {} } catch (IOException e) {}
} }
} }
// Couldn't read cpuinfo, so assume medium // Couldn't read cpuinfo, so assume medium
return MED_PERF; return MED_PERF;
} }
@Override @Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) { public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.targetFps = redrawRate; this.targetFps = redrawRate;
int perfLevel = LOW_PERF; //findOptimalPerformanceLevel(); int perfLevel = LOW_PERF; //findOptimalPerformanceLevel();
int threadCount; int threadCount;
int avcFlags = 0; int avcFlags = 0;
switch (perfLevel) { switch (perfLevel) {
case HIGH_PERF: case HIGH_PERF:
// Single threaded low latency decode is ideal but hard to acheive // Single threaded low latency decode is ideal but hard to acheive
avcFlags = AvcDecoder.LOW_LATENCY_DECODE; avcFlags = AvcDecoder.LOW_LATENCY_DECODE;
threadCount = 1; threadCount = 1;
break; break;
case LOW_PERF: case LOW_PERF:
// Disable the loop filter for performance reasons // Disable the loop filter for performance reasons
avcFlags = AvcDecoder.FAST_BILINEAR_FILTERING; avcFlags = AvcDecoder.DISABLE_LOOP_FILTER |
AvcDecoder.FAST_BILINEAR_FILTERING |
AvcDecoder.FAST_DECODE;
// Use plenty of threads to try to utilize the CPU as best we can // Use plenty of threads to try to utilize the CPU as best we can
threadCount = cpuCount - 1; threadCount = cpuCount - 1;
break; break;
default: default:
case MED_PERF: case MED_PERF:
avcFlags = AvcDecoder.BILINEAR_FILTERING; avcFlags = AvcDecoder.BILINEAR_FILTERING |
AvcDecoder.FAST_DECODE;
// Only use 2 threads to minimize frame processing latency // Only use 2 threads to minimize frame processing latency
threadCount = 2; threadCount = 2;
break; break;
} }
// If the user wants quality, we'll remove the low IQ flags // If the user wants quality, we'll remove the low IQ flags
if ((drFlags & VideoDecoderRenderer.FLAG_PREFER_QUALITY) != 0) { if ((drFlags & VideoDecoderRenderer.FLAG_PREFER_QUALITY) != 0) {
// Make sure the loop filter is enabled // Make sure the loop filter is enabled
avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER; avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER;
// Disable the non-compliant speed optimizations // Disable the non-compliant speed optimizations
avcFlags &= ~AvcDecoder.FAST_DECODE; avcFlags &= ~AvcDecoder.FAST_DECODE;
LimeLog.info("Using high quality decoding"); LimeLog.info("Using high quality decoding");
} }
SurfaceHolder sh = (SurfaceHolder)renderTarget; SurfaceHolder sh = (SurfaceHolder)renderTarget;
sh.setFormat(PixelFormat.RGBX_8888); sh.setFormat(PixelFormat.RGBX_8888);
int err = AvcDecoder.init(width, height, avcFlags, threadCount); int err = AvcDecoder.init(width, height, avcFlags, threadCount);
if (err != 0) { if (err != 0) {
throw new IllegalStateException("AVC decoder initialization failure: "+err); throw new IllegalStateException("AVC decoder initialization failure: "+err);
} }
if (!AvcDecoder.setRenderTarget(sh.getSurface())) { AvcDecoder.setRenderTarget(sh.getSurface());
return false;
}
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize()); decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
LimeLog.info("Using software decoding (performance level: "+perfLevel+")"); LimeLog.info("Using software decoding (performance level: "+perfLevel+")");
return true; return true;
} }
@Override @Override
public boolean start(final VideoDepacketizer depacketizer) { public boolean start(final VideoDepacketizer depacketizer) {
decoderThread = new Thread() { rendererThread = new Thread() {
@Override @Override
public void run() { public void run() {
DecodeUnit du; long nextFrameTime = System.currentTimeMillis();
while (!isInterrupted()) { DecodeUnit du;
try { while (!isInterrupted())
du = depacketizer.takeNextDecodeUnit(); {
} catch (InterruptedException e) { du = depacketizer.pollNextDecodeUnit();
break; if (du != null) {
} submitDecodeUnit(du);
depacketizer.freeDecodeUnit(du);
}
submitDecodeUnit(du); long diff = nextFrameTime - System.currentTimeMillis();
depacketizer.freeDecodeUnit(du);
}
}
};
decoderThread.setName("Video - Decoder (CPU)");
decoderThread.setPriority(Thread.MAX_PRIORITY - 1);
decoderThread.start();
rendererThread = new Thread() { if (diff > WAIT_CEILING_MS) {
@Override LockSupport.parkNanos(1);
public void run() { continue;
long nextFrameTime = System.currentTimeMillis(); }
DecodeUnit du;
while (!isInterrupted())
{
long diff = nextFrameTime - System.currentTimeMillis();
if (diff > WAIT_CEILING_MS) { nextFrameTime = computePresentationTimeMs(targetFps);
try { AvcDecoder.redraw();
Thread.sleep(diff - WAIT_CEILING_MS); }
} catch (InterruptedException e) { }
return; };
} rendererThread.setName("Video - Renderer (CPU)");
continue; rendererThread.setPriority(Thread.MAX_PRIORITY);
} rendererThread.start();
return true;
}
nextFrameTime = computePresentationTimeMs(targetFps); private long computePresentationTimeMs(int frameRate) {
AvcDecoder.redraw(); return System.currentTimeMillis() + (1000 / frameRate);
} }
}
};
rendererThread.setName("Video - Renderer (CPU)");
rendererThread.setPriority(Thread.MAX_PRIORITY);
rendererThread.start();
return true;
}
private long computePresentationTimeMs(int frameRate) { @Override
return System.currentTimeMillis() + (1000 / frameRate); public void stop() {
} rendererThread.interrupt();
@Override try {
public void stop() { rendererThread.join();
rendererThread.interrupt(); } catch (InterruptedException e) { }
decoderThread.interrupt(); }
try { @Override
rendererThread.join(); public void release() {
} catch (InterruptedException e) { } AvcDecoder.destroy();
try { }
decoderThread.join();
} catch (InterruptedException e) { }
}
@Override private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
public void release() { byte[] data;
AvcDecoder.destroy();
}
private boolean submitDecodeUnit(DecodeUnit decodeUnit) { // Use the reserved decoder buffer if this decode unit will fit
byte[] data; if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
decoderBuffer.clear();
// Use the reserved decoder buffer if this decode unit will fit for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) { decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
decoderBuffer.clear(); }
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) { data = decoderBuffer.array();
decoderBuffer.put(bbd.data, bbd.offset, bbd.length); }
} else {
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
data = decoderBuffer.array(); int offset = 0;
} for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
else { System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()]; offset += bbd.length;
}
}
int offset = 0; boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) { if (success) {
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length); long timeAfterDecode = System.currentTimeMillis();
offset += bbd.length;
}
}
boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0); // Add delta time to the totals (excluding probable outliers)
if (success) { long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
long timeAfterDecode = System.currentTimeMillis(); if (delta >= 0 && delta < 300) {
totalTimeMs += delta;
totalFrames++;
}
}
// Add delta time to the totals (excluding probable outliers) return success;
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp(); }
if (delta >= 0 && delta < 1000) {
totalTimeMs += delta;
totalFrames++;
}
}
return success; @Override
} public int getCapabilities() {
return 0;
}
@Override @Override
public int getCapabilities() { public int getAverageDecoderLatency() {
return 0; return 0;
} }
@Override @Override
public int getAverageDecoderLatency() { public int getAverageEndToEndLatency() {
return 0; if (totalFrames == 0) {
} return 0;
}
@Override return (int)(totalTimeMs / totalFrames);
public int getAverageEndToEndLatency() { }
if (totalFrames == 0) {
return 0;
}
return (int)(totalTimeMs / totalFrames);
}
@Override
public String getDecoderName() {
return "CPU decoding";
}
} }
@@ -3,85 +3,75 @@ package com.limelight.binding.video;
import com.limelight.nvstream.av.video.VideoDecoderRenderer; import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer; import com.limelight.nvstream.av.video.VideoDepacketizer;
public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer { public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
private EnhancedDecoderRenderer decoderRenderer; private VideoDecoderRenderer decoderRenderer;
@Override @Override
public void release() { public void release() {
if (decoderRenderer != null) { if (decoderRenderer != null) {
decoderRenderer.release(); decoderRenderer.release();
} }
} }
@Override @Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) { public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
if (decoderRenderer == null) { if (decoderRenderer == null) {
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized"); throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
} }
return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags); return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
} }
public void initializeWithFlags(int drFlags) { public void initializeWithFlags(int drFlags) {
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 || if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 && ((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
MediaCodecHelper.findProbableSafeDecoder() != null)) { MediaCodecHelper.findProbableSafeDecoder() != null)) {
decoderRenderer = new MediaCodecDecoderRenderer(); decoderRenderer = new MediaCodecDecoderRenderer();
} }
else { else {
decoderRenderer = new AndroidCpuDecoderRenderer(); decoderRenderer = new AndroidCpuDecoderRenderer();
} }
} }
public boolean isHardwareAccelerated() { public boolean isHardwareAccelerated() {
if (decoderRenderer == null) { if (decoderRenderer == null) {
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized"); throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
} }
return (decoderRenderer instanceof MediaCodecDecoderRenderer); return (decoderRenderer instanceof MediaCodecDecoderRenderer);
} }
@Override @Override
public boolean start(VideoDepacketizer depacketizer) { public boolean start(VideoDepacketizer depacketizer) {
return decoderRenderer.start(depacketizer); return decoderRenderer.start(depacketizer);
} }
@Override @Override
public void stop() { public void stop() {
decoderRenderer.stop(); decoderRenderer.stop();
} }
@Override @Override
public int getCapabilities() { public int getCapabilities() {
return decoderRenderer.getCapabilities(); return decoderRenderer.getCapabilities();
} }
@Override @Override
public int getAverageDecoderLatency() { public int getAverageDecoderLatency() {
if (decoderRenderer != null) { if (decoderRenderer != null) {
return decoderRenderer.getAverageDecoderLatency(); return decoderRenderer.getAverageDecoderLatency();
} }
else { else {
return 0; return 0;
} }
} }
@Override @Override
public int getAverageEndToEndLatency() { public int getAverageEndToEndLatency() {
if (decoderRenderer != null) { if (decoderRenderer != null) {
return decoderRenderer.getAverageEndToEndLatency(); return decoderRenderer.getAverageEndToEndLatency();
} }
else { else {
return 0; return 0;
} }
} }
@Override
public String getDecoderName() {
if (decoderRenderer != null) {
return decoderRenderer.getDecoderName();
}
else {
return null;
}
}
} }
@@ -1,7 +0,0 @@
package com.limelight.binding.video;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
public abstract class EnhancedDecoderRenderer implements VideoDecoderRenderer {
public abstract String getDecoderName();
}
File diff suppressed because it is too large. Load diff
@@ -20,12 +20,11 @@ import com.limelight.LimeLog;
public class MediaCodecHelper { public class MediaCodecHelper {
private static final List<String> preferredDecoders; public static final List<String> preferredDecoders;
private static final List<String> blacklistedDecoderPrefixes; public static final List<String> blacklistedDecoderPrefixes;
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes; public static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
private static final List<String> whitelistedAdaptiveResolutionPrefixes; public static final List<String> whitelistedAdaptiveResolutionPrefixes;
private static final List<String> baselineProfileHackPrefixes;
static { static {
preferredDecoders = new LinkedList<String>(); preferredDecoders = new LinkedList<String>();
@@ -44,10 +43,6 @@ public class MediaCodecHelper {
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia"); spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom"); spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk"); spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
baselineProfileHackPrefixes = new LinkedList<String>();
baselineProfileHackPrefixes.add("omx.intel");
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>(); whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia"); whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
@@ -71,10 +66,6 @@ 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
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)");
return true; return true;
@@ -93,7 +84,7 @@ public class MediaCodecHelper {
} catch (Exception e) { } catch (Exception e) {
// Tolerate buggy codecs // Tolerate buggy codecs
} }
}*/ }
return false; return false;
} }
@@ -102,10 +93,6 @@ public class MediaCodecHelper {
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName); return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
} }
public static boolean decoderNeedsBaselineSpsHack(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
}
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@SuppressLint("NewApi") @SuppressLint("NewApi")
private static LinkedList<MediaCodecInfo> getMediaCodecList() { private static LinkedList<MediaCodecInfo> getMediaCodecList() {
@@ -146,7 +133,7 @@ public class MediaCodecHelper {
return str; return str;
} }
private static MediaCodecInfo findPreferredDecoder() { public 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 +204,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")
private static MediaCodecInfo findKnownSafeDecoder() throws Exception { public 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,148 @@ 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;
// 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();
}
c.close(); return computerList;
}
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;
}
public ComputerDetails getComputerByName(String name) { details.name = c.getString(0);
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); String uuidStr = c.getString(1);
try {
details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name);
}
String uuidStr = c.getString(1); try {
try { details.localIp = InetAddress.getByAddress(c.getBlob(2));
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 local IP for "+details.name);
LimeLog.severe("DB: Corrupted UUID for "+details.name); }
}
try { try {
details.localIp = InetAddress.getByAddress(c.getBlob(2)); 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 local IP for "+details.name); LimeLog.severe("DB: Corrupted remote IP for "+details.name);
} }
try { details.macAddress = c.getString(4);
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();
c.close(); // If a field is corrupt or missing, delete the database entry
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
details.macAddress == null) {
deleteComputer(details.name);
return null;
}
details.state = ComputerDetails.State.UNKNOWN; return details;
details.reachability = ComputerDetails.Reachability.UNKNOWN; }
// If a field is corrupt or missing, delete the database entry
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
details.macAddress == null) {
deleteComputer(details.name);
return null;
}
return details;
}
} }
@@ -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,23 +1,22 @@
package com.limelight.computers; package com.limelight.computers;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.LinkedList; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import com.limelight.LimeLog; import com.limelight.LimeLog;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.discovery.DiscoveryService; import com.limelight.discovery.DiscoveryService;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.mdns.MdnsComputer; import com.limelight.nvstream.mdns.MdnsComputer;
import com.limelight.nvstream.mdns.MdnsDiscoveryListener; import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
import com.limelight.utils.CacheHelper;
import android.app.Service; import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
@@ -26,46 +25,45 @@ import android.content.ServiceConnection;
import android.os.Binder; import android.os.Binder;
import android.os.IBinder; import android.os.IBinder;
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 = 5000;
private static final int MDNS_QUERY_PERIOD_MS = 1000; private static final int MDNS_QUERY_PERIOD_MS = 1000;
private final ComputerManagerBinder binder = new ComputerManagerBinder(); private ComputerManagerBinder binder = new ComputerManagerBinder();
private ComputerDatabaseManager dbManager; private ComputerDatabaseManager dbManager;
private final AtomicInteger dbRefCount = new AtomicInteger(0); private AtomicInteger dbRefCount = new AtomicInteger(0);
private IdentityManager idManager; private IdentityManager idManager;
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>(); private HashMap<ComputerDetails, Thread> pollingThreads;
private ComputerManagerListener listener = null; private ComputerManagerListener listener = null;
private final AtomicInteger activePolls = new AtomicInteger(0); private AtomicInteger activePolls = new AtomicInteger(0);
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 // Set us as the event listener
privateBinder.setListener(createDiscoveryListener()); privateBinder.setListener(createDiscoveryListener());
// Signal a possible waiter that we're all setup // Signal a possible waiter that we're all setup
discoveryBinder = privateBinder; discoveryBinder = privateBinder;
discoveryServiceConnection.notifyAll(); discoveryServiceConnection.notifyAll();
} }
} }
public void onServiceDisconnected(ComponentName className) { public void onServiceDisconnected(ComponentName className) {
discoveryBinder = null; discoveryBinder = null;
} }
}; };
// Returns true if the details object was modified // Returns true if the details object was modified
private boolean runPoll(ComputerDetails details, boolean newPc) private boolean runPoll(ComputerDetails details)
{ {
boolean newPc = (details.name == null);
if (!getLocalDatabaseReference()) { if (!getLocalDatabaseReference()) {
return false; return false;
} }
@@ -108,9 +106,18 @@ public class ComputerManagerService extends Service {
Thread t = new Thread() { Thread t = new Thread() {
@Override @Override
public void run() { public void run() {
while (!isInterrupted() && pollingActive) { while (!isInterrupted()) {
ComputerDetails originalDetails = new ComputerDetails();
originalDetails.update(details);
// Check if this poll has modified the details // Check if this poll has modified the details
runPoll(details, false); if (runPoll(details) && !originalDetails.equals(details)) {
// Replace our thread entry with the new one
synchronized (pollingThreads) {
pollingThreads.remove(originalDetails);
pollingThreads.put(details, this);
}
}
// Wait until the next polling interval // Wait until the next polling interval
try { try {
@@ -125,455 +132,257 @@ public class ComputerManagerService extends Service {
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 // Set the listener
pollingActive = true; ComputerManagerService.this.listener = listener;
// Set the listener // Start mDNS autodiscovery too
ComputerManagerService.this.listener = listener; discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS);
// Start mDNS autodiscovery too // Start polling known machines
discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS); if (!getLocalDatabaseReference()) {
return;
}
List<ComputerDetails> computerList = dbManager.getAllComputers();
releaseLocalDatabaseReference();
synchronized (pollingTuples) { synchronized (pollingThreads) {
for (PollingTuple tuple : pollingTuples) { for (ComputerDetails computer : computerList) {
// This polling thread might already be there // This polling thread might already be there
if (tuple.thread == null) { if (!pollingThreads.containsKey(computer)) {
// Report this computer initially Thread t = createPollingThread(computer);
listener.notifyComputerUpdated(tuple.computer); pollingThreads.put(computer, t);
t.start();
tuple.thread = createPollingThread(tuple.computer);
tuple.thread.start();
} }
} }
} }
}
public void waitForReady() {
synchronized (discoveryServiceConnection) {
try {
while (discoveryBinder == null) {
// Wait for the bind notification
discoveryServiceConnection.wait(1000);
}
} catch (InterruptedException ignored) {
}
}
}
public void waitForPollingStopped() {
while (activePolls.get() != 0) {
try {
Thread.sleep(250);
} catch (InterruptedException ignored) {}
}
}
public boolean addComputerBlocking(InetAddress addr) {
return ComputerManagerService.this.addComputerBlocking(addr);
}
public void addComputer(InetAddress addr) {
ComputerManagerService.this.addComputer(addr);
}
public void removeComputer(String name) {
ComputerManagerService.this.removeComputer(name);
}
public void stopPolling() {
// Just call the unbind handler to cleanup
ComputerManagerService.this.onUnbind(null);
}
public String getUniqueId() {
return idManager.getUniqueId();
}
}
@Override
public boolean onUnbind(Intent intent) {
// Stop mDNS autodiscovery
discoveryBinder.stopDiscovery();
// Stop polling
synchronized (pollingThreads) {
for (Thread t : pollingThreads.values()) {
t.interrupt();
}
pollingThreads.clear();
} }
public void waitForReady() { // Remove the listener
synchronized (discoveryServiceConnection) { listener = null;
try {
while (discoveryBinder == null) { return false;
// Wait for the bind notification }
discoveryServiceConnection.wait(1000);
} private MdnsDiscoveryListener createDiscoveryListener() {
} catch (InterruptedException ignored) { return new MdnsDiscoveryListener() {
} @Override
public void notifyComputerAdded(MdnsComputer computer) {
// Kick off a serverinfo poll on this machine
addComputer(computer.getAddress());
}
@Override
public void notifyComputerRemoved(MdnsComputer computer) {
// Nothing to do here
}
@Override
public void notifyDiscoveryFailure(Exception e) {
LimeLog.severe("mDNS discovery failed");
e.printStackTrace();
}
};
}
public void addComputer(InetAddress addr) {
// Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr;
fakeDetails.remoteIp = addr;
// Spawn a thread for this computer
synchronized (pollingThreads) {
// This polling thread might already be there
if (!pollingThreads.containsKey(fakeDetails)) {
Thread t = createPollingThread(fakeDetails);
pollingThreads.put(fakeDetails, t);
t.start();
} }
} }
}
public void waitForPollingStopped() {
while (activePolls.get() != 0) { public boolean addComputerBlocking(InetAddress addr) {
try { // Setup a placeholder
Thread.sleep(250); ComputerDetails fakeDetails = new ComputerDetails();
} catch (InterruptedException ignored) {} fakeDetails.localIp = addr;
} fakeDetails.remoteIp = addr;
}
// Block while we try to fill the details
public boolean addComputerBlocking(InetAddress addr) { runPoll(fakeDetails);
return ComputerManagerService.this.addComputerBlocking(addr);
} // If the machine is reachable, it was successful
return fakeDetails.state == ComputerDetails.State.ONLINE;
public void removeComputer(String name) { }
ComputerManagerService.this.removeComputer(name);
} public void removeComputer(String name) {
if (!getLocalDatabaseReference()) {
public void stopPolling() { return;
// Just call the unbind handler to cleanup }
ComputerManagerService.this.onUnbind(null);
} // Remove it from the database
dbManager.deleteComputer(name);
public ApplistPoller createAppListPoller(ComputerDetails computer) {
return new ApplistPoller(computer); releaseLocalDatabaseReference();
} }
public String getUniqueId() { private boolean getLocalDatabaseReference() {
return idManager.getUniqueId(); if (dbRefCount.get() == 0) {
} return false;
}
public ComputerDetails getComputer(UUID uuid) {
synchronized (pollingTuples) { dbRefCount.incrementAndGet();
for (PollingTuple tuple : pollingTuples) { return true;
if (uuid.equals(tuple.computer.uuid)) { }
return tuple.computer;
} private void releaseLocalDatabaseReference() {
} if (dbRefCount.decrementAndGet() == 0) {
} dbManager.close();
}
return null; }
}
} private ComputerDetails tryPollIp(InetAddress ipAddr) {
try {
@Override NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
public boolean onUnbind(Intent intent) { null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
// Stop mDNS autodiscovery
discoveryBinder.stopDiscovery(); return http.getComputerDetails();
} catch (Exception e) {
// Stop polling return null;
pollingActive = false; }
synchronized (pollingTuples) { }
for (PollingTuple tuple : pollingTuples) {
if (tuple.thread != null) { private boolean pollComputer(ComputerDetails details, boolean localFirst) {
// Interrupt and remove the thread ComputerDetails polledDetails;
tuple.thread.interrupt();
tuple.thread = null; if (localFirst) {
} polledDetails = tryPollIp(details.localIp);
} }
} else {
polledDetails = tryPollIp(details.remoteIp);
// Remove the listener }
listener = null;
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
return false; // Failed, so let's try the fallback
} if (!localFirst) {
polledDetails = tryPollIp(details.localIp);
private MdnsDiscoveryListener createDiscoveryListener() { }
return new MdnsDiscoveryListener() { else {
@Override polledDetails = tryPollIp(details.remoteIp);
public void notifyComputerAdded(MdnsComputer computer) { }
// Kick off a serverinfo poll on this machine
addComputerBlocking(computer.getAddress()); // The fallback poll worked
} if (polledDetails != null) {
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL :
@Override ComputerDetails.Reachability.REMOTE;
public void notifyComputerRemoved(MdnsComputer computer) { }
// Nothing to do here }
} else if (polledDetails != null) {
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
@Override ComputerDetails.Reachability.REMOTE;
public void notifyDiscoveryFailure(Exception e) { }
LimeLog.severe("mDNS discovery failed");
e.printStackTrace(); // Machine was unreachable both tries
} if (polledDetails == null) {
}; return false;
} }
private void addTuple(ComputerDetails details) { // If we got here, it's reachable
synchronized (pollingTuples) { details.update(polledDetails);
for (PollingTuple tuple : pollingTuples) { return true;
// Check if this is the same computer }
if (tuple.computer.uuid.equals(details.uuid)) {
// Update details anyway in case this machine has been re-added by IP private boolean doPollMachine(ComputerDetails details) {
// after not being reachable by our existing information return pollComputer(details, true);
tuple.computer.localIp = details.localIp; }
tuple.computer.remoteIp = details.remoteIp;
@Override
// Start a polling thread if polling is active public void onCreate() {
if (pollingActive && tuple.thread == null) { // Bind to the discovery service
tuple.thread = createPollingThread(details); bindService(new Intent(this, DiscoveryService.class),
tuple.thread.start(); discoveryServiceConnection, Service.BIND_AUTO_CREATE);
}
pollingThreads = new HashMap<ComputerDetails, Thread>();
// Found an entry so we're done
return; // Lookup or generate this device's UID
} idManager = new IdentityManager(this);
}
// Initialize the DB
// If we got here, we didn't find an entry dbManager = new ComputerDatabaseManager(this);
PollingTuple tuple = new PollingTuple(details, pollingActive ? createPollingThread(details) : null); dbRefCount.set(1);
pollingTuples.add(tuple); }
if (tuple.thread != null) {
tuple.thread.start(); @Override
} public void onDestroy() {
} if (discoveryBinder != null) {
} // Unbind from the discovery service
unbindService(discoveryServiceConnection);
public boolean addComputerBlocking(InetAddress addr) { }
// Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails(); // FIXME: Should await termination here but we have timeout issues in HttpURLConnection
fakeDetails.localIp = addr;
fakeDetails.remoteIp = addr; // Remove the initial DB reference
releaseLocalDatabaseReference();
// Block while we try to fill the details }
runPoll(fakeDetails, true);
@Override
// If the machine is reachable, it was successful public IBinder onBind(Intent intent) {
if (fakeDetails.state == ComputerDetails.State.ONLINE) { return binder;
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid); }
// Start a polling thread for this machine
addTuple(fakeDetails);
return true;
}
else {
return false;
}
}
public void removeComputer(String name) {
if (!getLocalDatabaseReference()) {
return;
}
// Remove it from the database
dbManager.deleteComputer(name);
synchronized (pollingTuples) {
// Remove the computer from the computer list
for (PollingTuple tuple : pollingTuples) {
if (tuple.computer.name.equals(name)) {
if (tuple.thread != null) {
// Interrupt the thread on this entry
tuple.thread.interrupt();
}
pollingTuples.remove(tuple);
break;
}
}
}
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
if (details.uuid != null && newDetails.uuid != null &&
!details.uuid.equals(newDetails.uuid)) {
// We got the wrong PC!
LimeLog.info("Polling returned the wrong PC!");
return null;
}
return newDetails;
} catch (Exception e) {
return null;
}
}
private boolean pollComputer(ComputerDetails details, boolean localFirst) {
ComputerDetails polledDetails;
// If the local address is routable across the Internet,
// always consider this PC remote to be conservative
if (details.localIp.equals(details.remoteIp)) {
localFirst = false;
}
if (localFirst) {
polledDetails = tryPollIp(details, details.localIp);
}
else {
polledDetails = tryPollIp(details, details.remoteIp);
}
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
// Failed, so let's try the fallback
if (!localFirst) {
polledDetails = tryPollIp(details, details.localIp);
}
else {
polledDetails = tryPollIp(details, details.remoteIp);
}
// The fallback poll worked
if (polledDetails != null) {
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL :
ComputerDetails.Reachability.REMOTE;
}
}
else if (polledDetails != null) {
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
ComputerDetails.Reachability.REMOTE;
}
// Machine was unreachable both tries
if (polledDetails == null) {
return false;
}
// If we got here, it's reachable
details.update(polledDetails);
return true;
}
private boolean doPollMachine(ComputerDetails details) {
if (details.reachability == ComputerDetails.Reachability.UNKNOWN ||
details.reachability == ComputerDetails.Reachability.OFFLINE) {
// Always try local first to avoid potential UDP issues when
// attempting to stream via the router's external IP address
// behind its NAT
return pollComputer(details, true);
}
else {
// If we're already reached a machine via a particular IP address,
// always try that one first
return pollComputer(details, details.reachability == ComputerDetails.Reachability.LOCAL);
}
}
@Override
public void onCreate() {
// Bind to the discovery service
bindService(new Intent(this, DiscoveryService.class),
discoveryServiceConnection, Service.BIND_AUTO_CREATE);
// Lookup or generate this device's UID
idManager = new IdentityManager(this);
// Initialize the DB
dbManager = new ComputerDatabaseManager(this);
dbRefCount.set(1);
// Grab known machines into our computer list
if (!getLocalDatabaseReference()) {
return;
}
for (ComputerDetails computer : dbManager.getAllComputers()) {
// Add tuples for each computer
addTuple(computer);
}
releaseLocalDatabaseReference();
}
@Override
public void onDestroy() {
if (discoveryBinder != null) {
// Unbind from the discovery service
unbindService(discoveryServiceConnection);
}
// FIXME: Should await termination here but we have timeout issues in HttpURLConnection
// Remove the initial DB reference
releaseLocalDatabaseReference();
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
public class ApplistPoller {
private Thread thread;
private final ComputerDetails computer;
private final Object pollEvent = new Object();
public ApplistPoller(ComputerDetails computer) {
this.computer = computer;
}
public void pollNow() {
synchronized (pollEvent) {
pollEvent.notify();
}
}
private boolean waitPollingDelay() {
try {
synchronized (pollEvent) {
pollEvent.wait(POLLING_PERIOD_MS);
}
} catch (InterruptedException e) {
return false;
}
return thread != null && !thread.isInterrupted();
}
public void start() {
thread = new Thread() {
@Override
public void run() {
do {
InetAddress selectedAddr;
// Can't poll if it's not online
if (computer.state != ComputerDetails.State.ONLINE) {
if (listener != null) {
listener.notifyComputerUpdated(computer);
}
continue;
}
// Can't poll if there's no UUID yet
if (computer.uuid == null) {
continue;
}
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
selectedAddr = computer.localIp;
}
else {
selectedAddr = computer.remoteIp;
}
NvHTTP http = new NvHTTP(selectedAddr, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
try {
// Query the app list from the server
String appList = http.getAppListRaw();
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
// Open the cache file
FileOutputStream cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
CacheHelper.writeStringToOutputStream(cacheOut, appList);
cacheOut.close();
// Update the computer
computer.rawAppList = appList;
// Notify that the app list has been updated
// and ensure that the thread is still active
if (listener != null && thread != null) {
listener.notifyComputerUpdated(computer);
}
}
else {
LimeLog.warning("Empty app list received from "+computer.uuid);
}
} catch (IOException e) {
e.printStackTrace();
} catch (XmlPullParserException e) {
e.printStackTrace();
}
} while (waitPollingDelay());
}
};
thread.start();
}
public void stop() {
if (thread != null) {
thread.interrupt();
// Don't join here because we might be blocked on network I/O
thread = null;
}
}
}
}
class PollingTuple {
public Thread thread;
public final ComputerDetails computer;
public PollingTuple(ComputerDetails computer, Thread thread) {
this.computer = computer;
this.thread = thread;
}
} }
@@ -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;
} }
} }
@@ -16,75 +16,75 @@ import android.os.IBinder;
public class DiscoveryService extends Service { public class DiscoveryService extends Service {
private MdnsDiscoveryAgent discoveryAgent; private MdnsDiscoveryAgent discoveryAgent;
private MdnsDiscoveryListener boundListener; private MdnsDiscoveryListener boundListener;
private MulticastLock multicastLock; private MulticastLock multicastLock;
public class DiscoveryBinder extends Binder { public class DiscoveryBinder extends Binder {
public void setListener(MdnsDiscoveryListener listener) { public void setListener(MdnsDiscoveryListener listener) {
boundListener = listener; boundListener = listener;
} }
public void startDiscovery(int queryIntervalMs) { public void startDiscovery(int queryIntervalMs) {
multicastLock.acquire(); multicastLock.acquire();
discoveryAgent.startDiscovery(queryIntervalMs); discoveryAgent.startDiscovery(queryIntervalMs);
} }
public void stopDiscovery() { public void stopDiscovery() {
discoveryAgent.stopDiscovery(); discoveryAgent.stopDiscovery();
multicastLock.release(); multicastLock.release();
} }
public List<MdnsComputer> getComputerSet() { public List<MdnsComputer> getComputerSet() {
return discoveryAgent.getComputerSet(); return discoveryAgent.getComputerSet();
} }
} }
@Override @Override
public void onCreate() { public void onCreate() {
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE); WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS"); multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
multicastLock.setReferenceCounted(false); multicastLock.setReferenceCounted(false);
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() { discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
@Override @Override
public void notifyComputerAdded(MdnsComputer computer) { public void notifyComputerAdded(MdnsComputer computer) {
if (boundListener != null) { if (boundListener != null) {
boundListener.notifyComputerAdded(computer); boundListener.notifyComputerAdded(computer);
} }
} }
@Override @Override
public void notifyComputerRemoved(MdnsComputer computer) { public void notifyComputerRemoved(MdnsComputer computer) {
if (boundListener != null) { if (boundListener != null) {
boundListener.notifyComputerRemoved(computer); boundListener.notifyComputerRemoved(computer);
} }
} }
@Override @Override
public void notifyDiscoveryFailure(Exception e) { public void notifyDiscoveryFailure(Exception e) {
if (boundListener != null) { if (boundListener != null) {
boundListener.notifyDiscoveryFailure(e); boundListener.notifyDiscoveryFailure(e);
} }
} }
}); });
} }
private final DiscoveryBinder binder = new DiscoveryBinder(); private DiscoveryBinder binder = new DiscoveryBinder();
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
return binder; return binder;
} }
@Override @Override
public boolean onUnbind(Intent intent) { public boolean onUnbind(Intent intent) {
// Stop any discovery session // Stop any discovery session
discoveryAgent.stopDiscovery(); discoveryAgent.stopDiscovery();
multicastLock.release(); multicastLock.release();
// Unbind the listener // Unbind the listener
boundListener = null; boundListener = null;
return false; return false;
} }
} }
@@ -1,289 +0,0 @@
package com.limelight.grid;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.widget.ImageView;
import android.widget.TextView;
import com.koushikdutta.async.future.FutureCallback;
import com.koushikdutta.ion.Ion;
import com.limelight.AppView;
import com.limelight.LimeLog;
import com.limelight.R;
import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.LimelightCryptoProvider;
import com.limelight.utils.CacheHelper;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.Future;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
private final ComputerDetails computer;
private final String uniqueId;
private final LimelightCryptoProvider cryptoProvider;
private final SSLContext sslContext;
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 {
super(context, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
this.computer = computer;
this.uniqueId = uniqueId;
cryptoProvider = PlatformBinding.getCryptoProvider(context);
sslContext = SSLContext.getInstance("SSL");
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom());
}
private final TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}};
private final KeyManager[] ourKeyman = new KeyManager[] {
new X509KeyManager() {
public String chooseClientAlias(String[] keyTypes,
Principal[] issuers, Socket socket) {
return "Limelight-RSA";
}
public String chooseServerAlias(String keyType, Principal[] issuers,
Socket socket) {
return null;
}
public X509Certificate[] getCertificateChain(String alias) {
return new X509Certificate[] {cryptoProvider.getClientCertificate()};
}
public String[] getClientAliases(String keyType, Principal[] issuers) {
return null;
}
public PrivateKey getPrivateKey(String alias) {
return cryptoProvider.getClientPrivateKey();
}
public String[] getServerAliases(String keyType, Principal[] issuers) {
return null;
}
}
};
// Ignore differences between given hostname and certificate hostname
HostnameVerifier hv = new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) { return true; }
};
private void sortList() {
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
@Override
public int compare(AppView.AppObject lhs, AppView.AppObject rhs) {
return lhs.app.getAppName().compareTo(rhs.app.getAppName());
}
});
}
private InetAddress getCurrentAddress() {
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
return computer.localIp;
}
else {
return computer.remoteIp;
}
}
public void addApp(AppView.AppObject app) {
itemList.add(app);
sortList();
}
public void abortPendingRequests() {
HashMap<ImageView, Future> tempMap;
synchronized (pendingRequests) {
// Copy the pending requests under a lock
tempMap = new HashMap<ImageView, Future>(pendingRequests);
}
for (Future f : tempMap.values()) {
if (!f.isCancelled() && !f.isDone()) {
f.cancel(true);
}
}
synchronized (pendingRequests) {
// Remove cancelled requests
for (ImageView v : tempMap.keySet()) {
pendingRequests.remove(v);
}
}
}
// TODO: Handle pruning of bitmap cache
private void populateBitmapCache(UUID uuid, int appId, Bitmap bitmap) {
try {
// PNG ignores quality setting
FileOutputStream out = CacheHelper.openCacheFileForOutput(context.getCacheDir(), "boxart", uuid.toString(), appId+".png");
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) {
// Clear existing contents of the image view
imgView.setAlpha(0.0f);
// Check the on-disk cache
new ImageCacheRequest(imgView, obj.app.getAppId()).execute();
return true;
}
@Override
public boolean populateTextView(TextView txtView, AppView.AppObject obj) {
// Select the text view so it starts marquee mode
txtView.setSelected(true);
// Return false to use the app's toString method
return false;
}
@Override
public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) {
if (obj.app.getIsRunning()) {
// Show the play button overlay
overlayView.setImageResource(R.drawable.play);
return true;
}
// No overlay
return false;
}
private class ImageCacheRequest extends AsyncTask<Void, Void, Bitmap> {
private final ImageView view;
private final int appId;
public ImageCacheRequest(ImageView view, int appId) {
this.view = view;
this.appId = appId;
}
@Override
protected Bitmap doInBackground(Void... v) {
InputStream in = null;
try {
in = CacheHelper.openCacheFileForInput(context.getCacheDir(), "boxart", computer.uuid.toString(), appId + ".png");
return BitmapFactory.decodeStream(in);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ignored) {}
}
}
return null;
}
private void fadeInImage(ImageView view) {
view.animate().alpha(1.0f).setDuration(250).start();
}
@Override
protected void onPostExecute(Bitmap result) {
if (result != null) {
// Disk cache was read successfully
LimeLog.info("Image disk cache hit for (" + computer.uuid + ", " + appId + ")");
view.setImageBitmap(result);
fadeInImage(view);
}
else {
LimeLog.info("Image disk cache miss for ("+computer.uuid+", "+appId+")");
LimeLog.info("Requesting: "+"https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
appId + "&AssetType=2&AssetIdx=0");
// Load the placeholder image
view.setImageResource(defaultImageRes);
fadeInImage(view);
// Set SSL contexts correctly to allow us to authenticate
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts);
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext);
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setHostnameVerifier(hv);
// Kick off the deferred image load
synchronized (pendingRequests) {
Future<Bitmap> f = Ion.with(context)
.load("https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
appId + "&AssetType=2&AssetIdx=0")
.asBitmap()
.setCallback(new FutureCallback<Bitmap>() {
@Override
public void onCompleted(Exception e, final Bitmap result) {
synchronized (pendingRequests) {
pendingRequests.remove(view);
}
if (result != null) {
// Make the view visible now
view.setImageBitmap(result);
fadeInImage(view);
// Populate the disk cache if we got an image back.
// 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 {
// Leave the loading icon as is (probably should change this eventually...)
}
}
});
pendingRequests.put(view, f);
}
}
}
}
}
@@ -1,82 +0,0 @@
package com.limelight.grid;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.limelight.R;
import java.util.ArrayList;
public abstract class GenericGridAdapter<T> extends BaseAdapter {
protected final Context context;
protected final int defaultImageRes;
protected final int layoutId;
protected final ArrayList<T> itemList = new ArrayList<T>();
protected final LayoutInflater inflater;
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
this.context = context;
this.layoutId = layoutId;
this.defaultImageRes = defaultImageRes;
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
public void clear() {
itemList.clear();
}
@Override
public int getCount() {
return itemList.size();
}
@Override
public Object getItem(int i) {
return itemList.get(i);
}
@Override
public long getItemId(int i) {
return i;
}
public abstract boolean populateImageView(ImageView imgView, T obj);
public abstract boolean populateTextView(TextView txtView, T obj);
public abstract boolean populateOverlayView(ImageView overlayView, T obj);
@Override
public View getView(int i, View convertView, ViewGroup viewGroup) {
if (convertView == null) {
convertView = inflater.inflate(layoutId, viewGroup, false);
}
ImageView imgView = (ImageView) convertView.findViewById(R.id.grid_image);
ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay);
TextView txtView = (TextView) convertView.findViewById(R.id.grid_text);
if (imgView != null) {
if (!populateImageView(imgView, itemList.get(i))) {
imgView.setImageResource(defaultImageRes);
}
}
if (!populateTextView(txtView, itemList.get(i))) {
txtView.setText(itemList.get(i).toString());
}
if (overlayView != null) {
if (!populateOverlayView(overlayView, itemList.get(i))) {
overlayView.setVisibility(View.INVISIBLE);
}
else {
overlayView.setVisibility(View.VISIBLE);
}
}
return convertView;
}
}
@@ -1,75 +0,0 @@
package com.limelight.grid;
import android.content.Context;
import android.widget.ImageView;
import android.widget.TextView;
import com.limelight.PcView;
import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import java.util.Collections;
import java.util.Comparator;
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
public PcGridAdapter(Context context, boolean listMode, boolean small) {
super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item), R.drawable.computer);
}
public void addComputer(PcView.ComputerObject computer) {
itemList.add(computer);
sortList();
}
private void sortList() {
Collections.sort(itemList, new Comparator<PcView.ComputerObject>() {
@Override
public int compare(PcView.ComputerObject lhs, PcView.ComputerObject rhs) {
return lhs.details.name.compareTo(rhs.details.name);
}
});
}
public boolean removeComputer(PcView.ComputerObject computer) {
return itemList.remove(computer);
}
@Override
public boolean populateImageView(ImageView imgView, PcView.ComputerObject obj) {
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
imgView.setAlpha(1.0f);
}
else {
imgView.setAlpha(0.4f);
}
// Return false to use the default drawable
return false;
}
@Override
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
txtView.setAlpha(1.0f);
}
else {
txtView.setAlpha(0.4f);
}
// Return false to use the computer's toString method
return false;
}
@Override
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
// Still refreshing this PC so display the overlay
overlayView.setImageResource(R.drawable.image_loading);
return true;
}
// No overlay
return false;
}
}
@@ -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"); System.loadLibrary("nv_avc_dec");
} }
/** Disables the deblocking filter at the cost of image quality */ /** Disables the deblocking filter at the cost of image quality */
public static final int DISABLE_LOOP_FILTER = 0x1; public static final int DISABLE_LOOP_FILTER = 0x1;
/** Uses the low latency decode flag (disables multithreading) */ /** Uses the low latency decode flag (disables multithreading) */
public static final int LOW_LATENCY_DECODE = 0x2; public static final int LOW_LATENCY_DECODE = 0x2;
/** Threads process each slice, rather than each frame */ /** Threads process each slice, rather than each frame */
public static final int SLICE_THREADING = 0x4; public static final int SLICE_THREADING = 0x4;
/** Uses nonstandard speedup tricks */ /** Uses nonstandard speedup tricks */
public static final int FAST_DECODE = 0x8; public static final int FAST_DECODE = 0x8;
/** Uses bilinear filtering instead of bicubic */ /** Uses bilinear filtering instead of bicubic */
public static final int BILINEAR_FILTERING = 0x10; public static final int BILINEAR_FILTERING = 0x10;
/** Uses a faster bilinear filtering with lower image quality */ /** Uses a faster bilinear filtering with lower image quality */
public static final int FAST_BILINEAR_FILTERING = 0x20; public static final int FAST_BILINEAR_FILTERING = 0x20;
/** Disables color conversion (output is NV21) */ /** Disables color conversion (output is NV21) */
public static final int NO_COLOR_CONVERSION = 0x40; public static final int NO_COLOR_CONVERSION = 0x40;
public static native int init(int width, int height, int perflvl, int threadcount); public static native int init(int width, int height, int perflvl, int threadcount);
public static native void destroy(); public static native void destroy();
// Rendering API when NO_COLOR_CONVERSION == 0 // Rendering API when NO_COLOR_CONVERSION == 0
public static native boolean setRenderTarget(Object androidSurface); public static native boolean setRenderTarget(Object androidSurface);
public static native boolean getRgbFrameInt(int[] rgbFrame, int bufferSize); public static native boolean getRgbFrameInt(int[] rgbFrame, int bufferSize);
public static native boolean getRgbFrame(byte[] rgbFrame, int bufferSize); public static native boolean getRgbFrame(byte[] rgbFrame, int bufferSize);
public static native boolean redraw(); public static native boolean redraw();
// Rendering API when NO_COLOR_CONVERSION == 1 // Rendering API when NO_COLOR_CONVERSION == 1
public static native boolean getRawFrame(byte[] yuvFrame, int bufferSize); public static native boolean getRawFrame(byte[] yuvFrame, int bufferSize);
public static native int getInputPaddingSize(); public static native int getInputPaddingSize();
public static native int decode(byte[] indata, int inoff, int inlen); public static native int decode(byte[] indata, int inoff, int inlen);
} }
@@ -2,180 +2,149 @@ package com.limelight.preferences;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.Locale;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import com.limelight.computers.ComputerManagerService; import com.limelight.computers.ComputerManagerService;
import com.limelight.R; import com.limelight.R;
import com.limelight.utils.Dialog; import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog;
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.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.view.KeyEvent; import android.view.View;
import android.view.inputmethod.EditorInfo; import android.view.View.OnClickListener;
import android.view.inputmethod.InputMethodManager; import android.widget.Button;
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 Button addPcButton;
private ComputerManagerService.ComputerManagerBinder managerBinder; private TextView hostText;
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>(); private ComputerManagerService.ComputerManagerBinder managerBinder;
private Thread addThread; private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
private final ServiceConnection serviceConnection = new ServiceConnection() { private Thread addThread;
public void onServiceConnected(ComponentName className, final IBinder binder) { private ServiceConnection serviceConnection = new ServiceConnection() {
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder); public void onServiceConnected(ComponentName className, final IBinder binder) {
startAddThread(); managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
} 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;
try {
InetAddress addr = InetAddress.getByName(host);
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc), if (!managerBinder.addComputerBlocking(addr)){
getResources().getString(R.string.msg_add_pc), false); msg = "Unable to connect to the specified computer. Make sure the required ports are allowed through the firewall.";
}
else {
msg = "Successfully added computer";
finish = true;
}
} catch (UnknownHostException e) {
msg = "Unable to resolve PC address. Make sure you didn't make a typo in the address.";
}
try { final boolean toastFinish = finish;
InetAddress addr = InetAddress.getByName(host); final String toastMsg = msg;
AddComputerManually.this.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
if (!managerBinder.addComputerBlocking(addr)){ if (toastFinish && !isFinishing()) {
msg = getResources().getString(R.string.addpc_fail); // Close the activity
} AddComputerManually.this.finish();
else { }
msg = getResources().getString(R.string.addpc_success); }
finish = true; });
} }
} catch (UnknownHostException e) {
msg = getResources().getString(R.string.addpc_unknown_host);
}
dialog.dismiss(); private void startAddThread() {
addThread = new Thread() {
@Override
public void run() {
while (!isInterrupted()) {
String computer;
final boolean toastFinish = finish; try {
final String toastMsg = msg; computer = computersToAdd.take();
AddComputerManually.this.runOnUiThread(new Runnable() { } catch (InterruptedException e) {
@Override return;
public void run() { }
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
if (toastFinish && !isFinishing()) { doAddPc(computer);
// Close the activity }
AddComputerManually.this.finish(); }
} };
} addThread.setName("UI - AddComputerManually");
}); addThread.start();
} }
private void startAddThread() { private void joinAddThread() {
addThread = new Thread() { if (addThread != null) {
@Override addThread.interrupt();
public void run() {
while (!isInterrupted()) {
String computer;
try { try {
computer = computersToAdd.take(); addThread.join();
} catch (InterruptedException e) { } catch (InterruptedException ignored) {}
return;
}
doAddPc(computer); addThread = null;
} }
} }
};
addThread.setName("UI - AddComputerManually");
addThread.start();
}
private void joinAddThread() { @Override
if (addThread != null) { protected void onStop() {
addThread.interrupt(); super.onStop();
try { Dialog.closeDialogs();
addThread.join(); }
} catch (InterruptedException ignored) {}
addThread = null; @Override
} protected void onDestroy() {
} super.onDestroy();
@Override if (managerBinder != null) {
protected void onStop() { joinAddThread();
super.onStop(); unbindService(serviceConnection);
}
}
Dialog.closeDialogs(); @Override
SpinnerDialog.closeDialogs(this); protected void onCreate(Bundle savedInstanceState) {
} super.onCreate(savedInstanceState);
@Override setContentView(R.layout.activity_add_computer_manually);
protected void onDestroy() {
super.onDestroy();
if (managerBinder != null) { this.addPcButton = (Button) findViewById(R.id.addPc);
joinAddThread(); this.hostText = (TextView) findViewById(R.id.hostTextView);
unbindService(serviceConnection);
}
}
@Override // Bind to the ComputerManager service
protected void onCreate(Bundle savedInstanceState) { bindService(new Intent(AddComputerManually.this,
super.onCreate(savedInstanceState); ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
String locale = PreferenceConfiguration.readPreferences(this).language; addPcButton.setOnClickListener(new OnClickListener() {
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) { @Override
Configuration config = new Configuration(getResources().getConfiguration()); public void onClick(View v) {
config.locale = new Locale(locale); if (hostText.getText().length() == 0) {
getResources().updateConfiguration(config, getResources().getDisplayMetrics()); Toast.makeText(AddComputerManually.this, "You must enter an IP address", Toast.LENGTH_LONG).show();
} return;
}
setContentView(R.layout.activity_add_computer_manually); Toast.makeText(AddComputerManually.this, "Adding PC...", Toast.LENGTH_SHORT).show();
computersToAdd.add(hostText.getText().toString());
UiHelper.notifyNewRootView(this); }
});
this.hostText = (TextView) findViewById(R.id.hostTextView); }
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
if (actionId == EditorInfo.IME_ACTION_DONE ||
(keyEvent != null &&
keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
if (hostText.getText().length() == 0) {
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
return true;
}
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;
}
});
// Bind to the ComputerManager service
bindService(new Intent(AddComputerManually.this,
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
}
} }
@@ -2,7 +2,6 @@ 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 {
@@ -13,16 +12,11 @@ public class PreferenceConfiguration {
private static final String SOPS_PREF_STRING = "checkbox_enable_sops"; private static final String SOPS_PREF_STRING = "checkbox_enable_sops";
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings"; private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio"; private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio";
private static final String DEADZONE_PREF_STRING = "seekbar_deadzone";
private static final String LANGUAGE_PREF_STRING = "list_languages";
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 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;
private static final int BITRATE_DEFAULT_1080_30 = 10; private static final int BITRATE_DEFAULT_1080_30 = 10;
private static final int BITRATE_DEFAULT_1080_60 = 20; private static final int BITRATE_DEFAULT_1080_60 = 30;
private static final String DEFAULT_RES_FPS = "720p60"; private static final String DEFAULT_RES_FPS = "720p60";
private static final String DEFAULT_DECODER = "auto"; private static final String DEFAULT_DECODER = "auto";
@@ -31,10 +25,6 @@ public class PreferenceConfiguration {
private static final boolean DEFAULT_SOPS = true; private static final boolean DEFAULT_SOPS = true;
private static final boolean DEFAULT_DISABLE_TOASTS = false; private static final boolean DEFAULT_DISABLE_TOASTS = false;
private static final boolean DEFAULT_HOST_AUDIO = false; private static final boolean DEFAULT_HOST_AUDIO = false;
private static final int DEFAULT_DEADZONE = 15;
public static final String DEFAULT_LANGUAGE = "default";
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,10 +33,7 @@ public class PreferenceConfiguration {
public int width, height, fps; public int width, height, fps;
public int bitrate; public int bitrate;
public int decoder; public int decoder;
public int deadzonePercentage;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings; public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language;
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")) {
@@ -67,17 +54,6 @@ public class PreferenceConfiguration {
} }
} }
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
return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
}
public static int getDefaultBitrate(Context context) { public static int getDefaultBitrate(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
@@ -154,18 +130,11 @@ public class PreferenceConfiguration {
config.decoder = getDecoderValue(context); config.decoder = getDecoderValue(context);
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
config.language = prefs.getString(LANGUAGE_PREF_STRING, DEFAULT_LANGUAGE);
// Checkbox preferences // Checkbox preferences
config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS); config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS);
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS); config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH); config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
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.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
return config; return config;
} }
@@ -19,15 +19,11 @@ public class SeekBarPreference extends DialogPreference
private static final String SCHEMA_URL = "http://schemas.android.com/apk/res/android"; private static final String SCHEMA_URL = "http://schemas.android.com/apk/res/android";
private SeekBar seekBar; private SeekBar seekBar;
private TextView valueText; private TextView splashText, valueText;
private final Context context; private Context context;
private final String dialogMessage; private String dialogMessage, suffix;
private final String suffix; private int defaultValue, maxValue, currentValue;
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);
@@ -51,10 +47,9 @@ public class SeekBarPreference extends DialogPreference
suffix = context.getString(suffixId); suffix = context.getString(suffixId);
} }
// Get default, min, and max seekbar values // Get default and max seekbar values
defaultValue = attrs.getAttributeIntValue(SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context)); defaultValue = PreferenceConfiguration.getDefaultBitrate(context);
maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100); maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100);
minValue = 1;
} }
@Override @Override
@@ -65,7 +60,7 @@ public class SeekBarPreference extends DialogPreference
layout.setOrientation(LinearLayout.VERTICAL); layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(6, 6, 6, 6); layout.setPadding(6, 6, 6, 6);
TextView splashText = new TextView(context); splashText = new TextView(context);
splashText.setPadding(30, 10, 30, 10); splashText.setPadding(30, 10, 30, 10);
if (dialogMessage != null) { if (dialogMessage != null) {
splashText.setText(dialogMessage); splashText.setText(dialogMessage);
@@ -76,7 +71,7 @@ public class SeekBarPreference extends DialogPreference
valueText.setGravity(Gravity.CENTER_HORIZONTAL); valueText.setGravity(Gravity.CENTER_HORIZONTAL);
valueText.setTextSize(32); valueText.setTextSize(32);
params = new LinearLayout.LayoutParams( params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.FILL_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT); LinearLayout.LayoutParams.WRAP_CONTENT);
layout.addView(valueText, params); layout.addView(valueText, params);
@@ -84,13 +79,8 @@ public class SeekBarPreference extends DialogPreference
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override @Override
public void onProgressChanged(SeekBar seekBar, int value, boolean b) { public void onProgressChanged(SeekBar seekBar, int value, boolean b) {
if (value < minValue) {
seekBar.setProgress(minValue);
return;
}
String t = String.valueOf(value); String t = String.valueOf(value);
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix)); valueText.setText(suffix == null ? t : t.concat(" " + suffix));
} }
@Override @Override
@@ -100,7 +90,7 @@ public class SeekBarPreference extends DialogPreference
public void onStopTrackingTouch(SeekBar seekBar) {} public void onStopTrackingTouch(SeekBar seekBar) {}
}); });
layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
if (shouldPersist()) { if (shouldPersist()) {
currentValue = getPersistedInt(defaultValue); currentValue = getPersistedInt(defaultValue);
@@ -131,6 +121,13 @@ 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) {
@@ -152,10 +149,10 @@ public class SeekBarPreference extends DialogPreference
if (shouldPersist()) { if (shouldPersist()) {
currentValue = seekBar.getProgress(); currentValue = seekBar.getProgress();
persistInt(seekBar.getProgress()); persistInt(seekBar.getProgress());
callChangeListener(seekBar.getProgress()); callChangeListener(Integer.valueOf(seekBar.getProgress()));
} }
getDialog().dismiss(); ((AlertDialog) getDialog()).dismiss();
} }
}); });
} }
@@ -1,21 +0,0 @@
package com.limelight.preferences;
import android.content.Context;
import android.content.res.TypedArray;
import android.preference.CheckBoxPreference;
import android.util.AttributeSet;
public class SmallIconCheckboxPreference extends CheckBoxPreference {
public SmallIconCheckboxPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SmallIconCheckboxPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
return PreferenceConfiguration.getDefaultSmallMode(getContext());
}
}
@@ -1,48 +1,23 @@
package com.limelight.preferences; package com.limelight.preferences;
import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.app.Activity; import android.app.Activity;
import android.preference.Preference; import android.preference.Preference;
import android.preference.PreferenceFragment; import android.preference.PreferenceFragment;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import com.limelight.PcView;
import com.limelight.R; import com.limelight.R;
import com.limelight.utils.UiHelper;
import java.util.Locale;
public class StreamSettings extends Activity { public class StreamSettings extends Activity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
setContentView(R.layout.activity_stream_settings); setContentView(R.layout.activity_stream_settings);
getFragmentManager().beginTransaction().replace( getFragmentManager().beginTransaction().replace(
R.id.stream_settings, new SettingsFragment() R.id.stream_settings, new SettingsFragment()
).commit(); ).commit();
UiHelper.notifyNewRootView(this);
}
@Override
public void onBackPressed() {
finish();
// Restart the PC view to apply UI changes
Intent intent = new Intent(this, PcView.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent, null);
} }
public static class SettingsFragment extends PreferenceFragment { public static class SettingsFragment extends PreferenceFragment {
@@ -1,35 +0,0 @@
package com.limelight.ui;
import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import com.limelight.R;
public class AdapterFragment extends Fragment {
private AdapterFragmentCallbacks callbacks;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
callbacks = (AdapterFragmentCallbacks) activity;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(callbacks.getAdapterFragmentLayoutId(), container, false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
callbacks.receiveAbsListView((AbsListView) getView().findViewById(R.id.fragmentView));
}
}
@@ -1,8 +0,0 @@
package com.limelight.ui;
import android.widget.AbsListView;
public interface AdapterFragmentCallbacks {
public int getAdapterFragmentLayoutId();
public void receiveAbsListView(AbsListView gridView);
}
@@ -1,5 +0,0 @@
package com.limelight.ui;
public interface GameGestures {
public void showKeyboard();
}
@@ -1,55 +0,0 @@
package com.limelight.utils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
public class CacheHelper {
private static File openPath(boolean createPath, File root, String... path) {
File f = root;
for (int i = 0; i < path.length; i++) {
String component = path[i];
if (i == path.length - 1) {
// This is the file component so now we create parent directories
if (createPath) {
f.mkdirs();
}
}
f = new File(f, component);
}
return f;
}
public static FileInputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
return new FileInputStream(openPath(false, root, path));
}
public static FileOutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
return new FileOutputStream(openPath(true, root, path));
}
public static String readInputStreamToString(InputStream in) throws IOException {
Reader r = new InputStreamReader(in);
StringBuilder sb = new StringBuilder();
char[] buf = new char[256];
int bytesRead;
while ((bytesRead = r.read(buf)) != -1) {
sb.append(buf, 0, bytesRead);
}
return sb.toString();
}
public static void writeStringToOutputStream(OutputStream out, String str) throws IOException {
out.write(str.getBytes("UTF-8"));
}
}
@@ -7,71 +7,70 @@ import android.app.AlertDialog;
import android.content.DialogInterface; import android.content.DialogInterface;
public class Dialog implements Runnable { public class Dialog implements Runnable {
private final String title; private String title, message;
private final String message; private Activity activity;
private final Activity activity; private boolean endAfterDismiss;
private final boolean endAfterDismiss;
private AlertDialog alert; private AlertDialog alert;
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>(); private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss) public Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
{ {
this.activity = activity; this.activity = activity;
this.title = title; this.title = title;
this.message = message; this.message = message;
this.endAfterDismiss = endAfterDismiss; this.endAfterDismiss = endAfterDismiss;
} }
public static void closeDialogs() public static void closeDialogs()
{ {
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
for (Dialog d : rundownDialogs) { for (Dialog d : rundownDialogs) {
if (d.alert.isShowing()) { if (d.alert.isShowing()) {
d.alert.dismiss(); d.alert.dismiss();
} }
} }
rundownDialogs.clear(); rundownDialogs.clear();
} }
} }
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss) public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
{ {
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss)); activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
} }
@Override @Override
public void run() { public void run() {
// If we're dying, don't bother creating a dialog // If we're dying, don't bother creating a dialog
if (activity.isFinishing()) if (activity.isFinishing())
return; return;
alert = new AlertDialog.Builder(activity).create(); alert = new AlertDialog.Builder(activity).create();
alert.setTitle(title); alert.setTitle(title);
alert.setMessage(message); alert.setMessage(message);
alert.setCancelable(false); alert.setCancelable(false);
alert.setCanceledOnTouchOutside(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,112 +9,111 @@ 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 final String title; private String title, message;
private final String message; private Activity activity;
private final Activity activity; private ProgressDialog progress;
private ProgressDialog progress; private boolean finish;
private final boolean finish;
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>(); private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
private SpinnerDialog(Activity activity, String title, String message, boolean finish) public SpinnerDialog(Activity activity, String title, String message, boolean finish)
{ {
this.activity = activity; this.activity = activity;
this.title = title; this.title = title;
this.message = message; this.message = message;
this.progress = null; this.progress = null;
this.finish = finish; this.finish = finish;
} }
public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish) public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish)
{ {
SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish); SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish);
activity.runOnUiThread(spinner); activity.runOnUiThread(spinner);
return spinner; return spinner;
} }
public static void closeDialogs(Activity activity) public static void closeDialogs(Activity activity)
{ {
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
Iterator<SpinnerDialog> i = rundownDialogs.iterator(); Iterator<SpinnerDialog> i = rundownDialogs.iterator();
while (i.hasNext()) { while (i.hasNext()) {
SpinnerDialog dialog = i.next(); SpinnerDialog dialog = i.next();
if (dialog.activity == activity) { if (dialog.activity == activity) {
i.remove(); i.remove();
if (dialog.progress.isShowing()) { if (dialog.progress.isShowing()) {
dialog.progress.dismiss(); dialog.progress.dismiss();
} }
} }
} }
} }
} }
public void dismiss() public void dismiss()
{ {
// Running again with progress != null will destroy it // Running again with progress != null will destroy it
activity.runOnUiThread(this); activity.runOnUiThread(this);
} }
public void setMessage(final String message) public void setMessage(final String message)
{ {
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
progress.setMessage(message); progress.setMessage(message);
} }
}); });
} }
@Override @Override
public void run() { public void run() {
// If we're dying, don't bother doing anything // If we're dying, don't bother doing anything
if (activity.isFinishing()) { if (activity.isFinishing()) {
return; return;
} }
if (progress == null) if (progress == null)
{ {
progress = new ProgressDialog(activity); progress = new ProgressDialog(activity);
progress.setTitle(title); progress.setTitle(title);
progress.setMessage(message); progress.setMessage(message);
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER); progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progress.setOnCancelListener(this); progress.setOnCancelListener(this);
// If we want to finish the activity when this is killed, make it cancellable // If we want to finish the activity when this is killed, make it cancellable
if (finish) if (finish)
{ {
progress.setCancelable(true); progress.setCancelable(true);
progress.setCanceledOnTouchOutside(false); progress.setCanceledOnTouchOutside(false);
} }
else else
{ {
progress.setCancelable(false); progress.setCancelable(false);
} }
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
rundownDialogs.add(this); rundownDialogs.add(this);
progress.show(); progress.show();
} }
} }
else else
{ {
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
if (rundownDialogs.remove(this) && progress.isShowing()) { if (rundownDialogs.remove(this) && progress.isShowing()) {
progress.dismiss(); progress.dismiss();
} }
} }
} }
} }
@Override @Override
public void onCancel(DialogInterface dialog) { public void onCancel(DialogInterface dialog) {
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
rundownDialogs.remove(this); rundownDialogs.remove(this);
} }
// This will only be called if finish was true, so we don't need to check again // This will only be called if finish was true, so we don't need to check again
activity.finish(); activity.finish();
} }
} }
@@ -1,31 +0,0 @@
package com.limelight.utils;
import android.app.Activity;
import android.app.UiModeManager;
import android.content.Context;
import android.content.res.Configuration;
import android.view.View;
public class UiHelper {
// Values from https://developer.android.com/training/tv/start/layouts.html
private static final int TV_VERTICAL_PADDING_DP = 27;
private static final int TV_HORIZONTAL_PADDING_DP = 48;
public static void notifyNewRootView(Activity activity)
{
View rootView = activity.findViewById(android.R.id.content);
UiModeManager modeMgr = (UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
if (modeMgr.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION)
{
// Increase view padding on TVs
float scale = activity.getResources().getDisplayMetrics().density;
int verticalPaddingPixels = (int) (TV_VERTICAL_PADDING_DP*scale + 0.5f);
int horizontalPaddingPixels = (int) (TV_HORIZONTAL_PADDING_DP*scale + 0.5f);
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
horizontalPaddingPixels, verticalPaddingPixels);
}
}
}
+12 -12
View File
@@ -69,6 +69,9 @@ int nv_avc_init(int width, int height, int perf_lvl, int thread_count) {
return -1; return -1;
} }
// Show frames even before a reference frame
decoder_ctx->flags2 |= CODEC_FLAG2_SHOW_ALL;
if (perf_lvl & DISABLE_LOOP_FILTER) { if (perf_lvl & DISABLE_LOOP_FILTER) {
// Skip the loop filter for performance reasons // Skip the loop filter for performance reasons
decoder_ctx->skip_loop_filter = AVDISCARD_ALL; decoder_ctx->skip_loop_filter = AVDISCARD_ALL;
@@ -367,20 +370,17 @@ int nv_avc_decode(unsigned char* indata, int inlen) {
// Only copy the picture at the end of decoding the packet // Only copy the picture at the end of decoding the packet
if (got_pic) { if (got_pic) {
// Clone the current decode frame outside of the mutex
AVFrame* new_frame = av_frame_clone(dec_frame);
AVFrame* old_frame;
// Swap it in under lock
pthread_mutex_lock(&mutex); pthread_mutex_lock(&mutex);
old_frame = yuv_frame;
yuv_frame = new_frame;
pthread_mutex_unlock(&mutex);
// Free the old frame outside of the mutex // Only clone this frame if the last frame was taken.
if (old_frame != NULL) { // This saves on extra copies for frames that don't get
av_frame_free(&old_frame); // rendered.
} if (yuv_frame == NULL) {
// Clone a new frame
yuv_frame = av_frame_clone(dec_frame);
}
pthread_mutex_unlock(&mutex);
} }
return err < 0 ? err : 0; return err < 0 ? err : 0;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

@@ -2,6 +2,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" > android:shape="rectangle" >
<stroke android:width="1dip" android:color="#ffffff"/> <stroke android:width="1dip" android:color="#ffffff"/>
</shape> </shape>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

@@ -8,67 +8,48 @@
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".PcView" > tools:context=".PcView" >
<RelativeLayout <ListView
android:id="@+id/no_pc_found_layout" android:id="@+id/pcListView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/settingsButton"
android:background="@drawable/list_view_unselected"
android:fastScrollEnabled="true"
android:longClickable="false"
android:stackFromBottom="false" >
</ListView>
<TextView
android:id="@+id/discoveryText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_centerVertical="true" android:layout_centerHorizontal="true"
android:layout_centerHorizontal="true"> android:layout_alignBaseline="@+id/settingsButton"
<ImageView android:text="@string/title_pc_view" />
android:id="@+id/pcs_loading"
android:layout_width="75dp"
android:layout_height="75dp"
android:src="@drawable/image_loading"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/pcs_loading"
android:layout_toEndOf="@+id/pcs_loading"
android:layout_marginLeft="5dp"
android:layout_marginStart="5dp"
android:layout_centerVertical="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:gravity="center"
android:text="@string/searching_pc"/>
</RelativeLayout>
<FrameLayout <Button
android:id="@+id/pcFragmentContainer"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_alignParentBottom="true"
android:layout_alignParentTop="true"
android:layout_toLeftOf="@+id/manuallyAddPc"
android:layout_toStartOf="@+id/manuallyAddPc"
android:layout_toRightOf="@+id/settingsButton"
android:layout_toEndOf="@+id/settingsButton"/>
<ImageButton
android:id="@+id/settingsButton" android:id="@+id/settingsButton"
android:layout_width="70dp" android:layout_width="wrap_content"
android:layout_height="65dp" android:layout_height="wrap_content"
android:cropToPadding="false" android:layout_alignLeft="@+id/pcListView"
android:scaleType="fitXY"
android:nextFocusDown="@+id/pcGridView"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:src="@drawable/settings" android:layout_marginTop="10dp"
style="?android:attr/borderlessButtonStyle"/> android:layout_marginBottom="15dp"
android:text="@string/button_stream_settings" />
<ImageButton <Button
android:id="@+id/manuallyAddPc" android:id="@+id/manuallyAddPc"
android:layout_width="70dp" android:layout_width="wrap_content"
android:layout_height="65dp" android:layout_height="wrap_content"
android:cropToPadding="false" android:layout_alignRight="@+id/pcListView"
android:scaleType="fitXY"
android:nextFocusDown="@+id/pcGridView"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:layout_alignParentRight="true" android:layout_marginTop="10dp"
android:layout_alignParentEnd="true" android:layout_marginBottom="15dp"
android:src="@drawable/add_computer" android:text="@string/button_add_pc_manually" />
style="?android:attr/borderlessButtonStyle"/>
</RelativeLayout> </RelativeLayout>
@@ -8,65 +8,46 @@
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".PcView" > tools:context=".PcView" >
<RelativeLayout <ListView
android:id="@+id/no_pc_found_layout" android:id="@+id/pcListView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/discoveryText"
android:background="@drawable/list_view_unselected"
android:fastScrollEnabled="true"
android:longClickable="false"
android:stackFromBottom="false" >
</ListView>
<TextView
android:id="@+id/discoveryText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerHorizontal="true"
android:layout_centerVertical="true" android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_centerHorizontal="true"> android:layout_below="@+id/manuallyAddPc"
<ImageView android:paddingTop="10dp"
android:id="@+id/pcs_loading" android:paddingBottom="10dp"
android:layout_width="75dp" android:text="@string/title_pc_view" />
android:layout_height="75dp"
android:src="@drawable/image_loading"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/pcs_loading"
android:layout_toEndOf="@+id/pcs_loading"
android:layout_marginLeft="5dp"
android:layout_marginStart="5dp"
android:layout_centerVertical="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:gravity="center"
android:text="@string/searching_pc"/>
</RelativeLayout>
<FrameLayout <Button
android:id="@+id/pcFragmentContainer"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_toLeftOf="@+id/manuallyAddPc"
android:layout_toStartOf="@+id/manuallyAddPc"
android:layout_toRightOf="@+id/settingsButton"
android:layout_toEndOf="@+id/settingsButton"
android:layout_alignParentBottom="true"
android:layout_alignParentTop="true"/>
<ImageButton
android:id="@+id/settingsButton" android:id="@+id/settingsButton"
android:layout_width="70dp" android:layout_width="wrap_content"
android:layout_height="65dp" android:layout_height="wrap_content"
android:cropToPadding="false" android:layout_centerHorizontal="true"
android:scaleType="fitXY"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:src="@drawable/settings" android:text="@string/button_stream_settings" />
style="?android:attr/borderlessButtonStyle"/>
<ImageButton <Button
android:id="@+id/manuallyAddPc" android:id="@+id/manuallyAddPc"
android:layout_width="70dp" android:layout_width="wrap_content"
android:layout_height="65dp" android:layout_height="wrap_content"
android:cropToPadding="false" android:layout_below="@+id/settingsButton"
android:scaleType="fitXY" android:layout_centerHorizontal="true"
android:layout_alignParentTop="true" android:text="@string/button_add_pc_manually" />
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:src="@drawable/add_computer"
style="?android:attr/borderlessButtonStyle"/>
</RelativeLayout> </RelativeLayout>
@@ -5,28 +5,16 @@
android:paddingBottom="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="10dp"
tools:context=".AddComputerManually" > tools:context=".Connection" >
<TextView
android:id="@+id/manuallyAddPcText"
android:text="@string/title_add_pc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_alignParentTop="true"/>
<EditText <EditText
android:id="@+id/hostTextView" android:id="@+id/hostTextView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/manuallyAddPcText"
android:layout_marginTop="25dp"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" android:layout_alignParentTop="true"
android:ems="10" android:ems="10"
android:singleLine="true" android:singleLine="true"
android:inputType="textNoSuggestions" android:inputType="textNoSuggestions"
@@ -35,4 +23,12 @@
<requestFocus /> <requestFocus />
</EditText> </EditText>
<Button
android:id="@+id/addPc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/hostTextView"
android:layout_centerHorizontal="true"
android:text="@string/button_add_pc" />
</RelativeLayout> </RelativeLayout>
+11 -10
View File
@@ -8,27 +8,28 @@
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".AppView" > tools:context=".AppView" >
<FrameLayout <ListView
android:id="@+id/appFragmentContainer" android:id="@+id/pcListView"
android:layout_width="fill_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:layout_below="@+id/appListText"/> android:layout_below="@+id/appListText"
android:fastScrollEnabled="true"
android:longClickable="false"
android:background="@drawable/list_view_unselected"
android:stackFromBottom="false">
</ListView>
<TextView <TextView
android:id="@+id/appListText" android:id="@+id/appListText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerHorizontal="true" android:layout_centerHorizontal="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:gravity="center"
android:paddingTop="0dp" android:paddingTop="0dp"
android:paddingBottom="10dp" android:paddingBottom="10dp" />
android:textSize="28sp"/>
</RelativeLayout> </RelativeLayout>
@@ -2,6 +2,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#000"
tools:context=".Game" > tools:context=".Game" >
<SurfaceView <SurfaceView
-41
View File
@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp">
<RelativeLayout
android:id="@+id/grid_image_layout"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/grid_image"
android:cropToPadding="false"
android:scaleType="fitXY"
android:layout_centerHorizontal="true"
android:layout_width="150dp"
android:layout_height="175dp">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="50dp"
android:layout_height="50dp">
</ImageView>
</RelativeLayout>
<TextView
android:id="@+id/grid_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
android:gravity="center"
android:singleLine="true"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:textSize="18sp" >
</TextView>
</RelativeLayout>
@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp">
<RelativeLayout
android:id="@+id/grid_image_layout"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/grid_image"
android:cropToPadding="false"
android:scaleType="fitXY"
android:layout_centerHorizontal="true"
android:layout_width="100dp"
android:layout_height="117dp">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="33dp"
android:layout_height="33dp">
</ImageView>
</RelativeLayout>
<TextView
android:id="@+id/grid_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
android:gravity="center"
android:singleLine="true"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:textSize="14sp" >
</TextView>
</RelativeLayout>
-15
View File
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
-19
View File
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/fragmentView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/list_view_unselected"
android:fastScrollEnabled="true"
android:longClickable="false"
android:focusable="true"
android:focusableInTouchMode="true"
android:stackFromBottom="false" >
</ListView>
</LinearLayout>
-37
View File
@@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp">
<RelativeLayout
android:id="@+id/grid_image_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/grid_image"
android:layout_centerHorizontal="true"
android:layout_width="150dp"
android:layout_height="100dp">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_marginTop="15dp"
android:layout_marginLeft="65dp"
android:layout_marginStart="65dp"
android:layout_marginRight="20dp"
android:layout_marginEnd="20dp"
android:layout_width="50dp"
android:layout_height="50dp">
</ImageView>
</RelativeLayout>
<TextView
android:id="@+id/grid_text"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
android:gravity="center"
android:textSize="18sp" >
</TextView>
</RelativeLayout>
@@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="15dp">
<RelativeLayout
android:id="@+id/grid_image_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/grid_image"
android:layout_centerHorizontal="true"
android:layout_width="100dp"
android:layout_height="67dp">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_marginTop="10dp"
android:layout_marginLeft="42dp"
android:layout_marginStart="42dp"
android:layout_marginRight="13dp"
android:layout_marginEnd="13dp"
android:layout_width="33dp"
android:layout_height="33dp">
</ImageView>
</RelativeLayout>
<TextView
android:id="@+id/grid_text"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
android:gravity="center"
android:textSize="14sp" >
</TextView>
</RelativeLayout>
-14
View File
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
@@ -5,7 +5,7 @@
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<TextView <TextView
android:id="@+id/grid_text" android:id="@+id/rowTextView"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textIsSelectable="false" android:textIsSelectable="false"
-15
View File
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="resolution_names">
<item>720p - 30 FPS</item>
<item>720p - 60 FPS</item>
<item>1080p - 30 FPS</item>
<item>1080p - 60 FPS</item>
</string-array>
<string-array name="decoder_names">
<item>Scegli decoder automaticamente</item>
<item>Forza decoder software</item>
<item>Forza decoder hardware</item>
</string-array>
</resources>
-120
View File
@@ -1,120 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- PC view menu entries -->
<string name="pcview_menu_app_list">Lista applicazioni</string>
<string name="pcview_menu_pair_pc">Accoppia PC</string>
<string name="pcview_menu_unpair_pc">Disaccoppia PC</string>
<string name="pcview_menu_send_wol">Invia richiesta Wake-On-LAN</string>
<string name="pcview_menu_delete_pc">Rimuovi PC</string>
<!-- Pair messages -->
<string name="pairing">Accoppiamento…</string>
<string name="pair_pc_offline">PC offline</string>
<string name="pair_pc_ingame">PC con applicazione avviata. Devi chiudere l\'applicazione prima dell\'accoppiamento.</string>
<string name="pair_pairing_title">Accoppiamento</string>
<string name="pair_pairing_msg">Inserisci il seguente PIN sul PC:</string>
<string name="pair_incorrect_pin">PIN non corretto</string>
<string name="pair_fail">Accoppiamento fallito</string>
<!-- WOL messages -->
<string name="wol_pc_online">PC già avviato</string>
<string name="wol_no_mac">Impossibile risvegliare il PC perchè GFE non ha inviato nessun indirizzo MAC</string>
<string name="wol_waking_pc">Risveglio PC…</string>
<string name="wol_waking_msg">Il PC potrebbe impiegare qualche secondo per risvegliarsi.
Se non succede niente, assicurati che l\'opzione Wake-On-LAN sia configurata correttamente.
</string>
<string name="wol_fail">Invio pacchetto Wake-On-LAN fallito</string>
<!-- Unpair messages -->
<string name="unpairing">Disaccoppiamento…</string>
<string name="unpair_success">Disaccoppiato con successo</string>
<string name="unpair_fail">Disaccoppiamento fallito</string>
<string name="unpair_error">PC non accoppiato</string>
<!-- Errors -->
<string name="error_pc_offline">PC offline</string>
<string name="error_manager_not_running">Il servizio ComputerManager non è avviato. Attendi qualche secondo o riavvia l\'applicazione.</string>
<string name="error_unknown_host">Risoluzione nome host fallita</string>
<string name="error_404">GFE ha ritornato un errore HTTP 404 error. Assicurati che il PC stia usando una GPU supportata.
Usare un software di remote-desktop può causare questo errore. Prova a riavviare il PC o a reinstallare GFE.
</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Connessione</string>
<string name="conn_establishing_msg">Connessione in corso</string>
<string name="conn_metered">Attenzione: la rete attiva prevede costi aggiuntivi in base all\'utilizzo!</string>
<string name="conn_client_latency">Latenza frame media client-side:</string>
<string name="conn_client_latency_hw">latenza decoder hardware:</string>
<string name="conn_hardware_latency">Latenza decoder hardware media:</string>
<string name="conn_starting">Avvio in corso…</string>
<string name="conn_error_title">Errore connessione</string>
<string name="conn_error_msg">Avvio fallito</string>
<string name="conn_terminated_title">Connessione terminata</string>
<string name="conn_terminated_msg">La connessione è stata interrotta</string>
<!-- General strings -->
<string name="ip_hint">Indirizzo IP del PC</string>
<string name="searching_pc">Ricerca PC in corso…</string>
<string name="yes">Sì</string>
<string name="no">No</string>
<string name="lost_connection">Connessione con il PC persa</string>
<!-- AppList activity -->
<string name="title_applist">Applicazioni su</string>
<string name="applist_menu_resume">Riprendi Sessione</string>
<string name="applist_menu_quit">Chiudi Sessione</string>
<string name="applist_menu_quit_and_start">Chiudi sessione corrente e avvia</string>
<string name="applist_menu_cancel">Annulla</string>
<string name="applist_refresh_title">Lista applicazioni</string>
<string name="applist_refresh_msg">Aggiornamento lista in corso…</string>
<string name="applist_refresh_error_title">Errore</string>
<string name="applist_refresh_error_msg">Ricezione lista applicazioni fallita</string>
<string name="applist_quit_app">Chiusura in corso…</string>
<string name="applist_quit_success">Sessione chiusa con successo</string>
<string name="applist_quit_fail">Chiusura sessione fallita</string>
<string name="applist_quit_confirmation">Sei sicuro di voler chiudere l\'applicazione avviata? Tutti i dati non salvati saranno persi.</string>
<!-- Add computer manually activity -->
<string name="title_add_pc">Aggiungi PC Manualmente</string>
<string name="msg_add_pc">Connessione al PC in corso…</string>
<string name="addpc_fail">Impossibile connettersi al PC. Assicurati che il firewall del PC sia configurato correttamente.</string>
<string name="addpc_success">PC aggiunto con successo</string>
<string name="addpc_unknown_host">Impossibile risovere l\'indirizzo del PC. Assicurati di aver scritto correttamente l\'indirizzo.</string>
<string name="addpc_enter_ip">Devi inserire un indirizzo IP</string>
<!-- Preferences -->
<string name="category_basic_settings">Impostazioni Base</string>
<string name="title_resolution_list">Risoluzione e FPS</string>
<string name="summary_resolution_list">Valori troppo elevati possono causare lag o crash</string>
<string name="title_seekbar_bitrate">Bitrate video</string>
<string name="summary_seekbar_bitrate">Abbassa il bitrate per ridurre lo stuttering; alza il bitrate per aumenteare la qualità dell\'immagine</string>
<string name="suffix_seekbar_bitrate">Mbps</string>
<string name="title_checkbox_stretch_video">Forza video in full-screen</string>
<string name="title_checkbox_disable_warnings">Disabilita messaggi di warning</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="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="suffix_seekbar_deadzone">%</string>
<string name="category_ui_settings">Impostazioni Interfaccia</string>
<string name="title_language_list">Lingua</string>
<string name="summary_language_list">Lingua da usare in Limelight</string>
<string name="title_checkbox_list_mode">Usa lista invece della griglia</string>
<string name="summary_checkbox_list_mode">Visualizza applicazioni e computers in una lista invece di una griglia</string>
<string name="title_checkbox_small_icon_mode">Usa icone piccole</string>
<string name="summary_checkbox_small_icon_mode">Usa icone piccole nella vista a griglia per avere più oggetti sullo schermo</string>
<string name="category_host_settings">Impostazioni Host</string>
<string name="title_checkbox_enable_sops">Ottimizza le impostazioni dei giochi</string>
<string name="summary_checkbox_enable_sops">Permetti a GFE di modificare le impostazioni dei giochi per uno streaming ottimale</string>
<string name="title_checkbox_host_audio">Riproduci audio sul PC</string>
<string name="summary_checkbox_host_audio">Riproduci l\'audio sul computer e su questo dispositivo</string>
<string name="category_advanced_settings">Impostazioni Avanzate</string>
<string name="title_decoder_list">Cambia decoder</string>
<string name="summary_decoder_list">Il decoder software può ridurre la latenza video quando si usano impostazioni streaming basse</string>
</resources>
@@ -0,0 +1,8 @@
<resources>
<!--
Customize dimensions originally defined in res/values/dimens.xml (such as
screen margins) for sw600dp devices (e.g. 7" tablets) here.
-->
</resources>
@@ -0,0 +1,9 @@
<resources>
<!--
Customize dimensions originally defined in res/values/dimens.xml (such as
screen margins) for sw720dp devices (e.g. 10" tablets) in landscape here.
-->
<dimen name="activity_horizontal_margin">128dp</dimen>
</resources>
+1 -1
View File
@@ -3,7 +3,7 @@
<!-- <!--
Base application theme for API 21+. This theme completely replaces Base application theme for API 21+. This theme completely replaces
AppBaseTheme from BOTH res/values/styles.xml and AppBaseTheme from BOTH res/values/styles.xml and
res/values-v21/styles.xml on API 21+ devices. res/values-v11/styles.xml on API 21+ devices.
--> -->
<style name="AppBaseTheme" parent="android:Theme.Material"> <style name="AppBaseTheme" parent="android:Theme.Material">
<!-- API 21 theme customizations can go here. --> <!-- API 21 theme customizations can go here. -->
+4 -15
View File
@@ -6,32 +6,21 @@
<item>1080p 30 FPS</item> <item>1080p 30 FPS</item>
<item>1080p 60 FPS</item> <item>1080p 60 FPS</item>
</string-array> </string-array>
<string-array name="resolution_values" translatable="false"> <string-array name="resolution_values">
<item>720p30</item> <item>720p30</item>
<item>720p60</item> <item>720p60</item>
<item>1080p30</item> <item>1080p30</item>
<item>1080p60</item> <item>1080p60</item>
</string-array> </string-array>
<string-array name="language_names" translatable="false">
<item>Default</item>
<item>English</item>
<item>Italiano</item>
</string-array>
<string-array name="language_values" translatable="false">
<item>default</item>
<item>en</item>
<item>it</item>
</string-array>
<string-array name="decoder_names"> <string-array name="decoder_names">
<item>Auto-select Decoder</item>
<item>Force Software Decoding</item> <item>Force Software Decoding</item>
<item>Auto-select Decoder</item>
<item>Force Hardware Decoding</item> <item>Force Hardware Decoding</item>
</string-array> </string-array>
<string-array name="decoder_values" translatable="false"> <string-array name="decoder_values">
<item>auto</item>
<item>software</item> <item>software</item>
<item>auto</item>
<item>hardware</item> <item>hardware</item>
</string-array> </string-array>
</resources> </resources>
+14
View File
@@ -0,0 +1,14 @@
<resources>
<!--
Declare custom theme attributes that allow changing which styles are
used for button bars depending on the API level.
?android:attr/buttonBarStyle is new as of API 11 so this is
necessary to support previous API levels.
-->
<declare-styleable name="ButtonBarContainerTheme">
<attr name="buttonBarStyle" format="reference" />
<attr name="buttonBarButtonStyle" format="reference" />
</declare-styleable>
</resources>
+6 -92
View File
@@ -1,87 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- PC view menu entries -->
<string name="pcview_menu_app_list">View Game List</string>
<string name="pcview_menu_pair_pc">Pair with PC</string>
<string name="pcview_menu_unpair_pc">Unpair</string>
<string name="pcview_menu_send_wol">Send Wake-On-LAN request</string>
<string name="pcview_menu_delete_pc">Delete PC</string>
<!-- Pair messages -->
<string name="pairing">Pairing…</string>
<string name="pair_pc_offline">Computer is offline</string>
<string name="pair_pc_ingame">Computer is currently in a game. You must close the game before pairing.</string>
<string name="pair_pairing_title">Pairing</string>
<string name="pair_pairing_msg">Please enter the following PIN on the target PC:</string>
<string name="pair_incorrect_pin">Incorrect PIN</string>
<string name="pair_fail">Pairing failed</string>
<!-- WOL messages -->
<string name="wol_pc_online">Computer is online</string>
<string name="wol_no_mac">Unable to wake PC because GFE didn\'t send a MAC address</string>
<string name="wol_waking_pc">Waking PC…</string>
<string name="wol_waking_msg">It may take a few seconds for your PC to wake up.
If it doesn\'t, make sure it\'s configured properly for Wake-On-LAN.
</string>
<string name="wol_fail">Failed to send Wake-On-LAN packets</string>
<!-- Unpair messages -->
<string name="unpairing">Unpairing…</string>
<string name="unpair_success">Unpaired successfully</string>
<string name="unpair_fail">Failed to unpair</string>
<string name="unpair_error">Device was not paired</string>
<!-- Errors -->
<string name="error_pc_offline">Computer is offline</string>
<string name="error_manager_not_running">The ComputerManager service is not running. Please wait a few seconds or restart the app.</string>
<string name="error_unknown_host">Failed to resolve host</string>
<string name="error_404">GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU.
Using remote desktop software can also cause this error. Try rebooting your machine or reinstalling GFE.
</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Establishing Connection</string>
<string name="conn_establishing_msg">Starting connection</string>
<string name="conn_metered">Warning: Your active network connection is metered!</string>
<string name="conn_client_latency">Average client-side frame latency:</string>
<string name="conn_client_latency_hw">hardware decoder latency:</string>
<string name="conn_hardware_latency">Average hardware decoder latency:</string>
<string name="conn_starting">Starting</string>
<string name="conn_error_title">Connection Error</string>
<string name="conn_error_msg">Failed to start</string>
<string name="conn_terminated_title">Connection Terminated</string>
<string name="conn_terminated_msg">The connection was terminated</string>
<!-- General strings --> <!-- General strings -->
<string name="ip_hint">IP address of GeForce PC</string> <string name="ip_hint">IP address of GeForce PC</string>
<string name="searching_pc">Searching for PCs…</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="lost_connection">Lost connection to PC</string>
<!-- AppList activity --> <!-- PC view activity -->
<string name="title_applist">Apps on</string> <string name="title_pc_view">PC List</string>
<string name="applist_menu_resume">Resume Session</string> <string name="button_stream_settings">Streaming Settings</string>
<string name="applist_menu_quit">Quit Session</string> <string name="button_add_pc_manually">Add PC Manually</string>
<string name="applist_menu_quit_and_start">Quit Current Game and Start</string>
<string name="applist_menu_cancel">Cancel</string>
<string name="applist_refresh_title">App List</string>
<string name="applist_refresh_msg">Refreshing apps…</string>
<string name="applist_refresh_error_title">Error</string>
<string name="applist_refresh_error_msg">Failed to get app list</string>
<string name="applist_quit_app">Quitting</string>
<string name="applist_quit_success">Successfully quit</string>
<string name="applist_quit_fail">Failed to quit</string>
<string name="applist_quit_confirmation">Are you sure you want to quit the running app? All unsaved data will be lost.</string>
<!-- Add computer manually activity --> <!-- Add computer manually activity -->
<string name="title_add_pc">Add PC Manually</string> <string name="button_add_pc">Manually Add PC</string>
<string name="msg_add_pc">Connecting to the PC…</string>
<string name="addpc_fail">Unable to connect to the specified computer. Make sure the required ports are allowed through the firewall.</string>
<string name="addpc_success">Successfully added computer</string>
<string name="addpc_unknown_host">Unable to resolve PC address. Make sure you didn\'t make a typo in the address.</string>
<string name="addpc_enter_ip">You must enter an IP address</string>
<!-- Preferences --> <!-- Preferences -->
<string name="category_basic_settings">Basic Settings</string> <string name="category_basic_settings">Basic Settings</string>
@@ -94,27 +23,12 @@
<string name="title_checkbox_disable_warnings">Disable warning messages</string> <string name="title_checkbox_disable_warnings">Disable warning messages</string>
<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="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="suffix_seekbar_deadzone">%</string>
<string name="category_ui_settings">UI Settings</string>
<string name="title_language_list">Language</string>
<string name="summary_language_list">Language to use for Limelight</string>
<string name="title_checkbox_list_mode">Use lists instead of grids</string>
<string name="summary_checkbox_list_mode">Display apps and PCs in lists instead of grids</string>
<string name="title_checkbox_small_icon_mode">Use small icons</string>
<string name="summary_checkbox_small_icon_mode">Use small icons in grid items to allow more items on screen</string>
<string name="category_host_settings">Host Settings</string> <string name="category_host_settings">Host Settings</string>
<string name="title_checkbox_enable_sops">Optimize game settings</string> <string name="title_checkbox_enable_sops">Optimize game settings</string>
<string name="summary_checkbox_enable_sops">Allow GFE to modify game settings for optimal streaming</string> <string name="summary_checkbox_enable_sops">Allow GFE to modify game settings for optimal streaming</string>
<string name="title_checkbox_host_audio">Play audio on PC</string> <string name="title_checkbox_host_audio">Play audio on PC</string>
<string name="summary_checkbox_host_audio">Play audio from the computer and this device</string> <string name="summary_checkbox_host_audio">Play audio from the computer and this device. Requires GFE 2.1.2+</string>
<string name="category_advanced_settings">Advanced Settings</string> <string name="category_advanced_settings">Advanced Settings</string>
<string name="title_decoder_list">Change decoder</string> <string name="title_decoder_list">Change decoder</string>
<string name="summary_decoder_list">Software decoding may improve video latency at lower streaming settings</string>
</resources> </resources>
+15 -10
View File
@@ -4,14 +4,12 @@
<!-- <!--
Base application theme, dependent on API level. This theme is replaced Base application theme, dependent on API level. This theme is replaced
by AppBaseTheme from res/values-vXX/styles.xml on newer devices.␍ by AppBaseTheme from res/values-vXX/styles.xml on newer devices.␍
--> -->
<style name="AppBaseTheme" parent="android:Theme"> <style name="AppBaseTheme" parent="android:Theme">
<!-- <!--
Theme customizations available in newer API levels can go in Theme customizations available in newer API levels can go in
res/values-vXX/styles.xml, while customizations related to res/values-vXX/styles.xml, while customizations related to
backward-compatibility can go here.␍ backward-compatibility can go here.␍
--> -->
</style> </style>
@@ -22,14 +20,21 @@
<item name="android:windowNoTitle">true</item> <item name="android:windowNoTitle">true</item>
</style> </style>
<!-- Stream activity theme --> <style name="FullscreenTheme" parent="android:Theme.NoTitleBar">
<style name="StreamTheme" parent="AppBaseTheme"> <item name="android:windowContentOverlay">@null</item>
<!-- All customizations that are NOT specific to a particular API-level can go here. --> <item name="android:windowBackground">@null</item>
<item name="android:windowActionBar">false</item> <item name="buttonBarStyle">@style/ButtonBar</item>
<item name="android:windowNoTitle">true</item> <item name="buttonBarButtonStyle">@style/ButtonBarButton</item>
<!-- Transparent streaming background to avoid extra overdraw -->
<item name="android:windowBackground">@android:color/transparent</item>
</style> </style>
<style name="ButtonBar">
<item name="android:paddingLeft">2dp</item>
<item name="android:paddingTop">5dp</item>
<item name="android:paddingRight">2dp</item>
<item name="android:paddingBottom">0dp</item>
<item name="android:background">@android:drawable/bottom_bar</item>
</style>
<style name="ButtonBarButton" />
</resources> </resources>
-32
View File
@@ -26,19 +26,6 @@
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">
<!--com.limelight.preferences.SeekBarPreference
android:key="seekbar_deadzone"
android:defaultValue="15"
android:max="50"
android:text="@string/suffix_seekbar_deadzone"
android:title="@string/title_seekbar_deadzone"/-->
<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"
@@ -51,31 +38,12 @@
android:summary="@string/summary_checkbox_host_audio" android:summary="@string/summary_checkbox_host_audio"
android:defaultValue="false" /> android:defaultValue="false" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/category_ui_settings">
<ListPreference
android:key="list_languages"
android:title="@string/title_language_list"
android:entries="@array/language_names"
android:entryValues="@array/language_values"
android:summary="@string/summary_language_list"
android:defaultValue="default" />
<com.limelight.preferences.SmallIconCheckboxPreference
android:key="checkbox_small_icon_mode"
android:title="@string/title_checkbox_small_icon_mode"
android:summary="@string/summary_checkbox_small_icon_mode" />
<CheckBoxPreference
android:key="checkbox_list_mode"
android:title="@string/title_checkbox_list_mode"
android:summary="@string/summary_checkbox_list_mode"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_advanced_settings"> <PreferenceCategory android:title="@string/category_advanced_settings">
<ListPreference <ListPreference
android:key="list_decoders" android:key="list_decoders"
android:title="@string/title_decoder_list" android:title="@string/title_decoder_list"
android:entries="@array/decoder_names" android:entries="@array/decoder_names"
android:entryValues="@array/decoder_values" android:entryValues="@array/decoder_values"
android:summary="@string/summary_decoder_list"
android:defaultValue="auto" /> android:defaultValue="auto" />
</PreferenceCategory> </PreferenceCategory>
+2 -1
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.limelight" >
<!-- Non-root application name --> <!-- Non-root application name -->
<application android:label="Limelight" /> <application android:label="Limelight" />
+2 -1
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.limelight.root" >
<!-- Root permissions --> <!-- Root permissions -->
<uses-permission android:name="android.permission.ACCESS_SUPERUSER" /> <uses-permission android:name="android.permission.ACCESS_SUPERUSER" />
+1 -1
View File
@@ -4,7 +4,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:1.0.1' classpath 'com.android.tools.build:gradle:0.13.2'
} }
} }
+2 -8
View File
@@ -4,12 +4,12 @@ This file serves to document some of the decoder errata when using MediaCodec ha
- Affected decoders: TI OMAP4, Allwinner A20 - Affected decoders: TI OMAP4, Allwinner A20
2. Some decoders have a huge per-frame latency with the unmodified SPS sent from NVENC. Setting max_dec_frame_buffering fixes this latency issue. 2. Some decoders have a huge per-frame latency with the unmodified SPS sent from NVENC. Setting max_dec_frame_buffering fixes this latency issue.
- Affected decoders: NVIDIA Tegra 3 and 4, Broadcom VideoCore IV - Affected decoders: NVIDIA Tegra 3 and 4
3. Some decoders strictly require that you pass BUFFER_FLAG_CODEC_CONFIG and crash upon the IDR frame if you don't 3. Some decoders strictly require that you pass BUFFER_FLAG_CODEC_CONFIG and crash upon the IDR frame if you don't
- Affected decoders: TI OMAP4 - Affected decoders: TI OMAP4
4. Some decoders require num_ref_frames=1 and max_dec_frame_buffering=1 to avoid crashing on SPS on first I-frame 4. Some decoders require num_ref_frames=1 and max_dec_frame_buffering=1 to avoid crashing on SPS or first I-frame
- Affected decoders: Qualcomm in GS3 on 4.3+, Exynos 4 at 1080p only - Affected decoders: Qualcomm in GS3 on 4.3+, Exynos 4 at 1080p only
5. Some decoders will hang if max_dec_frame_buffering is not present 5. Some decoders will hang if max_dec_frame_buffering is not present
@@ -17,9 +17,3 @@ This file serves to document some of the decoder errata when using MediaCodec ha
6. Some decoders will hang if max_dec_frame_buffering IS present 6. Some decoders will hang if max_dec_frame_buffering IS present
- Affected decoders: Exynos 5 in Galaxy Note 10.1 (2014) - Affected decoders: Exynos 5 in Galaxy Note 10.1 (2014)
7. Some decoders will not enter low latency mode if adaptive playback is enabled
- Affected decoders: Intel decoder in Nexus Player
8. Some decoders will not enter low latency mode if the profile isn't baseline in the first SPS.
- Affected decoders: Intel decoder in Nexus Player
+2 -2
View File
@@ -1,6 +1,6 @@
#Sun Dec 07 22:52:07 PST 2014 #Wed Apr 10 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-all.zip