Compare commits

..
62 changed files with 1146 additions and 1750 deletions

No files matched your search

+10 -8
View File
@@ -1,14 +1,14 @@
#Moonlight
#Limelight
Moonlight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
Limelight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
Limelight will allow you to stream your full collection of games from your Windows PC to your Android device,
whether in your own home or over the internet.
[Moonlight-pc](https://github.com/moonlight-stream/moonlight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/moonlight-stream/moonlight-ios) and [Windows and Windows Phone](https://github.com/moonlight-stream/moonlight-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/moonlight-stream/moonlight-android/wiki) for more detailed information or a troubleshooting guide.
Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for more detailed information or a troubleshooting guide.
##Features
@@ -18,7 +18,7 @@ Check our [wiki](https://github.com/moonlight-stream/moonlight-android/wiki) for
##Installation
* Download and install Moonlight for Android from
* Download and install Limelight for Android from
[Google Play](https://play.google.com/store/apps/details?id=com.limelight)
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
@@ -33,7 +33,7 @@ Check our [wiki](https://github.com/moonlight-stream/moonlight-android/wiki) for
* Turn on GameStream in the GFE settings
* If you are connecting from outside the same network, turn on internet
streaming
* When on the same network as your PC, open Moonlight and tap on your PC in the list
* When on the same network as your PC, open Limelight and tap on your PC in the list
* Accept the pairing confirmation on your PC
* Tap your PC again to view the list of apps to stream
* Play games!
@@ -46,6 +46,8 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
2. Write code
3. Send Pull Requests
Check out our [website](http://limelight-stream.com) for project links and information.
##Authors
* [Cameron Gutman](https://github.com/cgutman)
@@ -53,5 +55,5 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
* [Aaron Neyer](https://github.com/Aaronneyer)
* [Andrew Hennessy](https://github.com/yetanothername)
Moonlight is the work of students at [Case Western](http://case.edu) and was
Limelight is the work of students at [Case Western](http://case.edu) and was
started as a project at [MHacks](http://mhacks.org).
+14 -12
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="moonlight-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="limelight-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
@@ -9,12 +9,11 @@
<facet type="android" name="Android">
<configuration>
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugAndroidTest" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" />
<option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" />
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugAndroidTestSources" />
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugTestSources" />
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
@@ -25,7 +24,6 @@
</component>
<component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/nonRoot/debug" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
@@ -41,12 +39,12 @@
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/androidTest/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/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/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/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
@@ -103,13 +101,17 @@
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content>
<orderEntry type="jdk" jdkName="Android API 22 Platform" jdkType="Android SDK" />
<orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<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="bcpkix-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="tinyrtsp" 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="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" />
+13 -9
View File
@@ -4,15 +4,15 @@ import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application'
android {
compileSdkVersion 22
compileSdkVersion 21
buildToolsVersion "21.1.2"
defaultConfig {
minSdkVersion 16
targetSdkVersion 22
targetSdkVersion 21
versionName "3.1.5"
versionCode = 60
versionName "3.1-beta1"
versionCode = 50
}
productFlavors {
@@ -62,13 +62,17 @@ android {
}
dependencies {
compile group: 'org.jcodec', name: 'jcodec', version: '0.1.9'
compile group: 'org.jcodec', name: 'jcodec', version: '+'
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: 'bcprov-jdk15on', version: '+'
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '+'
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.2.0'
compile group: 'com.squareup.okio', name:'okio', version:'1.2.0'
compile group: 'com.google.android', name: 'support-v4', version:'+'
compile group: 'com.koushikdutta.ion', name: 'ion', version:'+'
compile group: 'com.google.code.gson', name: 'gson', version:'+'
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'+'
compile group: 'com.squareup.okio', name:'okio', version:'+'
compile files('libs/jmdns-fixed.jar')
compile files('libs/limelight-common.jar')
Binary file not shown.
+129 -119
View File
@@ -1,6 +1,9 @@
package com.limelight;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
@@ -8,7 +11,10 @@ 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.crypto.AndroidCryptoProvider;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.AppGridAdapter;
@@ -21,7 +27,6 @@ import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.CacheHelper;
import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
@@ -43,6 +48,7 @@ import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
@@ -55,8 +61,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
private ComputerManagerService.ApplistPoller poller;
private SpinnerDialog blockingLoadSpinner;
private String lastRawApplist;
private int lastRunningAppId;
private boolean suspendGridUpdates;
private int consecutiveAppListFailures = 0;
private final static int CONSECUTIVE_FAILURE_LIMIT = 3;
private final static int START_OR_RESUME_ID = 1;
private final static int QUIT_ID = 2;
@@ -67,7 +74,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
public final static String UUID_EXTRA = "UUID";
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final ServiceConnection serviceConnection = new ServiceConnection() {
private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder);
@@ -114,6 +121,11 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
}
};
private InetAddress getAddress() {
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp : computer.remoteIp;
}
private void startComputerUpdates() {
if (managerBinder == null) {
return;
@@ -122,17 +134,15 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(ComputerDetails details) {
// Do nothing if updates are suspended
if (suspendGridUpdates) {
return;
}
// Don't care about other computers
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
if (details != computer) {
return;
}
if (details.state == ComputerDetails.State.OFFLINE) {
if (details.state != ComputerDetails.State.ONLINE) {
consecutiveAppListFailures++;
if (consecutiveAppListFailures >= CONSECUTIVE_FAILURE_LIMIT) {
// The PC is unreachable now
AppView.this.runOnUiThread(new Runnable() {
@Override
@@ -142,34 +152,27 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
finish();
}
});
}
return;
}
// App list is the same or empty
consecutiveAppListFailures = 0;
// App list is the same or empty; nothing to do
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
// Let's check if the running app ID changed
if (details.runningGameId != lastRunningAppId) {
// Update the currently running game using the app ID
lastRunningAppId = details.runningGameId;
updateUiWithServerinfo(details);
}
return;
}
lastRunningAppId = details.runningGameId;
lastRawApplist = details.rawAppList;
try {
lastRawApplist = details.rawAppList;
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
} catch (Exception ignored) {}
} catch (Exception e) {}
}
});
@@ -187,10 +190,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
if (managerBinder != null) {
managerBinder.stopPolling();
}
if (appGridAdapter != null) {
appGridAdapter.cancelQueuedOperations();
}
}
@Override
@@ -273,6 +272,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
int runningAppId = -1;
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject app = (AppObject) appGridAdapter.getItem(i);
if (app.app == null) {
continue;
}
if (app.app.getIsRunning()) {
runningAppId = app.app.getAppId();
break;
@@ -287,6 +290,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
if (selectedApp == null || selectedApp.app == null) {
return;
}
int runningAppId = getRunningAppId();
if (runningAppId != -1) {
if (runningAppId == selectedApp.app.getAppId()) {
@@ -304,6 +311,33 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
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
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
@@ -311,37 +345,25 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
switch (item.getItemId()) {
case START_WTIH_QUIT:
// Display a confirmation dialog first
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
displayQuitConfirmationDialog(new Runnable() {
@Override
public void run() {
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
doStart(app.app);
}
}, null);
return true;
case START_OR_RESUME_ID:
// Resume is the same as start for us
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
doStart(app.app);
return true;
case QUIT_ID:
// Display a confirmation dialog first
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
displayQuitConfirmationDialog(new Runnable() {
@Override
public void run() {
suspendGridUpdates = true;
ServerHelper.doQuit(AppView.this,
ServerHelper.getCurrentAddressFromComputer(computer),
app.app, managerBinder, new Runnable() {
@Override
public void run() {
// Trigger a poll immediately
suspendGridUpdates = false;
if (poller != null) {
poller.pollNow();
}
}
});
doQuit(app.app);
}
}, null);
return true;
@@ -354,61 +376,26 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
}
}
private void updateUiWithServerinfo(final ComputerDetails details) {
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
boolean updated = false;
// Look through our current app list to tag the running app
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
// There can only be one or zero apps running.
if (existingApp.app.getIsRunning() &&
existingApp.app.getAppId() == details.runningGameId) {
// This app was running and still is, so we're done now
return;
}
else if (existingApp.app.getAppId() == details.runningGameId) {
// This app wasn't running but now is
existingApp.app.setIsRunning(true);
updated = true;
}
else if (existingApp.app.getIsRunning()) {
// This app was running but now isn't
existingApp.app.setIsRunning(false);
updated = true;
}
else {
// This app wasn't running and still isn't
}
}
if (updated) {
appGridAdapter.notifyDataSetChanged();
}
}
});
}
private void updateUiWithAppList(final List<NvApp> appList) {
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
boolean updated = false;
// First handle app updates and additions
for (NvApp app : appList) {
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.setIsRunning(app.getIsRunning());
existingApp.app.setIsRunningBoolean(app.getIsRunning());
updated = true;
}
if (!existingApp.app.getAppName().equals(app.getAppName())) {
@@ -428,34 +415,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
}
}
// Next handle app removals
int i = 0;
while (i < appGridAdapter.getCount()) {
boolean foundExistingApp = false;
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
// Check if this app is in the latest list
for (NvApp app : appList) {
if (existingApp.app.getAppId() == app.getAppId()) {
foundExistingApp = true;
break;
}
}
// This app was removed in the latest app list
if (!foundExistingApp) {
appGridAdapter.removeApp(existingApp);
updated = true;
// Check this same index again because the item at i+1 is now at i after
// the removal
continue;
}
// Move on to the next item
i++;
}
if (updated) {
appGridAdapter.notifyDataSetChanged();
}
@@ -463,6 +422,58 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
});
}
private void doStart(NvApp app) {
Intent intent = new Intent(this, Game.class);
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) {
Toast.makeText(AppView.this, getResources().getString(R.string.applist_quit_app)+" "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
NvHTTP httpConn;
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;
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
}
});
}
}).start();
}
@Override
public int getAdapterFragmentLayoutId() {
return PreferenceConfiguration.readPreferences(this).listMode ?
@@ -478,26 +489,25 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
AppObject app = (AppObject) appGridAdapter.getItem(pos);
if (app == null || app.app == null) {
return;
}
// Only open the context menu if something is running, otherwise start it
if (getRunningAppId() != -1) {
openContextMenu(arg1);
} else {
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
doStart(app.app);
}
}
});
registerForContextMenu(listView);
listView.requestFocus();
}
public class AppObject {
public final NvApp app;
public NvApp app;
public AppObject(NvApp app) {
if (app == null) {
throw new IllegalArgumentException("app must not be null");
}
this.app = app;
}
+18 -23
View File
@@ -13,7 +13,6 @@ import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.NvConnectionListener;
import com.limelight.nvstream.StreamConfiguration;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.input.KeyboardPacket;
import com.limelight.nvstream.input.MouseButtonPacket;
import com.limelight.preferences.PreferenceConfiguration;
@@ -26,7 +25,6 @@ import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Point;
import android.hardware.input.InputManager;
import android.media.AudioManager;
import android.net.ConnectivityManager;
import android.net.wifi.WifiManager;
@@ -61,7 +59,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private int lastButtonState = 0;
// Only 2 touches are supported
private final TouchContext[] touchContextMap = new TouchContext[2];
private TouchContext[] touchContextMap = new TouchContext[2];
private long threeFingerDownTime = 0;
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
@@ -70,7 +68,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private KeyboardTranslator keybTranslator;
private PreferenceConfiguration prefConfig;
private final Point screenSize = new Point(0, 0);
private Point screenSize = new Point(0, 0);
private NvConnection conn;
private SpinnerDialog spinner;
@@ -90,8 +88,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private int drFlags = 0;
public static final String EXTRA_HOST = "Host";
public static final String EXTRA_APP_NAME = "AppName";
public static final String EXTRA_APP_ID = "AppId";
public static final String EXTRA_APP = "App";
public static final String EXTRA_UNIQUEID = "UniqueId";
public static final String EXTRA_STREAMING_REMOTE = "Remote";
@@ -173,23 +170,17 @@ public class Game extends Activity implements SurfaceHolder.Callback,
wifiLock.acquire();
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME);
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
String app = Game.this.getIntent().getStringExtra(EXTRA_APP);
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
if (appId == StreamConfiguration.INVALID_APP_ID) {
finish();
return;
}
decoderRenderer = new ConfigurableDecoderRenderer();
decoderRenderer.initializeWithFlags(drFlags);
StreamConfiguration config = new StreamConfiguration.Builder()
.setResolution(prefConfig.width, prefConfig.height)
.setRefreshRate(prefConfig.fps)
.setApp(new NvApp(appName, appId))
.setApp(app)
.setBitrate(prefConfig.bitrate * 1000)
.setEnableSops(prefConfig.enableSops)
.enableAdaptiveResolution((decoderRenderer.getCapabilities() &
@@ -202,10 +193,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Initialize the connection
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
keybTranslator = new KeyboardTranslator(conn);
controllerHandler = new ControllerHandler(conn, this, prefConfig.multiController, prefConfig.deadzonePercentage);
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
inputManager.registerInputDeviceListener(controllerHandler, null);
controllerHandler = new ControllerHandler(conn, this, prefConfig.deadzonePercentage);
SurfaceHolder sh = sv.getHolder();
if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) {
@@ -234,12 +222,22 @@ public class Game extends Activity implements SurfaceHolder.Callback,
{
// Get the visible width of the activity
double visibleWidth = getWindow().getDecorView().getWidth();
double visibleHeight = getWindow().getDecorView().getHeight();
ViewGroup.LayoutParams lp = sv.getLayoutParams();
double vidRatio = vidHeight / vidWidth;
double visibleRatio = visibleHeight / visibleWidth;
// Calculate the new size of the SurfaceView
if (vidRatio < visibleRatio) {
lp.width = (int) visibleWidth;
lp.height = (int) ((vidHeight / vidWidth) * visibleWidth);
}
else {
lp.width = (int) ((vidWidth / vidHeight) * visibleHeight);
lp.height = (int) visibleHeight;
}
// Apply the size change
sv.setLayoutParams(lp);
@@ -254,7 +252,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
@SuppressLint("InlinedApi")
private final Runnable hideSystemUi = new Runnable() {
private Runnable hideSystemUi = new Runnable() {
@Override
public void run() {
// Use immersive mode on 4.4+ or standard low profile on previous builds
@@ -290,9 +288,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs();
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
inputManager.unregisterInputDeviceListener(controllerHandler);
displayedFailureDialog = true;
stopConnection();
@@ -323,7 +318,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
wifiLock.release();
}
private final Runnable toggleGrab = new Runnable() {
private Runnable toggleGrab = new Runnable() {
@Override
public void run() {
+10 -54
View File
@@ -12,7 +12,6 @@ import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.PcGridAdapter;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.nvstream.http.PairingManager.PairState;
@@ -23,10 +22,10 @@ import com.limelight.preferences.StreamSettings;
import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.UiHelper;
import android.app.Activity;
import android.app.FragmentTransaction;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
@@ -54,7 +53,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
private PcGridAdapter pcGridAdapter;
private ComputerManagerService.ComputerManagerBinder managerBinder;
private boolean freezeUpdates, runningPolling;
private final ServiceConnection serviceConnection = new ServiceConnection() {
private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder);
@@ -95,8 +94,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
private final static int UNPAIR_ID = 3;
private final static int WOL_ID = 4;
private final static int DELETE_ID = 5;
private final static int RESUME_ID = 6;
private final static int QUIT_ID = 7;
private void initializeViews() {
setContentView(R.layout.activity_pc_view);
@@ -241,7 +238,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
if (computer == null || computer.details == null ||
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
startComputerUpdates();
return;
}
@@ -256,16 +254,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
}
else {
if (computer.details.runningGameId != 0) {
menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
}
menu.add(Menu.NONE, APP_LIST_ID, 3, getResources().getString(R.string.pcview_menu_app_list));
// FIXME: We used to be able to unpair here but it's been broken since GFE 2.1.x, so I've replaced
// it with delete which actually work
menu.add(Menu.NONE, DELETE_ID, 4, getResources().getString(R.string.pcview_menu_delete_pc));
menu.add(Menu.NONE, APP_LIST_ID, 1, getResources().getString(R.string.pcview_menu_app_list));
menu.add(Menu.NONE, UNPAIR_ID, 2, getResources().getString(R.string.pcview_menu_unpair_pc));
}
}
@@ -486,8 +476,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
final ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
switch (item.getItemId()) {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
switch (item.getItemId())
{
case PAIR_ID:
doPair(computer.details);
return true;
@@ -513,32 +504,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
doAppList(computer.details);
return true;
case RESUME_ID:
if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return true;
}
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId), computer.details, managerBinder);
return true;
case QUIT_ID:
if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return true;
}
// Display a confirmation dialog first
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
public void run() {
ServerHelper.doQuit(PcView.this,
ServerHelper.getCurrentAddressFromComputer(computer.details),
new NvApp("app", 0), managerBinder, null);
}
}, null);
return true;
default:
return super.onContextItemSelected(item);
}
@@ -551,12 +516,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
if (details.equals(computer.details)) {
pcGridAdapter.removeComputer(computer);
pcGridAdapter.notifyDataSetChanged();
if (pcGridAdapter.getCount() == 0) {
// Show the "Discovery in progress" view
noPcFoundLayout.setVisibility(View.VISIBLE);
}
break;
}
}
@@ -569,7 +528,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
// Check if this is the same computer
if (details.uuid.equals(computer.details.uuid)) {
if (details.equals(computer.details)) {
existingEntry = computer;
break;
}
@@ -626,9 +585,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
public ComputerDetails details;
public ComputerObject(ComputerDetails details) {
if (details == null) {
throw new IllegalArgumentException("details must not be null");
}
this.details = details;
}
@@ -9,7 +9,7 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
public class AndroidAudioRenderer implements AudioRenderer {
private static final int FRAME_SIZE = 960;
public static final int FRAME_SIZE = 960;
private AudioTrack track;
@@ -45,8 +45,8 @@ import com.limelight.nvstream.http.LimelightCryptoProvider;
public class AndroidCryptoProvider implements LimelightCryptoProvider {
private final File certFile;
private final File keyFile;
private File certFile;
private File keyFile;
private X509Certificate cert;
private RSAPrivateKey key;
@@ -1,9 +1,7 @@
package com.limelight.binding.input;
import java.util.HashMap;
import java.util.Map;
import android.hardware.input.InputManager;
import android.os.SystemClock;
import android.view.InputDevice;
import android.view.KeyEvent;
@@ -15,10 +13,26 @@ import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.ui.GameGestures;
import com.limelight.utils.Vector2d;
public class ControllerHandler implements InputManager.InputDeviceListener {
public class ControllerHandler {
private short inputMap = 0x0000;
private byte leftTrigger = 0x00;
private byte rightTrigger = 0x00;
private short rightStickX = 0x0000;
private short rightStickY = 0x0000;
private short leftStickX = 0x0000;
private short leftStickY = 0x0000;
private int emulatingButtonFlags = 0;
// Used for OUYA bumper state tracking since they force all buttons
// up when the OUYA button goes down. We watch the last time we get
// a bumper up and compare that to our maximum delay when we receive
// a Start button press to see if we should activate one of our
// emulated button combos.
private long lastLbUpTime = 0;
private long lastRbUpTime = 0;
private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
private long startDownTime = 0;
private static final int START_DOWN_TIME_KEYB_MS = 750;
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
@@ -29,23 +43,19 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
private static final int EMULATED_SPECIAL_UP_DELAY_MS = 100;
private static final int EMULATED_SELECT_UP_DELAY_MS = 30;
private final Vector2d inputVector = new Vector2d();
private Vector2d inputVector = new Vector2d();
private final HashMap<String, ControllerContext> contexts = new HashMap<String, ControllerContext>();
private HashMap<String, ControllerMapping> mappings = new HashMap<String, ControllerMapping>();
private final NvConnection conn;
private final double stickDeadzone;
private final ControllerContext defaultContext = new ControllerContext();
private final GameGestures gestures;
private NvConnection conn;
private double stickDeadzone;
private final ControllerMapping defaultMapping = new ControllerMapping();
private GameGestures gestures;
private boolean hasGameController;
private final boolean multiControllerEnabled;
private short currentControllers;
public ControllerHandler(NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
public ControllerHandler(NvConnection conn, GameGestures gestures, int deadzonePercentage) {
this.conn = conn;
this.gestures = gestures;
this.multiControllerEnabled = multiControllerEnabled;
// HACK: For now we're hardcoding a 10% deadzone. Some deadzone
// is required for controller batching support to work.
@@ -72,16 +82,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
this.stickDeadzone = (double)deadzonePercentage / 100.0;
// Initialize the default context for events with no device
defaultContext.leftStickXAxis = MotionEvent.AXIS_X;
defaultContext.leftStickYAxis = MotionEvent.AXIS_Y;
defaultContext.leftStickDeadzoneRadius = (float) stickDeadzone;
defaultContext.rightStickXAxis = MotionEvent.AXIS_Z;
defaultContext.rightStickYAxis = MotionEvent.AXIS_RZ;
defaultContext.rightStickDeadzoneRadius = (float) stickDeadzone;
defaultContext.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
defaultContext.controllerNumber = (short) 0;
// Initialize the default mapping for events with no device
defaultMapping.leftStickXAxis = MotionEvent.AXIS_X;
defaultMapping.leftStickYAxis = MotionEvent.AXIS_Y;
defaultMapping.leftStickDeadzoneRadius = (float) stickDeadzone;
defaultMapping.rightStickXAxis = MotionEvent.AXIS_Z;
defaultMapping.rightStickYAxis = MotionEvent.AXIS_RZ;
defaultMapping.rightStickDeadzoneRadius = (float) stickDeadzone;
defaultMapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
defaultMapping.rightTriggerAxis = MotionEvent.AXIS_GAS;
}
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
@@ -97,89 +106,19 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
return range;
}
@Override
public void onInputDeviceAdded(int deviceId) {
// Nothing happening here yet
}
@Override
public void onInputDeviceRemoved(int deviceId) {
for (Map.Entry<String, ControllerContext> device : contexts.entrySet()) {
if (device.getValue().id == deviceId) {
LimeLog.info("Removed controller: "+device.getValue().name);
releaseControllerNumber(device.getValue());
contexts.remove(device.getKey());
return;
}
}
}
@Override
public void onInputDeviceChanged(int deviceId) {
// Remove and re-add
onInputDeviceRemoved(deviceId);
onInputDeviceAdded(deviceId);
}
private void releaseControllerNumber(ControllerContext context) {
if (context.reservedControllerNumber) {
LimeLog.info("Controller number "+context.controllerNumber+" is now available");
currentControllers &= ~(1 << context.controllerNumber);
}
}
// Called before sending input but after we've determined that this
// is definitely a controller (not a keyboard, mouse, or something else)
private void assignControllerNumberIfNeeded(ControllerContext context) {
if (context.assignedControllerNumber) {
return;
}
LimeLog.info(context.name+" needs a controller number assigned");
if (context.name != null && context.name.contains("gpio-keys")) {
// This is the back button on Shield portable consoles
LimeLog.info("Built-in buttons hardcoded as controller 0");
context.controllerNumber = 0;
}
else if (multiControllerEnabled && context.hasJoystickAxes) {
context.controllerNumber = 0;
LimeLog.info("Reserving the next available controller number");
for (short i = 0; i < 4; i++) {
if ((currentControllers & (1 << i)) == 0) {
// Found an unused controller value
currentControllers |= (1 << i);
context.controllerNumber = i;
context.reservedControllerNumber = true;
break;
}
}
}
else {
LimeLog.info("Not reserving a controller number");
context.controllerNumber = 0;
}
LimeLog.info("Assigned as controller "+context.controllerNumber);
context.assignedControllerNumber = true;
}
private ControllerContext createContextForDevice(InputDevice dev) {
ControllerContext context = new ControllerContext();
private ControllerMapping createMappingForDevice(InputDevice dev) {
ControllerMapping mapping = new ControllerMapping();
String devName = dev.getName();
LimeLog.info("Creating controller context for device: "+devName);
LimeLog.info("Creating controller mapping for device: "+devName);
context.name = devName;
context.id = dev.getId();
context.leftStickXAxis = MotionEvent.AXIS_X;
context.leftStickYAxis = MotionEvent.AXIS_Y;
if (getMotionRangeForJoystickAxis(dev, context.leftStickXAxis) != null &&
getMotionRangeForJoystickAxis(dev, context.leftStickYAxis) != null) {
mapping.leftStickXAxis = MotionEvent.AXIS_X;
mapping.leftStickYAxis = MotionEvent.AXIS_Y;
if (getMotionRangeForJoystickAxis(dev, mapping.leftStickXAxis) != null &&
getMotionRangeForJoystickAxis(dev, mapping.leftStickYAxis) != null) {
// This is a gamepad
hasGameController = true;
context.hasJoystickAxes = true;
mapping.hasJoystickAxes = true;
}
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
@@ -189,14 +128,14 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
if (leftTriggerRange != null && rightTriggerRange != null)
{
// Some controllers use LTRIGGER and RTRIGGER (like Ouya)
context.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER;
context.rightTriggerAxis = MotionEvent.AXIS_RTRIGGER;
mapping.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER;
mapping.rightTriggerAxis = MotionEvent.AXIS_RTRIGGER;
}
else if (brakeRange != null && gasRange != null)
{
// Others use GAS and BRAKE (like Moga)
context.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
context.rightTriggerAxis = MotionEvent.AXIS_GAS;
mapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
mapping.rightTriggerAxis = MotionEvent.AXIS_GAS;
}
else
{
@@ -205,34 +144,34 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
if (rxRange != null && ryRange != null && devName != null) {
if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) {
// Xbox controllers use RX and RY for right stick
context.rightStickXAxis = MotionEvent.AXIS_RX;
context.rightStickYAxis = MotionEvent.AXIS_RY;
mapping.rightStickXAxis = MotionEvent.AXIS_RX;
mapping.rightStickYAxis = MotionEvent.AXIS_RY;
// Xbox controllers use Z and RZ for triggers
context.leftTriggerAxis = MotionEvent.AXIS_Z;
context.rightTriggerAxis = MotionEvent.AXIS_RZ;
context.triggersIdleNegative = true;
context.isXboxController = true;
mapping.leftTriggerAxis = MotionEvent.AXIS_Z;
mapping.rightTriggerAxis = MotionEvent.AXIS_RZ;
mapping.triggersIdleNegative = true;
mapping.isXboxController = true;
}
else {
// DS4 controller uses RX and RY for triggers
context.leftTriggerAxis = MotionEvent.AXIS_RX;
context.rightTriggerAxis = MotionEvent.AXIS_RY;
context.triggersIdleNegative = true;
mapping.leftTriggerAxis = MotionEvent.AXIS_RX;
mapping.rightTriggerAxis = MotionEvent.AXIS_RY;
mapping.triggersIdleNegative = true;
context.isDualShock4 = true;
mapping.isDualShock4 = true;
}
}
}
if (context.rightStickXAxis == -1 && context.rightStickYAxis == -1) {
if (mapping.rightStickXAxis == -1 && mapping.rightStickYAxis == -1) {
InputDevice.MotionRange zRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z);
InputDevice.MotionRange rzRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ);
// Most other controllers use Z and RZ for the right stick
if (zRange != null && rzRange != null) {
context.rightStickXAxis = MotionEvent.AXIS_Z;
context.rightStickYAxis = MotionEvent.AXIS_RZ;
mapping.rightStickXAxis = MotionEvent.AXIS_Z;
mapping.rightStickYAxis = MotionEvent.AXIS_RZ;
}
else {
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
@@ -240,8 +179,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
// Try RX and RY now
if (rxRange != null && ryRange != null) {
context.rightStickXAxis = MotionEvent.AXIS_RX;
context.rightStickYAxis = MotionEvent.AXIS_RY;
mapping.rightStickXAxis = MotionEvent.AXIS_RX;
mapping.rightStickYAxis = MotionEvent.AXIS_RY;
}
}
}
@@ -250,30 +189,30 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
InputDevice.MotionRange hatXRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_X);
InputDevice.MotionRange hatYRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_Y);
if (hatXRange != null && hatYRange != null) {
context.hatXAxis = MotionEvent.AXIS_HAT_X;
context.hatYAxis = MotionEvent.AXIS_HAT_Y;
mapping.hatXAxis = MotionEvent.AXIS_HAT_X;
mapping.hatYAxis = MotionEvent.AXIS_HAT_Y;
}
if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
context.leftStickDeadzoneRadius = (float) stickDeadzone;
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
mapping.leftStickDeadzoneRadius = (float) stickDeadzone;
}
if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
context.rightStickDeadzoneRadius = (float) stickDeadzone;
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
mapping.rightStickDeadzoneRadius = (float) stickDeadzone;
}
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, context.leftTriggerAxis);
InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, context.rightTriggerAxis);
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, mapping.leftTriggerAxis);
InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, mapping.rightTriggerAxis);
// It's important to have a valid deadzone so controller packet batching works properly
context.triggerDeadzone = Math.max(Math.abs(ltRange.getFlat()), Math.abs(rtRange.getFlat()));
mapping.triggerDeadzone = Math.max(Math.abs(ltRange.getFlat()), Math.abs(rtRange.getFlat()));
// For triggers without (valid) deadzones, we'll use 13% (around XInput's default)
if (context.triggerDeadzone < 0.13f ||
context.triggerDeadzone > 0.30f)
if (mapping.triggerDeadzone < 0.13f ||
mapping.triggerDeadzone > 0.30f)
{
context.triggerDeadzone = 0.13f;
mapping.triggerDeadzone = 0.13f;
}
}
@@ -287,75 +226,71 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
boolean[] hasStartKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_MENU, 0);
if (!hasStartKey[0] && !hasStartKey[1]) {
context.backIsStart = true;
context.modeIsSelect = true;
mapping.backIsStart = true;
}
}
// The ASUS Gamepad has triggers that sit far forward and are prone to false presses
// so we increase the deadzone on them to minimize this
context.triggerDeadzone = 0.30f;
mapping.triggerDeadzone = 0.30f;
}
// Classify this device as a remote by name
else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) {
// It's only a remote if it doesn't any sticks
if (!context.hasJoystickAxes) {
context.isRemote = true;
if (!mapping.hasJoystickAxes) {
mapping.isRemote = true;
}
}
// SHIELD controllers will use small stick deadzones
else if (devName.contains("SHIELD")) {
context.leftStickDeadzoneRadius = 0.07f;
context.rightStickDeadzoneRadius = 0.07f;
// NYKO Playpad has a fake hat that mimics the left stick for some reason
else if (devName.contains("NYKO PLAYPAD")) {
mapping.hatXAxis = -1;
mapping.hatYAxis = -1;
}
}
LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius);
LimeLog.info("Trigger deadzone: "+context.triggerDeadzone);
LimeLog.info("Analog stick deadzone: "+mapping.leftStickDeadzoneRadius+" "+mapping.rightStickDeadzoneRadius);
LimeLog.info("Trigger deadzone: "+mapping.triggerDeadzone);
return context;
return mapping;
}
private ControllerContext getContextForDevice(InputDevice dev) {
// Unknown devices use the default context
private ControllerMapping getMappingForDevice(InputDevice dev) {
// Unknown devices use the default mapping
if (dev == null) {
return defaultContext;
return defaultMapping;
}
String descriptor = dev.getDescriptor();
// Return the existing context if it exists
ControllerContext context = contexts.get(descriptor);
if (context != null) {
return context;
// Return the existing mapping if it exists
ControllerMapping mapping = mappings.get(descriptor);
if (mapping != null) {
return mapping;
}
// Otherwise create a new context
context = createContextForDevice(dev);
contexts.put(descriptor, context);
// Otherwise create a new mapping
mapping = createMappingForDevice(dev);
mappings.put(descriptor, mapping);
return context;
return mapping;
}
private void sendControllerInputPacket(ControllerContext context) {
assignControllerNumberIfNeeded(context);
conn.sendControllerInput(context.controllerNumber, context.inputMap,
context.leftTrigger, context.rightTrigger,
context.leftStickX, context.leftStickY,
context.rightStickX, context.rightStickY);
private void sendControllerInputPacket() {
conn.sendControllerInput(inputMap, leftTrigger, rightTrigger,
leftStickX, leftStickY, rightStickX, rightStickY);
}
// Return a valid keycode, 0 to consume, or -1 to not consume the event
// Device MAY BE NULL
private int handleRemapping(ControllerContext context, KeyEvent event) {
private int handleRemapping(ControllerMapping mapping, KeyEvent event) {
// For remotes, don't capture the back button
if (context.isRemote) {
if (mapping.isRemote) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
return -1;
}
}
if (context.isDualShock4) {
if (mapping.isDualShock4) {
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_BUTTON_Y:
return KeyEvent.KEYCODE_BUTTON_L1;
@@ -394,7 +329,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
}
}
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
switch (event.getKeyCode()) {
// These are duplicate dpad events for hat input
case KeyEvent.KEYCODE_DPAD_LEFT:
@@ -405,9 +340,9 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
return 0;
}
}
else if (context.hatXAxis == -1 &&
context.hatYAxis == -1 &&
context.isXboxController &&
else if (mapping.hatXAxis == -1 &&
mapping.hatYAxis == -1 &&
mapping.isXboxController &&
event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
// If there's not a proper Xbox controller mapping, we'll translate the raw d-pad
// scan codes into proper key codes
@@ -440,20 +375,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
if (keyCode == KeyEvent.KEYCODE_BUTTON_START ||
keyCode == KeyEvent.KEYCODE_MENU) {
// Ensure that we never use back as start if we have a real start
context.backIsStart = false;
mapping.backIsStart = false;
}
else if (keyCode == KeyEvent.KEYCODE_BUTTON_SELECT) {
// Don't use mode as select if we have a select
context.modeIsSelect = false;
}
else if (context.backIsStart && keyCode == KeyEvent.KEYCODE_BACK) {
else if (mapping.backIsStart && keyCode == KeyEvent.KEYCODE_BACK) {
// Emulate the start button with back
return KeyEvent.KEYCODE_BUTTON_START;
}
else if (context.modeIsSelect && keyCode == KeyEvent.KEYCODE_BUTTON_MODE) {
// Emulate the select button with mode
return KeyEvent.KEYCODE_BUTTON_SELECT;
}
return keyCode;
}
@@ -475,101 +402,101 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
// evaluates the deadzone.
}
private void handleAxisSet(ControllerContext context, float lsX, float lsY, float rsX,
private void handleAxisSet(ControllerMapping mapping, float lsX, float lsY, float rsX,
float rsY, float lt, float rt, float hatX, float hatY) {
if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
Vector2d leftStickVector = populateCachedVector(lsX, lsY);
handleDeadZone(leftStickVector, context.leftStickDeadzoneRadius);
handleDeadZone(leftStickVector, mapping.leftStickDeadzoneRadius);
context.leftStickX = (short) (leftStickVector.getX() * 0x7FFE);
context.leftStickY = (short) (-leftStickVector.getY() * 0x7FFE);
leftStickX = (short) (leftStickVector.getX() * 0x7FFE);
leftStickY = (short) (-leftStickVector.getY() * 0x7FFE);
}
if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
Vector2d rightStickVector = populateCachedVector(rsX, rsY);
handleDeadZone(rightStickVector, context.rightStickDeadzoneRadius);
handleDeadZone(rightStickVector, mapping.rightStickDeadzoneRadius);
context.rightStickX = (short) (rightStickVector.getX() * 0x7FFE);
context.rightStickY = (short) (-rightStickVector.getY() * 0x7FFE);
rightStickX = (short) (rightStickVector.getX() * 0x7FFE);
rightStickY = (short) (-rightStickVector.getY() * 0x7FFE);
}
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
if (context.triggersIdleNegative) {
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
if (mapping.triggersIdleNegative) {
lt = (lt + 1) / 2;
rt = (rt + 1) / 2;
}
if (lt <= context.triggerDeadzone) {
if (lt <= mapping.triggerDeadzone) {
lt = 0;
}
if (rt <= context.triggerDeadzone) {
if (rt <= mapping.triggerDeadzone) {
rt = 0;
}
context.leftTrigger = (byte)(lt * 0xFF);
context.rightTrigger = (byte)(rt * 0xFF);
leftTrigger = (byte)(lt * 0xFF);
rightTrigger = (byte)(rt * 0xFF);
}
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
context.inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
if (hatX < -0.5) {
context.inputMap |= ControllerPacket.LEFT_FLAG;
inputMap |= ControllerPacket.LEFT_FLAG;
}
else if (hatX > 0.5) {
context.inputMap |= ControllerPacket.RIGHT_FLAG;
inputMap |= ControllerPacket.RIGHT_FLAG;
}
context.inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
if (hatY < -0.5) {
context.inputMap |= ControllerPacket.UP_FLAG;
inputMap |= ControllerPacket.UP_FLAG;
}
else if (hatY > 0.5) {
context.inputMap |= ControllerPacket.DOWN_FLAG;
inputMap |= ControllerPacket.DOWN_FLAG;
}
}
sendControllerInputPacket(context);
sendControllerInputPacket();
}
public boolean handleMotionEvent(MotionEvent event) {
ControllerContext context = getContextForDevice(event.getDevice());
ControllerMapping mapping = getMappingForDevice(event.getDevice());
float lsX = 0, lsY = 0, rsX = 0, rsY = 0, rt = 0, lt = 0, hatX = 0, hatY = 0;
// We purposefully ignore the historical values in the motion event as it makes
// the controller feel sluggish for some users.
if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
lsX = event.getAxisValue(context.leftStickXAxis);
lsY = event.getAxisValue(context.leftStickYAxis);
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
lsX = event.getAxisValue(mapping.leftStickXAxis);
lsY = event.getAxisValue(mapping.leftStickYAxis);
}
if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
rsX = event.getAxisValue(context.rightStickXAxis);
rsY = event.getAxisValue(context.rightStickYAxis);
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
rsX = event.getAxisValue(mapping.rightStickXAxis);
rsY = event.getAxisValue(mapping.rightStickYAxis);
}
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
lt = event.getAxisValue(context.leftTriggerAxis);
rt = event.getAxisValue(context.rightTriggerAxis);
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
lt = event.getAxisValue(mapping.leftTriggerAxis);
rt = event.getAxisValue(mapping.rightTriggerAxis);
}
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
}
handleAxisSet(context, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY);
handleAxisSet(mapping, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY);
return true;
}
public boolean handleButtonUp(KeyEvent event) {
ControllerContext context = getContextForDevice(event.getDevice());
ControllerMapping mapping = getMappingForDevice(event.getDevice());
int keyCode = handleRemapping(context, event);
int keyCode = handleRemapping(mapping, event);
if (keyCode == 0) {
return true;
}
@@ -588,80 +515,78 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_MODE:
context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU:
if (SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) {
// FIXME: The stock keyboard doesn't have controller focus so isn't usable. I'm not enabling this shortcut
// until we have a custom keyboard or some other fix
//gestures.showKeyboard();
if (SystemClock.uptimeMillis() - startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) {
gestures.showKeyboard();
}
context.inputMap &= ~ControllerPacket.PLAY_FLAG;
inputMap &= ~ControllerPacket.PLAY_FLAG;
break;
case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_BUTTON_SELECT:
context.inputMap &= ~ControllerPacket.BACK_FLAG;
inputMap &= ~ControllerPacket.BACK_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
context.inputMap &= ~ControllerPacket.LEFT_FLAG;
inputMap &= ~ControllerPacket.LEFT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
context.inputMap &= ~ControllerPacket.RIGHT_FLAG;
inputMap &= ~ControllerPacket.RIGHT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_UP:
context.inputMap &= ~ControllerPacket.UP_FLAG;
inputMap &= ~ControllerPacket.UP_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
context.inputMap &= ~ControllerPacket.DOWN_FLAG;
inputMap &= ~ControllerPacket.DOWN_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_B:
context.inputMap &= ~ControllerPacket.B_FLAG;
inputMap &= ~ControllerPacket.B_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_BUTTON_A:
context.inputMap &= ~ControllerPacket.A_FLAG;
inputMap &= ~ControllerPacket.A_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_X:
context.inputMap &= ~ControllerPacket.X_FLAG;
inputMap &= ~ControllerPacket.X_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_Y:
context.inputMap &= ~ControllerPacket.Y_FLAG;
inputMap &= ~ControllerPacket.Y_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L1:
context.inputMap &= ~ControllerPacket.LB_FLAG;
context.lastLbUpTime = SystemClock.uptimeMillis();
inputMap &= ~ControllerPacket.LB_FLAG;
lastLbUpTime = SystemClock.uptimeMillis();
break;
case KeyEvent.KEYCODE_BUTTON_R1:
context.inputMap &= ~ControllerPacket.RB_FLAG;
context.lastRbUpTime = SystemClock.uptimeMillis();
inputMap &= ~ControllerPacket.RB_FLAG;
lastRbUpTime = SystemClock.uptimeMillis();
break;
case KeyEvent.KEYCODE_BUTTON_THUMBL:
context.inputMap &= ~ControllerPacket.LS_CLK_FLAG;
inputMap &= ~ControllerPacket.LS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBR:
context.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
inputMap &= ~ControllerPacket.RS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L2:
context.leftTrigger = 0;
leftTrigger = 0;
break;
case KeyEvent.KEYCODE_BUTTON_R2:
context.rightTrigger = 0;
rightTrigger = 0;
break;
default:
return false;
}
// Check if we're emulating the select button
if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
{
// If either start or LB is up, select comes up too
if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
(context.inputMap & ControllerPacket.LB_FLAG) == 0)
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
(inputMap & ControllerPacket.LB_FLAG) == 0)
{
context.inputMap &= ~ControllerPacket.BACK_FLAG;
inputMap &= ~ControllerPacket.BACK_FLAG;
context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT;
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT;
try {
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
@@ -670,16 +595,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
}
// Check if we're emulating the special button
if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0)
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0)
{
// If either start or select and RB is up, the special button comes up too
if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
((context.inputMap & ControllerPacket.BACK_FLAG) == 0 &&
(context.inputMap & ControllerPacket.RB_FLAG) == 0))
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
((inputMap & ControllerPacket.BACK_FLAG) == 0 &&
(inputMap & ControllerPacket.RB_FLAG) == 0))
{
context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL;
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL;
try {
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
@@ -687,115 +612,112 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
}
}
sendControllerInputPacket(context);
sendControllerInputPacket();
return true;
}
public boolean handleButtonDown(KeyEvent event) {
ControllerContext context = getContextForDevice(event.getDevice());
ControllerMapping mapping = getMappingForDevice(event.getDevice());
int keyCode = handleRemapping(context, event);
int keyCode = handleRemapping(mapping, event);
if (keyCode == 0) {
return true;
}
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_MODE:
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU:
if (event.getRepeatCount() == 0) {
context.startDownTime = SystemClock.uptimeMillis();
startDownTime = SystemClock.uptimeMillis();
}
context.inputMap |= ControllerPacket.PLAY_FLAG;
inputMap |= ControllerPacket.PLAY_FLAG;
break;
case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_BUTTON_SELECT:
context.inputMap |= ControllerPacket.BACK_FLAG;
inputMap |= ControllerPacket.BACK_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
context.inputMap |= ControllerPacket.LEFT_FLAG;
inputMap |= ControllerPacket.LEFT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
context.inputMap |= ControllerPacket.RIGHT_FLAG;
inputMap |= ControllerPacket.RIGHT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_UP:
context.inputMap |= ControllerPacket.UP_FLAG;
inputMap |= ControllerPacket.UP_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
context.inputMap |= ControllerPacket.DOWN_FLAG;
inputMap |= ControllerPacket.DOWN_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_B:
context.inputMap |= ControllerPacket.B_FLAG;
inputMap |= ControllerPacket.B_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_BUTTON_A:
context.inputMap |= ControllerPacket.A_FLAG;
inputMap |= ControllerPacket.A_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_X:
context.inputMap |= ControllerPacket.X_FLAG;
inputMap |= ControllerPacket.X_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_Y:
context.inputMap |= ControllerPacket.Y_FLAG;
inputMap |= ControllerPacket.Y_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L1:
context.inputMap |= ControllerPacket.LB_FLAG;
inputMap |= ControllerPacket.LB_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_R1:
context.inputMap |= ControllerPacket.RB_FLAG;
inputMap |= ControllerPacket.RB_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBL:
context.inputMap |= ControllerPacket.LS_CLK_FLAG;
inputMap |= ControllerPacket.LS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBR:
context.inputMap |= ControllerPacket.RS_CLK_FLAG;
inputMap |= ControllerPacket.RS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L2:
context.leftTrigger = (byte)0xFF;
leftTrigger = (byte)0xFF;
break;
case KeyEvent.KEYCODE_BUTTON_R2:
context.rightTrigger = (byte)0xFF;
rightTrigger = (byte)0xFF;
break;
default:
return false;
}
// Start+LB acts like select for controllers with one button
if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
((context.inputMap & ControllerPacket.LB_FLAG) != 0 ||
SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
if ((inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
((inputMap & ControllerPacket.LB_FLAG) != 0 ||
SystemClock.uptimeMillis() - lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
{
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
context.inputMap |= ControllerPacket.BACK_FLAG;
inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
inputMap |= ControllerPacket.BACK_FLAG;
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
}
// We detect select+start or start+RB as the special button combo
if (((context.inputMap & ControllerPacket.RB_FLAG) != 0 ||
(SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
(context.inputMap & ControllerPacket.BACK_FLAG) != 0) &&
(context.inputMap & ControllerPacket.PLAY_FLAG) != 0)
if (((inputMap & ControllerPacket.RB_FLAG) != 0 ||
(SystemClock.uptimeMillis() - lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
(inputMap & ControllerPacket.BACK_FLAG) != 0) &&
(inputMap & ControllerPacket.PLAY_FLAG) != 0)
{
context.inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
}
// Send a new input packet if this is the first instance of a button down event
// or anytime if we're emulating a button
if (event.getRepeatCount() == 0 || context.emulatingButtonFlags != 0) {
sendControllerInputPacket(context);
if (event.getRepeatCount() == 0 || emulatingButtonFlags != 0) {
sendControllerInputPacket();
}
return true;
}
class ControllerContext {
public String name;
public int id;
class ControllerMapping {
public int leftStickXAxis = -1;
public int leftStickYAxis = -1;
public float leftStickDeadzoneRadius;
@@ -815,31 +737,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
public boolean isDualShock4;
public boolean isXboxController;
public boolean backIsStart;
public boolean modeIsSelect;
public boolean isRemote;
public boolean hasJoystickAxes;
public boolean assignedControllerNumber;
public boolean reservedControllerNumber;
public short controllerNumber;
public short inputMap = 0x0000;
public byte leftTrigger = 0x00;
public byte rightTrigger = 0x00;
public short rightStickX = 0x0000;
public short rightStickY = 0x0000;
public short leftStickX = 0x0000;
public short leftStickY = 0x0000;
public int emulatingButtonFlags = 0;
// Used for OUYA bumper state tracking since they force all buttons
// up when the OUYA button goes down. We watch the last time we get
// a bumper up and compare that to our maximum delay when we receive
// a Start button press to see if we should activate one of our
// emulated button combos.
public long lastLbUpTime = 0;
public long lastRbUpTime = 0;
public long startDownTime = 0;
}
}
@@ -15,7 +15,7 @@ public class KeyboardTranslator extends KeycodeTranslator {
/**
* 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_9 = 57;
@@ -11,10 +11,9 @@ public class TouchContext {
private long originalTouchTime = 0;
private boolean cancelled;
private final NvConnection conn;
private final int actionIndex;
private final double xFactor;
private final double yFactor;
private NvConnection conn;
private int actionIndex;
private double xFactor, yFactor;
private static final int TAP_MOVEMENT_THRESHOLD = 10;
private static final int TAP_TIME_THRESHOLD = 250;
@@ -29,9 +29,9 @@ public class EvdevEvent {
/* Keys */
public static final short KEY_Q = 16;
public final short type;
public final short code;
public final int value;
public short type;
public short code;
public int value;
public EvdevEvent(short type, short code, int value) {
this.type = type;
@@ -7,12 +7,12 @@ import com.limelight.LimeLog;
public class EvdevHandler {
private final String absolutePath;
private final EvdevListener listener;
private String absolutePath;
private EvdevListener listener;
private boolean shutdown = false;
private int fd = -1;
private final Thread handlerThread = new Thread() {
private Thread handlerThread = new Thread() {
@Override
public void run() {
// All the finally blocks here make this code look like a mess
@@ -26,7 +26,7 @@ public class EvdevReader {
// 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 }\" " +
shell.runCommand("supolicy --live \"allow untrusted_app input_device dir { getattr read search }\" " +
"\"allow untrusted_app input_device chr_file { open read write ioctl }\"");
}
}
@@ -4,7 +4,7 @@ import android.view.KeyEvent;
public class EvdevTranslator {
private static final short[] EVDEV_KEY_CODES = {
public static final short EVDEV_KEY_CODES[] = {
0, //KeyEvent.VK_RESERVED
KeyEvent.KEYCODE_ESCAPE,
KeyEvent.KEYCODE_1,
@@ -36,7 +36,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
private int totalFrames;
private long totalTimeMs;
private final int cpuCount = Runtime.getRuntime().availableProcessors();
private int cpuCount = Runtime.getRuntime().availableProcessors();
@SuppressWarnings("unused")
private int findOptimalPerformanceLevel() {
@@ -178,6 +178,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
@Override
public void run() {
long nextFrameTime = System.currentTimeMillis();
DecodeUnit du;
while (!isInterrupted())
{
long diff = nextFrameTime - System.currentTimeMillis();
@@ -231,8 +232,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
decoderBuffer.clear();
for (ByteBufferDescriptor bbd = decodeUnit.getBufferHead();
bbd != null; bbd = bbd.nextDescriptor) {
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
}
@@ -242,8 +242,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
int offset = 0;
for (ByteBufferDescriptor bbd = decodeUnit.getBufferHead();
bbd != null; bbd = bbd.nextDescriptor) {
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
offset += bbd.length;
}
@@ -1,6 +1,5 @@
package com.limelight.binding.video;
import com.limelight.nvstream.av.DecodeUnit;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer;
@@ -56,11 +55,6 @@ public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer {
return decoderRenderer.getCapabilities();
}
@Override
public void directSubmitDecodeUnit(DecodeUnit du) {
decoderRenderer.directSubmitDecodeUnit(du);
}
@Override
public int getAverageDecoderLatency() {
if (decoderRenderer != null) {
@@ -2,6 +2,6 @@ package com.limelight.binding.video;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
public abstract class EnhancedDecoderRenderer extends VideoDecoderRenderer {
public abstract class EnhancedDecoderRenderer implements VideoDecoderRenderer {
public abstract String getDecoderName();
}
@@ -28,9 +28,9 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
private ByteBuffer[] videoDecoderInputBuffers;
private MediaCodec videoDecoder;
private Thread rendererThread;
private final boolean needsSpsBitstreamFixup, isExynos4;
private boolean needsSpsBitstreamFixup, isExynos4;
private VideoDepacketizer depacketizer;
private final boolean adaptivePlayback, directSubmit;
private boolean adaptivePlayback;
private int initialWidth, initialHeight;
private boolean needsBaselineSpsHack;
@@ -46,6 +46,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
private int numPpsIn;
private int numIframeIn;
private static final boolean ENABLE_ASYNC_RENDERER = false;
@TargetApi(Build.VERSION_CODES.KITKAT)
public MediaCodecDecoderRenderer() {
//dumpDecoders();
@@ -56,15 +58,12 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
}
if (decoder == null) {
// This case is handled later in setup()
needsSpsBitstreamFixup = isExynos4 =
adaptivePlayback = directSubmit = false;
return;
}
decoderName = decoder.getName();
// Set decoder-specific attributes
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(decoderName, decoder);
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder);
@@ -78,11 +77,9 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
if (isExynos4) {
LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
}
if (directSubmit) {
LimeLog.info("Decoder "+decoderName+" will use direct submit");
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.initialWidth = width;
@@ -110,6 +107,52 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
}
// On Lollipop, we use asynchronous mode to avoid having a busy looping renderer thread
if (ENABLE_ASYNC_RENDERER && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
videoDecoder.setCallback(new MediaCodec.Callback() {
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + format);
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index,
BufferInfo info) {
try {
// FIXME: It looks like we can't frameskip here
codec.releaseOutputBuffer(index, true);
} catch (Exception e) {
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
try {
submitDecodeUnit(depacketizer.takeNextDecodeUnit(), codec.getInputBuffer(index), index);
} catch (InterruptedException e) {
// What do we do here?
e.printStackTrace();
} catch (Exception e) {
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
@Override
public void onError(MediaCodec codec, CodecException e) {
if (e.isTransient()) {
LimeLog.warning(e.getDiagnosticInfo());
e.printStackTrace();
}
else {
LimeLog.severe(e.getDiagnosticInfo());
e.printStackTrace();
}
}
});
}
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
@@ -119,7 +162,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void handleDecoderException(Exception e, ByteBuffer buf, int codecFlags) {
private void handleDecoderException(MediaCodecDecoderRenderer dr, Exception e, ByteBuffer buf, int codecFlags) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (e instanceof CodecException) {
CodecException codecExc = (CodecException) e;
@@ -135,92 +178,14 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
}
if (buf != null || codecFlags != 0) {
throw new RendererException(this, e, buf, codecFlags);
throw new RendererException(dr, e, buf, codecFlags);
}
else {
throw new RendererException(this, e);
throw new RendererException(dr, e);
}
}
private void startDirectSubmitRendererThread()
{
rendererThread = new Thread() {
@SuppressWarnings("deprecation")
@Override
public void run() {
BufferInfo info = new BufferInfo();
while (!isInterrupted()) {
try {
// Try to output a frame
int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
if (outIndex >= 0) {
long presentationTimeUs = info.presentationTimeUs;
int lastIndex = outIndex;
// Get the last output buffer in the queue
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
videoDecoder.releaseOutputBuffer(lastIndex, false);
lastIndex = outIndex;
presentationTimeUs = info.presentationTimeUs;
}
// Render the last buffer
videoDecoder.releaseOutputBuffer(lastIndex, true);
// Add delta time to the totals (excluding probable outliers)
long delta = System.currentTimeMillis() - (presentationTimeUs / 1000);
if (delta >= 0 && delta < 1000) {
decoderTimeMs += delta;
totalTimeMs += delta;
}
} else {
switch (outIndex) {
case MediaCodec.INFO_TRY_AGAIN_LATER:
break;
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
LimeLog.info("Output buffers changed");
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
break;
default:
break;
}
}
} catch (Exception e) {
handleDecoderException(e, null, 0);
}
}
}
};
rendererThread.setName("Video - Renderer (MediaCodec)");
rendererThread.setPriority(Thread.NORM_PRIORITY + 2);
rendererThread.start();
}
private int dequeueInputBuffer(boolean wait, boolean infiniteWait) {
int index;
long startTime, queueTime;
startTime = System.currentTimeMillis();
index = videoDecoder.dequeueInputBuffer(wait ? (infiniteWait ? -1 : 3000) : 0);
if (index < 0) {
return index;
}
queueTime = System.currentTimeMillis();
if (queueTime - startTime >= 20) {
LimeLog.warning("Queue input buffer ran long: "+(queueTime - startTime)+" ms");
}
return index;
}
private void startLegacyRendererThread()
private void startRendererThread()
{
rendererThread = new Thread() {
@SuppressWarnings("deprecation")
@@ -229,7 +194,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
BufferInfo info = new BufferInfo();
DecodeUnit du = null;
int inputIndex = -1;
long lastDuDequeueTime = 0;
while (!isInterrupted())
{
// In order to get as much data to the decoder as early as possible,
@@ -237,12 +201,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
if (inputIndex == -1 && du == null) {
try {
for (int i = 0; i < 5; i++) {
inputIndex = dequeueInputBuffer(false, false);
inputIndex = videoDecoder.dequeueInputBuffer(0);
du = depacketizer.pollNextDecodeUnit();
if (du != null) {
lastDuDequeueTime = System.currentTimeMillis();
notifyDuReceived(du);
}
// Stop if we can't get a DU or input buffer
if (du == null || inputIndex == -1) {
@@ -256,7 +216,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
}
} catch (Exception e) {
inputIndex = -1;
handleDecoderException(e, null, 0);
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
@@ -271,30 +231,21 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
// If we've got a DU waiting to be given to the decoder,
// wait a full 3 ms for an input buffer. Otherwise
// just see if we can get one immediately.
inputIndex = dequeueInputBuffer(du != null, false);
inputIndex = videoDecoder.dequeueInputBuffer(du != null ? 3000 : 0);
} catch (Exception e) {
inputIndex = -1;
handleDecoderException(e, null, 0);
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
// Grab a decode unit if we don't have one already
if (du == null) {
du = depacketizer.pollNextDecodeUnit();
if (du != null) {
lastDuDequeueTime = System.currentTimeMillis();
notifyDuReceived(du);
}
}
// If we've got both a decode unit and an input buffer, we'll
// submit now. Otherwise, we wait until we have one.
if (du != null && inputIndex >= 0) {
long submissionTime = System.currentTimeMillis();
if (submissionTime - lastDuDequeueTime >= 20) {
LimeLog.warning("Receiving an input buffer took too long: "+(submissionTime - lastDuDequeueTime)+" ms");
}
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
// DU and input buffer have both been consumed
@@ -347,7 +298,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
}
}
} catch (Exception e) {
handleDecoderException(e, null, 0);
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
}
@@ -365,15 +316,11 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
// Start the decoder
videoDecoder.start();
// On devices pre-Lollipop, we'll use a rendering thread
if (!ENABLE_ASYNC_RENDERER || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
if (directSubmit) {
startDirectSubmitRendererThread();
startRendererThread();
}
else {
startLegacyRendererThread();
}
return true;
}
@@ -410,7 +357,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
timestampUs, codecFlags);
break;
} catch (Exception e) {
handleDecoderException(e, null, codecFlags);
handleDecoderException(this, e, null, codecFlags);
lastException = e;
}
}
@@ -422,7 +369,14 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
@SuppressWarnings("deprecation")
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
long timestampUs = System.currentTimeMillis() * 1000;
long currentTime = System.currentTimeMillis();
long delta = currentTime-decodeUnit.getReceiveTimestamp();
if (delta >= 0 && delta < 1000) {
totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp();
totalFrames++;
}
long timestampUs = currentTime * 1000;
if (timestampUs <= lastTimestampUs) {
// We can't submit multiple buffers with the same timestamp
// so bump it up by one before queuing
@@ -446,7 +400,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
boolean needsSpsReplay = false;
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
ByteBufferDescriptor header = decodeUnit.getBufferHead();
ByteBufferDescriptor header = decodeUnit.getBufferList().get(0);
if (header.data[header.offset+4] == 0x67) {
numSpsIn++;
@@ -531,8 +485,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
}
// Copy data from our buffer list into the input buffer
for (ByteBufferDescriptor desc = decodeUnit.getBufferHead();
desc != null; desc = desc.nextDescriptor) {
for (ByteBufferDescriptor desc : decodeUnit.getBufferList())
{
buf.put(desc.data, desc.offset, desc.length);
}
@@ -548,7 +502,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
}
private void replaySps() {
int inputIndex = dequeueInputBuffer(true, true);
int inputIndex = videoDecoder.dequeueInputBuffer(-1);
ByteBuffer inputBuffer = videoDecoderInputBuffers[inputIndex];
inputBuffer.clear();
@@ -576,15 +530,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
@Override
public int getCapabilities() {
int caps = 0;
caps |= adaptivePlayback ?
return adaptivePlayback ?
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0;
caps |= directSubmit ?
VideoDecoderRenderer.CAPABILITY_DIRECT_SUBMIT : 0;
return caps;
}
@Override
@@ -608,40 +555,11 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
return decoderName;
}
private void notifyDuReceived(DecodeUnit du) {
long currentTime = System.currentTimeMillis();
long delta = currentTime-du.getReceiveTimestamp();
if (delta >= 0 && delta < 1000) {
totalTimeMs += currentTime-du.getReceiveTimestamp();
totalFrames++;
}
}
@Override
public void directSubmitDecodeUnit(DecodeUnit du) {
int inputIndex;
notifyDuReceived(du);
for (;;) {
try {
inputIndex = dequeueInputBuffer(true, true);
break;
} catch (Exception e) {
handleDecoderException(e, null, 0);
}
}
if (inputIndex >= 0) {
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
}
}
public class RendererException extends RuntimeException {
private static final long serialVersionUID = 8985937536997012406L;
private final Exception originalException;
private final MediaCodecDecoderRenderer renderer;
private Exception originalException;
private MediaCodecDecoderRenderer renderer;
private ByteBuffer currentBuffer;
private int currentCodecFlags;
@@ -20,27 +20,12 @@ import com.limelight.LimeLog;
public class MediaCodecHelper {
private static final List<String> preferredDecoders;
public static final List<String> preferredDecoders;
private static final List<String> blacklistedDecoderPrefixes;
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
private static final List<String> whitelistedAdaptiveResolutionPrefixes;
private static final List<String> baselineProfileHackPrefixes;
private static final List<String> directSubmitPrefixes;
static {
directSubmitPrefixes = new LinkedList<String>();
// These decoders have low enough input buffer latency that they
// can be directly invoked from the receive thread
directSubmitPrefixes.add("omx.qcom");
directSubmitPrefixes.add("omx.sec");
directSubmitPrefixes.add("omx.exynos");
directSubmitPrefixes.add("omx.intel");
directSubmitPrefixes.add("omx.brcm");
directSubmitPrefixes.add("omx.TI");
directSubmitPrefixes.add("omx.arc");
}
public static final List<String> blacklistedDecoderPrefixes;
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
public static final List<String> whitelistedAdaptiveResolutionPrefixes;
public static final List<String> baselineProfileHackPrefixes;
static {
preferredDecoders = new LinkedList<String>();
@@ -113,10 +98,6 @@ public class MediaCodecHelper {
return false;
}
public static boolean decoderCanDirectSubmit(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device();
}
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
}
@@ -165,7 +146,7 @@ public class MediaCodecHelper {
return str;
}
private static MediaCodecInfo findPreferredDecoder() {
public static MediaCodecInfo findPreferredDecoder() {
// This is a different algorithm than the other findXXXDecoder functions,
// because we want to evaluate the decoders in our list's order
// rather than MediaCodecList's order
@@ -236,7 +217,7 @@ public class MediaCodecHelper {
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
// and we want to be sure all callers are handling this possibility
@SuppressWarnings("RedundantThrows")
private static MediaCodecInfo findKnownSafeDecoder() throws Exception {
public static MediaCodecInfo findKnownSafeDecoder() throws Exception {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders
if (codecInfo.isEncoder()) {
@@ -1,11 +1,10 @@
package com.limelight.computers;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
@@ -31,21 +30,18 @@ import android.os.IBinder;
import org.xmlpull.v1.XmlPullParserException;
public class ComputerManagerService extends Service {
private static final int SERVERINFO_POLLING_PERIOD_MS = 3000;
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
private static final int POLLING_PERIOD_MS = 3000;
private static final int MDNS_QUERY_PERIOD_MS = 1000;
private static final int FAST_POLL_TIMEOUT = 500;
private static final int OFFLINE_POLL_TRIES = 3;
private final ComputerManagerBinder binder = new ComputerManagerBinder();
private ComputerManagerBinder binder = new ComputerManagerBinder();
private ComputerDatabaseManager dbManager;
private final AtomicInteger dbRefCount = new AtomicInteger(0);
private AtomicInteger dbRefCount = new AtomicInteger(0);
private IdentityManager idManager;
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
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;
@@ -69,7 +65,10 @@ public class ComputerManagerService extends Service {
};
// Returns true if the details object was modified
private boolean runPoll(ComputerDetails details, boolean newPc, int offlineCount) throws InterruptedException {
private boolean runPoll(ComputerDetails details)
{
boolean newPc = details.name.isEmpty();
if (!getLocalDatabaseReference()) {
return false;
}
@@ -77,23 +76,12 @@ public class ComputerManagerService extends Service {
activePolls.incrementAndGet();
// Poll the machine
try {
if (!pollComputer(details)) {
if (!newPc && offlineCount < OFFLINE_POLL_TRIES) {
// Return without calling the listener
releaseLocalDatabaseReference();
return false;
}
if (!doPollMachine(details)) {
details.state = ComputerDetails.State.OFFLINE;
details.reachability = ComputerDetails.Reachability.OFFLINE;
}
} catch (InterruptedException e) {
releaseLocalDatabaseReference();
throw e;
} finally {
activePolls.decrementAndGet();
}
// If it's online, update our persistent state
if (details.state == ComputerDetails.State.ONLINE) {
@@ -103,7 +91,7 @@ public class ComputerManagerService extends Service {
if (dbManager.getComputerByName(details.name) == null) {
// It's gone
releaseLocalDatabaseReference();
return false;
return true;
}
}
@@ -123,21 +111,13 @@ public class ComputerManagerService extends Service {
Thread t = new Thread() {
@Override
public void run() {
int offlineCount = 0;
while (!isInterrupted() && pollingActive) {
try {
// Check if this poll has modified the details
if (!runPoll(details, false, offlineCount)) {
LimeLog.warning(details.name + " is offline (try " + offlineCount + ")");
offlineCount++;
}
else {
offlineCount = 0;
}
runPoll(details);
// Wait until the next polling interval
Thread.sleep(SERVERINFO_POLLING_PERIOD_MS / ((offlineCount > 0) ? 2 : 1));
try {
Thread.sleep(POLLING_PERIOD_MS);
} catch (InterruptedException e) {
break;
}
@@ -197,6 +177,10 @@ public class ComputerManagerService extends Service {
return ComputerManagerService.this.addComputerBlocking(addr);
}
public void addComputer(InetAddress addr) {
ComputerManagerService.this.addComputer(addr);
}
public void removeComputer(String name) {
ComputerManagerService.this.removeComputer(name);
}
@@ -255,7 +239,7 @@ public class ComputerManagerService extends Service {
@Override
public void notifyComputerAdded(MdnsComputer computer) {
// Kick off a serverinfo poll on this machine
addComputerBlocking(computer.getAddress());
addComputer(computer.getAddress());
}
@Override
@@ -271,11 +255,28 @@ public class ComputerManagerService extends Service {
};
}
public void addComputer(InetAddress addr) {
// Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr;
fakeDetails.remoteIp = addr;
fakeDetails.name = "";
addTuple(fakeDetails);
}
private void addTuple(ComputerDetails details) {
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
// Check if this is the same computer
if (tuple.computer.uuid.equals(details.uuid)) {
if (tuple.computer == details ||
// If there's no name on one of these computers, compare with the local IP
((details.name.isEmpty() || tuple.computer.name.isEmpty()) &&
tuple.computer.localIp.equals(details.localIp)) ||
// If there is a name on both computers, compare with name
((!details.name.isEmpty() && !tuple.computer.name.isEmpty()) &&
tuple.computer.name.equals(details.name))) {
// Update details anyway in case this machine has been re-added by IP
// after not being reachable by our existing information
tuple.computer.localIp = details.localIp;
@@ -306,18 +307,13 @@ public class ComputerManagerService extends Service {
ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr;
fakeDetails.remoteIp = addr;
fakeDetails.name = "";
// Block while we try to fill the details
try {
runPoll(fakeDetails, true, 0);
} catch (InterruptedException e) {
return false;
}
runPoll(fakeDetails);
// If the machine is reachable, it was successful
if (fakeDetails.state == ComputerDetails.State.ONLINE) {
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
// Start a polling thread for this machine
addTuple(fakeDetails);
return true;
@@ -388,91 +384,14 @@ public class ComputerManagerService extends Service {
}
}
// Just try to establish a TCP connection to speculatively detect a running
// GFE server
private boolean fastPollIp(InetAddress addr) {
Socket s = new Socket();
try {
s.connect(new InetSocketAddress(addr, NvHTTP.PORT), FAST_POLL_TIMEOUT);
s.close();
return true;
} catch (IOException e) {
return false;
}
}
private void startFastPollThread(final InetAddress addr, final boolean[] info) {
Thread t = new Thread() {
@Override
public void run() {
boolean pollRes = fastPollIp(addr);
synchronized (info) {
info[0] = true; // Done
info[1] = pollRes; // Polling result
info.notify();
}
}
};
t.setName("Fast Poll - "+addr.getHostAddress());
t.start();
}
private ComputerDetails.Reachability fastPollPc(final InetAddress local, final InetAddress remote) throws InterruptedException {
final boolean[] remoteInfo = new boolean[2];
final boolean[] localInfo = new boolean[2];
startFastPollThread(local, localInfo);
startFastPollThread(remote, remoteInfo);
// Check local first
synchronized (localInfo) {
while (!localInfo[0]) {
localInfo.wait(500);
}
if (localInfo[1]) {
return ComputerDetails.Reachability.LOCAL;
}
}
// Now remote
synchronized (remoteInfo) {
while (!remoteInfo[0]) {
remoteInfo.wait(500);
}
if (remoteInfo[1]) {
return ComputerDetails.Reachability.REMOTE;
}
}
return ComputerDetails.Reachability.OFFLINE;
}
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
private boolean pollComputer(ComputerDetails details, boolean localFirst) {
ComputerDetails polledDetails;
ComputerDetails.Reachability reachability;
// If the local address is routable across the Internet,
// always consider this PC remote to be conservative
if (details.localIp.equals(details.remoteIp)) {
reachability = ComputerDetails.Reachability.REMOTE;
localFirst = false;
}
else {
// Do a TCP-level connection to the HTTP server to see if it's listening
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localIp+", "+details.remoteIp+")");
reachability = fastPollPc(details.localIp, details.remoteIp);
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
}
// If no connection could be established to either IP address, there's nothing we can do
if (reachability == ComputerDetails.Reachability.OFFLINE) {
return false;
}
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
if (localFirst) {
polledDetails = tryPollIp(details, details.localIp);
@@ -506,19 +425,24 @@ public class ComputerManagerService extends Service {
return false;
}
// Save the old MAC address
String savedMacAddress = details.macAddress;
// If we got here, it's reachable
details.update(polledDetails);
// If the new MAC address is empty, restore the old one (workaround for GFE bug)
if (details.macAddress.equals("00:00:00:00:00:00") && savedMacAddress != null) {
LimeLog.info("MAC address was empty; using existing value: "+savedMacAddress);
details.macAddress = savedMacAddress;
return true;
}
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
@@ -567,8 +491,8 @@ public class ComputerManagerService extends Service {
public class ApplistPoller {
private Thread thread;
private final ComputerDetails computer;
private final Object pollEvent = new Object();
private ComputerDetails computer;
private Object pollEvent = new Object();
public ApplistPoller(ComputerDetails computer) {
this.computer = computer;
@@ -583,7 +507,7 @@ public class ComputerManagerService extends Service {
private boolean waitPollingDelay() {
try {
synchronized (pollEvent) {
pollEvent.wait(APPLIST_POLLING_PERIOD_MS);
pollEvent.wait(POLLING_PERIOD_MS);
}
} catch (InterruptedException e) {
return false;
@@ -628,19 +552,9 @@ public class ComputerManagerService extends Service {
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
// Open the cache file
OutputStream cacheOut = null;
try {
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
FileOutputStream cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
CacheHelper.writeStringToOutputStream(cacheOut, appList);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (cacheOut != null) {
cacheOut.close();
}
} catch (IOException e) {}
}
// Update the computer
computer.rawAppList = appList;
@@ -679,7 +593,7 @@ public class ComputerManagerService extends Service {
class PollingTuple {
public Thread thread;
public final ComputerDetails computer;
public ComputerDetails computer;
public PollingTuple(ComputerDetails computer, Thread thread) {
this.computer = computer;
@@ -70,7 +70,7 @@ public class DiscoveryService extends Service {
});
}
private final DiscoveryBinder binder = new DiscoveryBinder();
private DiscoveryBinder binder = new DiscoveryBinder();
@Override
public IBinder onBind(Intent intent) {
@@ -1,69 +1,118 @@
package com.limelight.grid;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.koushikdutta.async.future.FutureCallback;
import com.koushikdutta.ion.ImageViewBitmapInfo;
import com.koushikdutta.ion.Ion;
import com.limelight.AppView;
import com.limelight.LimeLog;
import com.limelight.R;
import com.limelight.grid.assets.CachedAppAssetLoader;
import com.limelight.grid.assets.DiskAssetLoader;
import com.limelight.grid.assets.MemoryAssetLoader;
import com.limelight.grid.assets.NetworkAssetLoader;
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.BufferedInputStream;
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.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.Map;
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;
@SuppressWarnings("unchecked")
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
private static final int ART_WIDTH_PX = 300;
private static final int SMALL_WIDTH_DP = 100;
private static final int LARGE_WIDTH_DP = 150;
private final CachedAppAssetLoader loader;
private ComputerDetails computer;
private String uniqueId;
private LimelightCryptoProvider cryptoProvider;
private SSLContext sslContext;
private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>();
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws KeyManagementException, NoSuchAlgorithmException {
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
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);
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
int dp;
this.computer = computer;
this.uniqueId = uniqueId;
if (small) {
dp = SMALL_WIDTH_DP;
}
else {
dp = LARGE_WIDTH_DP;
cryptoProvider = PlatformBinding.getCryptoProvider(context);
sslContext = SSLContext.getInstance("SSL");
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom());
}
double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160.0));
if (scalingDivisor < 1.0) {
// We don't want to make them bigger before draw-time
scalingDivisor = 1.0;
TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
LimeLog.info("Art scaling divisor: " + scalingDivisor);
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}};
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = (int) scalingDivisor;
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
new NetworkAssetLoader(context, uniqueId),
new MemoryAssetLoader(),
new DiskAssetLoader(context.getCacheDir()),
BitmapFactory.decodeResource(activity.getResources(),
R.drawable.image_loading, options));
KeyManager[] ourKeyman = new KeyManager[] {
new X509KeyManager() {
public String chooseClientAlias(String[] keyTypes,
Principal[] issuers, Socket socket) {
return "Limelight-RSA";
}
public void cancelQueuedOperations() {
loader.cancelForegroundLoads();
loader.cancelBackgroundLoads();
loader.freeCacheMemory();
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
@@ -73,22 +122,62 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
});
}
public void addApp(AppView.AppObject app) {
// Queue a request to fetch this bitmap into cache
loader.queueCacheLoad(app.app);
private InetAddress getCurrentAddress() {
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
return computer.localIp;
}
else {
return computer.remoteIp;
}
}
// Add the app to our sorted list
public void addApp(AppView.AppObject app) {
itemList.add(app);
sortList();
}
public void removeApp(AppView.AppObject app) {
itemList.remove(app);
public void abortPendingRequests() {
HashMap<ImageView, Future> tempMap;
synchronized (pendingRequests) {
// Copy the pending requests under a lock
tempMap = new HashMap<ImageView, Future>(pendingRequests);
}
public boolean populateImageView(ImageView imgView, AppView.AppObject obj) {
// Let the cached asset loader handle it
loader.populateImageView(obj.app, imgView);
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) {
// Hide the image view while we're loading the image from disk cache
imgView.setVisibility(View.INVISIBLE);
// Check the on-disk cache
new ImageCacheRequest(imgView, obj.app.getAppId()).execute();
return true;
}
@@ -112,4 +201,84 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
// No overlay
return false;
}
private class ImageCacheRequest extends AsyncTask<Void, Void, Bitmap> {
private ImageView view;
private 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 e) {}
}
}
return null;
}
@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);
view.setVisibility(View.VISIBLE);
}
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);
view.setVisibility(View.VISIBLE);
// Set SSL contexts correctly to allow us to authenticate
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts);
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext);
// 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, Bitmap result) {
synchronized (pendingRequests) {
pendingRequests.remove(view);
}
if (result != null) {
// Make the view visible now
view.setImageBitmap(result);
view.setVisibility(View.VISIBLE);
// Populate the disk cache if we got an image back
populateBitmapCache(computer.uuid, appId, result);
}
else {
// Leave the loading icon as is (probably should change this eventually...)
}
}
});
pendingRequests.put(view, f);
}
}
}
};
}
@@ -13,11 +13,11 @@ 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;
protected Context context;
protected int defaultImageRes;
protected int layoutId;
protected ArrayList<T> itemList = new ArrayList<T>();
protected LayoutInflater inflater;
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
this.context = context;
@@ -1,333 +0,0 @@
package com.limelight.grid.assets;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.widget.ImageView;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CachedAppAssetLoader {
private static final int MAX_CONCURRENT_DISK_LOADS = 3;
private static final int MAX_CONCURRENT_NETWORK_LOADS = 3;
private static final int MAX_CONCURRENT_CACHE_LOADS = 1;
private static final int MAX_PENDING_CACHE_LOADS = 100;
private static final int MAX_PENDING_NETWORK_LOADS = 40;
private static final int MAX_PENDING_DISK_LOADS = 40;
private final ThreadPoolExecutor cacheExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_CACHE_LOADS, MAX_CONCURRENT_CACHE_LOADS,
Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(MAX_PENDING_CACHE_LOADS),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_DISK_LOADS, MAX_CONCURRENT_DISK_LOADS,
Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(MAX_PENDING_DISK_LOADS),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final ThreadPoolExecutor networkExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_NETWORK_LOADS, MAX_CONCURRENT_NETWORK_LOADS,
Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(MAX_PENDING_NETWORK_LOADS),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final ComputerDetails computer;
private final double scalingDivider;
private final NetworkAssetLoader networkLoader;
private final MemoryAssetLoader memoryLoader;
private final DiskAssetLoader diskLoader;
private final Bitmap placeholderBitmap;
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
DiskAssetLoader diskLoader, Bitmap placeholderBitmap) {
this.computer = computer;
this.scalingDivider = scalingDivider;
this.networkLoader = networkLoader;
this.memoryLoader = memoryLoader;
this.diskLoader = diskLoader;
this.placeholderBitmap = placeholderBitmap;
}
public void cancelBackgroundLoads() {
Runnable r;
while ((r = cacheExecutor.getQueue().poll()) != null) {
cacheExecutor.remove(r);
}
}
public void cancelForegroundLoads() {
Runnable r;
while ((r = foregroundExecutor.getQueue().poll()) != null) {
foregroundExecutor.remove(r);
}
while ((r = networkExecutor.getQueue().poll()) != null) {
networkExecutor.remove(r);
}
}
public void freeCacheMemory() {
memoryLoader.clearCache();
}
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
// Try 3 times
for (int i = 0; i < 3; i++) {
// Check again whether we've been cancelled or the image view is gone
if (task != null && (task.isCancelled() || task.imageViewRef.get() == null)) {
return null;
}
InputStream in = networkLoader.getBitmapStream(tuple);
if (in != null) {
// Write the stream straight to disk
diskLoader.populateCacheWithStream(tuple, in);
// Close the network input stream
try {
in.close();
} catch (IOException ignored) {}
// If there's a task associated with this load, we should return the bitmap
if (task != null) {
return diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
}
else {
// Otherwise it's a background load and we return nothing
return null;
}
}
// Wait 1 second with a bit of fuzz
try {
Thread.sleep((int) (1000 + (Math.random() * 500)));
} catch (InterruptedException e) {
return null;
}
}
return null;
}
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
private final WeakReference<ImageView> imageViewRef;
private final boolean diskOnly;
private LoaderTuple tuple;
public LoaderTask(ImageView imageView, boolean diskOnly) {
this.imageViewRef = new WeakReference<ImageView>(imageView);
this.diskOnly = diskOnly;
}
@Override
protected Bitmap doInBackground(LoaderTuple... params) {
tuple = params[0];
// Check whether it has been cancelled or the image view is gone
if (isCancelled() || imageViewRef.get() == null) {
return null;
}
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp == null) {
if (!diskOnly) {
// Try to load the asset from the network
bmp = doNetworkAssetLoad(tuple, this);
} else {
// Report progress to display the placeholder and spin
// off the network-capable task
publishProgress();
}
}
// Cache the bitmap
if (bmp != null) {
memoryLoader.populateCache(tuple, bmp);
}
return bmp;
}
@Override
protected void onProgressUpdate(Void... nothing) {
// Do nothing if cancelled
if (isCancelled()) {
return;
}
// If the current loader task for this view isn't us, do nothing
final ImageView imageView = imageViewRef.get();
if (getLoaderTask(imageView) == this) {
// Set off another loader task on the network executor
LoaderTask task = new LoaderTask(imageView, false);
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
imageView.setAlpha(1.0f);
imageView.setImageDrawable(asyncDrawable);
task.executeOnExecutor(networkExecutor, tuple);
}
}
@Override
protected void onPostExecute(Bitmap bitmap) {
// Do nothing if cancelled
if (isCancelled()) {
return;
}
final ImageView imageView = imageViewRef.get();
if (getLoaderTask(imageView) == this) {
// Set the bitmap
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
}
// Show the view
imageView.setAlpha(1.0f);
}
}
}
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<LoaderTask> loaderTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,
LoaderTask loaderTask) {
super(res, bitmap);
loaderTaskReference = new WeakReference<LoaderTask>(loaderTask);
}
public LoaderTask getLoaderTask() {
return loaderTaskReference.get();
}
}
private static LoaderTask getLoaderTask(ImageView imageView) {
if (imageView == null) {
return null;
}
final Drawable drawable = imageView.getDrawable();
// If our drawable is in play, get the loader task
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getLoaderTask();
}
return null;
}
private static boolean cancelPendingLoad(LoaderTuple tuple, ImageView imageView) {
final LoaderTask loaderTask = getLoaderTask(imageView);
// Check if any task was pending for this image view
if (loaderTask != null && !loaderTask.isCancelled()) {
final LoaderTuple taskTuple = loaderTask.tuple;
// Cancel the task if it's not already loading the same data
if (taskTuple == null || !taskTuple.equals(tuple)) {
loaderTask.cancel(true);
} else {
// It's already loading what we want
return false;
}
}
// Allow the load to proceed
return true;
}
public void queueCacheLoad(NvApp app) {
final LoaderTuple tuple = new LoaderTuple(computer, app);
if (memoryLoader.loadBitmapFromCache(tuple) != null) {
// It's in memory which means it must also be on disk
return;
}
// Queue a fetch in the cache executor
cacheExecutor.execute(new Runnable() {
@Override
public void run() {
// Check if the image is cached on disk
if (diskLoader.checkCacheExists(tuple)) {
return;
}
// Try to load the asset from the network and cache result on disk
doNetworkAssetLoad(tuple, null);
}
});
}
public void populateImageView(NvApp app, ImageView view) {
LoaderTuple tuple = new LoaderTuple(computer, app);
// If there's already a task in progress for this view,
// cancel it. If the task is already loading the same image,
// we return and let that load finish.
if (!cancelPendingLoad(tuple, view)) {
return;
}
// First, try the memory cache in the current context
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
if (bmp != null) {
// Show the bitmap immediately
view.setAlpha(1.0f);
view.setImageBitmap(bmp);
return;
}
// If it's not in memory, create an async task to load it. This task will be attached
// via AsyncDrawable to this view.
final LoaderTask task = new LoaderTask(view, true);
final AsyncDrawable asyncDrawable = new AsyncDrawable(view.getResources(), placeholderBitmap, task);
view.setAlpha(0.0f);
view.setImageDrawable(asyncDrawable);
// Run the task on our foreground executor
task.executeOnExecutor(foregroundExecutor, tuple);
}
public class LoaderTuple {
public final ComputerDetails computer;
public final NvApp app;
public LoaderTuple(ComputerDetails computer, NvApp app) {
this.computer = computer;
this.app = app;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof LoaderTuple)) {
return false;
}
LoaderTuple other = (LoaderTuple) o;
return computer.uuid.equals(other.computer.uuid) && app.getAppId() == other.app.getAppId();
}
@Override
public String toString() {
return "("+computer.uuid+", "+app.getAppId()+")";
}
}
}
@@ -1,67 +0,0 @@
package com.limelight.grid.assets;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import com.limelight.LimeLog;
import com.limelight.utils.CacheHelper;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class DiskAssetLoader {
private final File cacheDir;
public DiskAssetLoader(File cacheDir) {
this.cacheDir = cacheDir;
}
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
InputStream in = null;
Bitmap bmp = null;
try {
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = sampleSize;
bmp = BitmapFactory.decodeStream(in, null, options);
} catch (FileNotFoundException ignored) {
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ignored) {}
}
}
if (bmp != null) {
LimeLog.info("Disk cache hit for tuple: "+tuple);
}
return bmp;
}
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
OutputStream out = null;
try {
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
CacheHelper.writeInputStreamToOutputStream(input, out);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException ignored) {}
}
}
}
}
@@ -1,37 +0,0 @@
package com.limelight.grid.assets;
import android.graphics.Bitmap;
import android.util.LruCache;
import com.limelight.LimeLog;
public class MemoryAssetLoader {
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 16) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// Sizeof returns kilobytes
return bitmap.getByteCount() / 1024;
}
};
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId();
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
Bitmap bmp = memoryCache.get(constructKey(tuple));
if (bmp != null) {
LimeLog.info("Memory cache hit for tuple: "+tuple);
}
return bmp;
}
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
memoryCache.put(constructKey(tuple), bitmap);
}
public void clearCache() {
memoryCache.evictAll();
}
}
@@ -1,49 +0,0 @@
package com.limelight.grid.assets;
import android.content.Context;
import com.limelight.LimeLog;
import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvHTTP;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
public class NetworkAssetLoader {
private final Context context;
private final String uniqueId;
public NetworkAssetLoader(Context context, String uniqueId) {
this.context = context;
this.uniqueId = uniqueId;
}
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
InputStream in = null;
try {
in = http.getBoxArt(tuple.app);
} catch (IOException e) {}
if (in != null) {
LimeLog.info("Network asset load complete: " + tuple);
}
else {
LimeLog.info("Network asset load failed: " + tuple);
}
return in;
}
private static InetAddress getCurrentAddress(ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
return computer.localIp;
}
else {
return computer.remoteIp;
}
}
}
@@ -14,7 +14,6 @@ import com.limelight.utils.UiHelper;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
@@ -22,16 +21,15 @@ import android.os.Bundle;
import android.os.IBinder;
import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
import android.widget.Toast;
public class AddComputerManually extends Activity {
private TextView hostText;
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
private Thread addThread;
private final ServiceConnection serviceConnection = new ServiceConnection() {
private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, final IBinder binder) {
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
startAddThread();
@@ -163,12 +161,6 @@ public class AddComputerManually extends Activity {
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;
}
@@ -2,11 +2,12 @@ package com.limelight.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.preference.PreferenceManager;
public class PreferenceConfiguration {
static final String RES_FPS_PREF_STRING = "list_resolution_fps";
static final String RESX_PREF_STRING = "resolution_x";
static final String RESY_PREF_STRING = "resolution_y";
private static final String FPS_PREF_STRING = "fps";
private static final String DECODER_PREF_STRING = "list_decoders";
static final String BITRATE_PREF_STRING = "seekbar_bitrate";
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
@@ -17,16 +18,17 @@ public class PreferenceConfiguration {
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_60 = 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_MAX = 35;
private static final String DEFAULT_RES_FPS = "720p60";
private static final int DEFAULT_RESX = 1280;
private static final int DEFAULT_RESY = 720;
private static final String DEFAULT_FPS = "60";
private static final String DEFAULT_DECODER = "auto";
private static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
private static final boolean DEFAULT_STRETCH = false;
private static final boolean DEFAULT_SOPS = true;
private static final boolean DEFAULT_DISABLE_TOASTS = false;
@@ -34,7 +36,6 @@ public class PreferenceConfiguration {
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 AUTOSELECT_DECODER = 0;
@@ -46,57 +47,42 @@ public class PreferenceConfiguration {
public int deadzonePercentage;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language;
public boolean listMode, smallIconMode, multiController;
public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("720p30")) {
return BITRATE_DEFAULT_720_30;
}
else if (resFpsString.equals("720p60")) {
return BITRATE_DEFAULT_720_60;
}
else if (resFpsString.equals("1080p30")) {
return BITRATE_DEFAULT_1080_30;
}
else if (resFpsString.equals("1080p60")) {
return BITRATE_DEFAULT_1080_60;
}
else {
// Should never get here
return DEFAULT_BITRATE;
}
}
public boolean listMode, smallIconMode;
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;
}
private static int getFpsInt(SharedPreferences prefs) {
try {
return Integer.parseInt(prefs.getString(FPS_PREF_STRING, DEFAULT_FPS));
} catch (NumberFormatException e) {
return Integer.parseInt(DEFAULT_FPS);
}
}
public static int getDefaultBitrate(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
if (str.equals("720p30")) {
long width = prefs.getInt(RESX_PREF_STRING, DEFAULT_RESX);
long height = prefs.getInt(RESY_PREF_STRING, DEFAULT_RESY);
long product = width * height * getFpsInt(prefs);
if (product <= 1280 * 720 * 30) {
return BITRATE_DEFAULT_720_30;
}
else if (str.equals("720p60")) {
else if (product <= 1280 * 720 * 60) {
return BITRATE_DEFAULT_720_60;
}
else if (str.equals("1080p30")) {
else if (product <= 1920 * 1080 * 30) {
return BITRATE_DEFAULT_1080_30;
}
else if (str.equals("1080p60")) {
else if (product <= 1920 * 1080 * 60) {
return BITRATE_DEFAULT_1080_60;
}
else {
// Should never get here
return DEFAULT_BITRATE;
return BITRATE_DEFAULT_MAX;
}
}
@@ -124,33 +110,9 @@ public class PreferenceConfiguration {
PreferenceConfiguration config = new PreferenceConfiguration();
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, getDefaultBitrate(context));
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
if (str.equals("720p30")) {
config.width = 1280;
config.height = 720;
config.fps = 30;
}
else if (str.equals("720p60")) {
config.width = 1280;
config.height = 720;
config.fps = 60;
}
else if (str.equals("1080p30")) {
config.width = 1920;
config.height = 1080;
config.fps = 30;
}
else if (str.equals("1080p60")) {
config.width = 1920;
config.height = 1080;
config.fps = 60;
}
else {
// Should never get here
config.width = 1280;
config.height = 720;
config.fps = 60;
}
config.width = prefs.getInt(RESX_PREF_STRING, DEFAULT_RESX);
config.height = prefs.getInt(RESY_PREF_STRING, DEFAULT_RESY);
config.fps = getFpsInt(prefs);
config.decoder = getDecoderValue(context);
@@ -165,7 +127,6 @@ public class PreferenceConfiguration {
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;
}
@@ -0,0 +1,208 @@
package com.limelight.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.DialogPreference;
import android.util.AttributeSet;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.RadioButton;
import android.widget.SeekBar;
import android.widget.TextView;
import com.limelight.R;
public class ResolutionPreference extends DialogPreference {
private RadioButton a4_3;
private RadioButton a16_9;
private RadioButton a16_10;
private SeekBar resolutionSeekBar;
private TextView resolutionText;
private int resolutionX;
private int resolutionY;
public ResolutionPreference(Context context, AttributeSet attrs) {
super(context, attrs);
setDialogLayoutResource(R.layout.dialog_resolution);
setPositiveButtonText(android.R.string.ok);
setNegativeButtonText(android.R.string.cancel);
setDialogIcon(null);
}
private int getPreferenceState() {
int checkboxState;
int sliderState;
if (a4_3.isChecked()) {
checkboxState = 1;
}
else if (a16_10.isChecked()) {
checkboxState = 2;
}
else {
checkboxState = 0;
}
sliderState = resolutionSeekBar.getProgress();
return checkboxState << 24 | sliderState;
}
private void setPreferenceState(int state) {
int checkboxState;
int sliderState;
checkboxState = state >> 24;
sliderState = state & 0x00FFFFFF;
a4_3.setChecked(false);
a16_10.setChecked(false);
a16_9.setChecked(false);
if (checkboxState == 1) {
a4_3.setChecked(true);
}
else if (checkboxState == 2) {
a16_10.setChecked(true);
}
else {
a16_9.setChecked(true);
}
resolutionSeekBar.setProgress(sliderState);
}
private void updateResolution() {
double aspectFactor;
int step = resolutionSeekBar.getProgress() + 1;
if (a4_3.isChecked()) {
aspectFactor = 4.0/3.0;
switch (step) {
case 1:
// 640x480
resolutionY = 480;
break;
case 2:
// 800x600
resolutionY = 600;
break;
case 3:
// 1024x768
resolutionY = 768;
break;
case 4:
// 1600x1200
resolutionY = 1200;
break;
}
}
else if (a16_9.isChecked()) {
aspectFactor = 16.0/9.0;
switch (step) {
case 1:
// 1280x720
resolutionY = 720;
break;
case 2:
// 1920x1080
resolutionY = 1080;
break;
case 3:
// 2560x1440
resolutionY = 1440;
break;
case 4:
// 3840x2160
resolutionY = 2160;
break;
}
}
else /* if (a16_10.isChecked() */ {
aspectFactor = 16.0/10.0;
switch (step) {
case 1:
// 1280x800
resolutionY = 800;
break;
case 2:
// 1920x1200
resolutionY = 1200;
break;
case 3:
// 2560x1600
resolutionY = 1600;
break;
case 4:
// 3840x2400
resolutionY = 2400;
break;
}
}
resolutionX = (int)(resolutionY * aspectFactor);
resolutionText.setText(resolutionX+"x"+resolutionY);
}
@Override
protected void onDialogClosed(boolean positiveResult) {
if (positiveResult) {
SharedPreferences.Editor e = getPreferenceManager().getSharedPreferences().edit();
e.putInt(PreferenceConfiguration.RESX_PREF_STRING, resolutionX);
e.putInt(PreferenceConfiguration.RESY_PREF_STRING, resolutionY);
persistInt(getPreferenceState());
e.commit();
}
}
@Override
protected View onCreateDialogView() {
View v = super.onCreateDialogView();
a4_3 = (RadioButton) v.findViewById(R.id.aspect_4_3);
a16_9 = (RadioButton) v.findViewById(R.id.aspect_16_9);
a16_10 = (RadioButton) v.findViewById(R.id.aspect_16_10);
resolutionText = (TextView) v.findViewById(R.id.resolutionText);
resolutionSeekBar = (SeekBar) v.findViewById(R.id.resolutionSeekbar);
CompoundButton.OnCheckedChangeListener ccListener = new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
updateResolution();
}
};
a4_3.setOnCheckedChangeListener(ccListener);
a16_9.setOnCheckedChangeListener(ccListener);
a16_10.setOnCheckedChangeListener(ccListener);
resolutionSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
updateResolution();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
// Check the correct checkbox
setPreferenceState(getPersistedInt(0));
// Set initial resolution value
updateResolution();
return v;
}
}
@@ -20,14 +20,10 @@ public class SeekBarPreference extends DialogPreference
private SeekBar seekBar;
private TextView valueText;
private final Context context;
private Context context;
private final String dialogMessage;
private final String suffix;
private final int defaultValue;
private final int maxValue;
private final int minValue;
private int currentValue;
private String dialogMessage, suffix;
private int defaultValue, maxValue, minValue, currentValue;
public SeekBarPreference(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -131,6 +127,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) {
this.currentValue = progress;
if (seekBar != null) {
@@ -51,26 +51,6 @@ public class StreamSettings extends Activity {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
// Add a listener to the FPS and resolution preference
// so the bitrate can be auto-adjusted
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING);
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
String valueStr = (String) newValue;
// Write the new bitrate value
prefs.edit()
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
PreferenceConfiguration.getDefaultBitrate(valueStr))
.apply();
// Allow the original preference change to take place
return true;
}
});
}
}
}
@@ -1,7 +1,5 @@
package com.limelight.utils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@@ -11,6 +9,8 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.Scanner;
public class CacheHelper {
private static File openPath(boolean createPath, File root, String... path) {
@@ -30,25 +30,12 @@ public class CacheHelper {
return f;
}
public static boolean cacheFileExists(File root, String... path) {
return openPath(false, root, path).exists();
public static FileInputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
return new FileInputStream(openPath(false, root, path));
}
public static InputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
return new BufferedInputStream(new FileInputStream(openPath(false, root, path)));
}
public static OutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
return new BufferedOutputStream(new FileOutputStream(openPath(true, root, path)));
}
public static void writeInputStreamToOutputStream(InputStream in, OutputStream out) throws IOException {
byte[] buf = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buf)) != -1) {
out.write(buf, 0, bytesRead);
}
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 {
@@ -61,10 +48,6 @@ public class CacheHelper {
sb.append(buf, 0, bytesRead);
}
try {
in.close();
} catch (IOException ignored) {}
return sb.toString();
}
@@ -7,16 +7,15 @@ import android.app.AlertDialog;
import android.content.DialogInterface;
public class Dialog implements Runnable {
private final String title;
private final String message;
private final Activity activity;
private final boolean endAfterDismiss;
private String title, message;
private Activity activity;
private boolean endAfterDismiss;
private AlertDialog alert;
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.title = title;
@@ -1,90 +0,0 @@
package com.limelight.utils;
import android.app.Activity;
import android.content.Intent;
import android.widget.Toast;
import com.limelight.Game;
import com.limelight.R;
import com.limelight.binding.PlatformBinding;
import com.limelight.computers.ComputerManagerService;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import java.io.FileNotFoundException;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class ServerHelper {
public static InetAddress getCurrentAddressFromComputer(ComputerDetails computer) {
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp : computer.remoteIp;
}
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
ComputerManagerService.ComputerManagerBinder managerBinder) {
Intent intent = new Intent(parent, Game.class);
intent.putExtra(Game.EXTRA_HOST,
computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
computer.reachability != ComputerDetails.Reachability.LOCAL);
parent.startActivity(intent);
}
public static void doQuit(final Activity parent,
final InetAddress address,
final NvApp app,
final ComputerManagerService.ComputerManagerBinder managerBinder,
final Runnable onComplete) {
Toast.makeText(parent, parent.getResources().getString(R.string.applist_quit_app) + " " + app.getAppName() + "...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
NvHTTP httpConn;
String message;
try {
httpConn = new NvHTTP(address,
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(parent));
if (httpConn.quitApp()) {
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
} else {
message = parent.getResources().getString(R.string.applist_quit_fail) + " " + app.getAppName();
}
} catch (GfeHttpResponseException e) {
if (e.getErrorCode() == 599) {
message = "This session wasn't started by this device," +
" so it cannot be quit. End streaming on the original " +
"device or the PC itself. (Error code: "+e.getErrorCode()+")";
}
else {
message = e.getMessage();
}
} catch (UnknownHostException e) {
message = parent.getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) {
message = parent.getResources().getString(R.string.error_404);
} catch (Exception e) {
message = e.getMessage();
} finally {
if (onComplete != null) {
onComplete.run();
}
}
final String toastMessage = message;
parent.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(parent, toastMessage, Toast.LENGTH_LONG).show();
}
});
}
}).start();
}
}
@@ -9,15 +9,14 @@ import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
public class SpinnerDialog implements Runnable,OnCancelListener {
private final String title;
private final String message;
private final Activity activity;
private String title, message;
private Activity activity;
private ProgressDialog progress;
private final boolean finish;
private boolean finish;
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.title = title;
@@ -1,15 +1,11 @@
package com.limelight.utils;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.UiModeManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Configuration;
import android.view.View;
import com.limelight.R;
public class UiHelper {
// Values from https://developer.android.com/training/tv/start/layouts.html
@@ -32,31 +28,4 @@ public class UiHelper {
horizontalPaddingPixels, verticalPaddingPixels);
}
}
public static void displayQuitConfirmationDialog(Activity parent, 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(parent);
builder.setMessage(parent.getResources().getString(R.string.applist_quit_confirmation))
.setPositiveButton(parent.getResources().getString(R.string.yes), dialogClickListener)
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
.show();
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 36 KiB

+2 -2
View File
@@ -1,4 +1,4 @@
<merge xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -10,4 +10,4 @@
android:layout_height="match_parent"
android:layout_gravity="center" />
</merge>
</FrameLayout>
@@ -9,7 +9,5 @@
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
@@ -9,7 +9,5 @@
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dialog_resolution"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<RadioGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_centerHorizontal="true"
android:id="@+id/aspectGroup">
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:id="@+id/aspect_4_3"
android:text="@string/aspect_4_3"/>
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:id="@+id/aspect_16_9"
android:text="@string/aspect_16_9"/>
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:id="@+id/aspect_16_10"
android:text="@string/aspect_16_10"/>
</RadioGroup>
<SeekBar
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/aspectGroup"
android:max="3"
android:id="@+id/resolutionSeekbar"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:textSize="20sp"
android:id="@+id/resolutionText"
android:layout_below="@+id/resolutionSeekbar"/>
</RelativeLayout>
+1 -3
View File
@@ -6,12 +6,10 @@
<ListView
android:id="@+id/fragmentView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/list_view_unselected"
android:fastScrollEnabled="true"
android:longClickable="false"
android:focusable="true"
android:focusableInTouchMode="true"
android:stackFromBottom="false" >
</ListView>
-2
View File
@@ -8,7 +8,5 @@
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
@@ -8,7 +8,5 @@
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
+1 -3
View File
@@ -95,14 +95,12 @@
<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 Moonlight</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>
+6 -10
View File
@@ -1,16 +1,12 @@
<?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 name="fps_names">
<item>30 FPS</item>
<item>60 FPS</item>
</string-array>
<string-array name="resolution_values" translatable="false">
<item>720p30</item>
<item>720p60</item>
<item>1080p30</item>
<item>1080p60</item>
<string-array name="fps_values" translatable="false">
<item>30</item>
<item>60</item>
</string-array>
<string-array name="language_names" translatable="false">
+12 -7
View File
@@ -44,9 +44,9 @@
<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 frame decoding latency:</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 decoding 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>
@@ -85,8 +85,10 @@
<!-- Preferences -->
<string name="category_basic_settings">Basic Settings</string>
<string name="title_resolution_list">Select resolution and FPS target</string>
<string name="summary_resolution_list">Setting values too high for your device may cause lag or crashing</string>
<string name="title_resolution">Select stream resolution</string>
<string name="summary_resolution">Some resolutions may not be supported on your device</string>
<string name="title_fps_list">Select target FPS</string>
<string name="summary_fps_list">Some FPS values may not be supported with high resolutions on your device</string>
<string name="title_seekbar_bitrate">Select target video bitrate</string>
<string name="summary_seekbar_bitrate">Lower bitrate to reduce stuttering. Raise bitrate to increase image quality.</string>
<string name="suffix_seekbar_bitrate">Mbps</string>
@@ -95,14 +97,12 @@
<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 Moonlight</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>
@@ -117,4 +117,9 @@
<string name="category_advanced_settings">Advanced Settings</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>
<!-- Resolution Preference -->
<string name="aspect_4_3">4:3</string>
<string name="aspect_16_9">16:9</string>
<string name="aspect_16_10">16:10</string>
</resources>
+13 -14
View File
@@ -2,13 +2,17 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="@string/category_basic_settings">
<com.limelight.preferences.ResolutionPreference
android:key="resolution"
android:title="@string/title_resolution"
android:summary="@string/summary_resolution" />
<ListPreference
android:key="list_resolution_fps"
android:title="@string/title_resolution_list"
android:summary="@string/summary_resolution_list"
android:entries="@array/resolution_names"
android:entryValues="@array/resolution_values"
android:defaultValue="720p60" />
android:title="@string/title_fps_list"
android:summary="@string/summary_fps_list"
android:entries="@array/fps_names"
android:entryValues="@array/fps_values"
android:defaultValue="60" />
<com.limelight.preferences.SeekBarPreference
android:key="seekbar_bitrate"
android:dialogMessage="@string/summary_seekbar_bitrate"
@@ -26,19 +30,14 @@
android:summary="@string/summary_checkbox_disable_warnings"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_gamepad_settings">
<!--com.limelight.preferences.SeekBarPreference
<!--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>
android:title="@string/title_seekbar_deadzone"/>
</PreferenceCategory-->
<PreferenceCategory android:title="@string/category_host_settings">
<CheckBoxPreference
android:key="checkbox_enable_sops"
+1 -1
View File
@@ -2,5 +2,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Non-root application name -->
<application android:label="Moonlight" />
<application android:label="Limelight" />
</manifest>
+1 -1
View File
@@ -5,5 +5,5 @@
<uses-permission android:name="android.permission.ACCESS_SUPERUSER" />
<!-- Root application name -->
<application android:label="Moonlight (Root)" />
<application android:label="Limelight (Root)" />
</manifest>
+1 -1
View File
@@ -4,7 +4,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.1.0'
classpath 'com.android.tools.build:gradle:1.0.0'
}
}
@@ -7,7 +7,9 @@
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/classes/main" />
<output-test url="file://$MODULE_DIR$/build/classes/test" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" />