Compare commits

...
Author SHA1 Message Date
Cameron Gutman eac6998e17 Update the latency message strings to be more clear that this isn't end to end latency 2015-03-25 01:20:55 -04:00
Cameron Gutman 17afbffdb5 Include the time it takes to get an input buffer in the frame latency calculation 2015-03-25 01:08:23 -04:00
Cameron Gutman 072a439c2d Update common and decode unit API 2015-03-25 00:32:22 -04:00
Cameron Gutman c533600983 Update for 3.1.3 release 2015-03-23 17:26:37 -04:00
Cameron Gutman 5847fbb6b6 Add TI decoders to the direct submit whitelist 2015-03-23 17:14:02 -04:00
Cameron Gutman 1876b30c1b Forgot this file 2015-03-23 16:51:57 -04:00
Cameron Gutman 5c71f55993 Add another Exynos prefix 2015-03-23 16:51:32 -04:00
Cameron Gutman 9c0960d03d Add options to quit and resume streaming from the PC view 2015-03-23 16:36:43 -04:00
Cameron Gutman 29a395f3f4 Prevent updating the UI while quitting is in progress 2015-03-23 15:57:29 -04:00
Cameron Gutman a676b8d8e6 Restore the legacy path and only use direct submit for certain whitelisted decoders 2015-03-23 15:51:11 -04:00
Cameron Gutman 7ab0be3b62 Optimize app grid performance on lower end devices 2015-03-23 15:12:25 -04:00
Cameron Gutman 115853fed2 Update version to 3.1.3-beta1 2015-03-16 21:29:07 -04:00
Cameron Gutman 60beb81ae4 Target API 22 2015-03-16 21:28:49 -04:00
Cameron Gutman 5310375d42 Target Android 5.1 2015-03-16 21:28:33 -04:00
Cameron Gutman 7ce29e3a09 Add a workaround for the Nexus 9 dropping frames with the new renderer 2015-03-16 21:26:02 -04:00
Cameron Gutman 42c65f4f16 Use smaller deadzones for SHIELD controllers 2015-03-16 19:36:09 -04:00
Cameron Gutman bf2cc2a4d5 Don't assign controller numbers to devices that don't have an analog stick 2015-03-16 19:35:43 -04:00
Cameron Gutman 6d6d7121f6 Remove the Playpad Pro hack that worked around an issue with old firmware and caused the D-pad to be unresponsive on updated firmware. Fixes #41 2015-03-15 14:30:56 -07:00
Cameron Gutman 2ab67380d6 Use direct submit decoding for MediaCodec. Based on my profiling of a few devices, dequeueInputBuffer and queueInputBuffer don't take much time anyway. It allows us to stop our semi-busy looping which saves power. The depacketizer can avoid expensive synchronization and additional context switching which costs time and CPU cycles. 2015-03-09 01:49:52 -05:00
Cameron Gutman 899387caa1 Use a separate executor for network loads to avoid stalling cached loads. Optimize background cache fill loads. 2015-03-02 18:34:21 -05:00
Cameron Gutman 56c8a9e6fe Use the regular serverinfo query to update the running status of apps 2015-03-02 17:05:45 -05:00
Cameron Gutman 896288a40b Use AsyncTasks and attached Drawables to track background image loading 2015-03-02 17:03:08 -05:00
Cameron Gutman fc8ce5e4b9 Quiet down disk cache misses 2015-03-02 16:13:54 -05:00
Cameron Gutman 4affc3c4ce Update to 3.1.2 release 2015-02-27 18:12:49 -05:00
Cameron Gutman 067be54715 Show the discovery in progress view if no computers remain after one is deleted 2015-02-27 18:05:02 -05:00
Cameron Gutman 0dad2dc64b Only close the app list activity if the PC is offline not unknown 2015-02-27 15:15:01 -05:00
Cameron Gutman 867b703644 Evict cached bitmaps when closing the app list 2015-02-27 15:13:43 -05:00
Cameron Gutman 3d398ef6dd Update common 2015-02-27 14:22:35 -05:00
Cameron Gutman 85d95b2d8e Revert "Immediately show the PC as offline if the first poll fails"
This reverts commit 7b12fd1ad2.
2015-02-27 13:52:17 -05:00
Cameron Gutman d091d9db6b Start apps by ID to work correctly with duplicate app names 2015-02-27 13:42:40 -05:00
Cameron Gutman e081ab5239 Code cleanup and Lint suggestions 2015-02-27 01:43:24 -05:00
Cameron Gutman 7b12fd1ad2 Immediately show the PC as offline if the first poll fails 2015-02-27 01:33:33 -05:00
Cameron Gutman 80d8c5953e Rewrite the app art caching and fetching (again!) to finally address OOM problems and speed up art loading 2015-02-27 01:16:06 -05:00
Cameron Gutman 194037ff41 Clear the bitmap cache since it can get pretty large 2015-02-26 22:04:39 -05:00
Cameron Gutman 094d642739 Stop scaling bitmaps down 2015-02-26 22:04:22 -05:00
Cameron Gutman 010e03252e Encapsulate the cache IO streams in buffered streams 2015-02-26 21:39:26 -05:00
Cameron Gutman 98638186b5 Use weak references to allow the image views to be garbage collected while a load is in progress 2015-02-26 21:05:33 -05:00
Cameron Gutman c5293ef21f Reduce the size of the LRU cache by 2 2015-02-26 21:04:40 -05:00
Cameron Gutman 366a1c91b8 Always close the output stream 2015-02-26 21:04:17 -05:00
Cameron Gutman 157450e674 Update common with stricter applist parser 2015-02-26 18:33:18 -05:00
Cameron Gutman 1b8d2bc81c Cancel asset fetching when the app view is paused 2015-02-26 18:30:02 -05:00
Cameron Gutman f1787c43e5 Generalize the polling grace period to all users of CMS 2015-02-26 18:27:50 -05:00
Cameron Gutman 95ea88e932 Only replace the MAC address if the existing one is non-null 2015-02-26 15:11:46 -05:00
Cameron Gutman f2b8461bb9 Increment version to beta 3.1.2 2015-02-25 23:15:10 -05:00
Cameron Gutman 7838a787df Fix a bug in app removal 2015-02-25 22:27:38 -05:00
Cameron Gutman cc3f2ecb07 Always close the cache output stream if an exception occurs 2015-02-25 22:15:41 -05:00
Cameron Gutman 833b7c3916 Fetch app assets in the background while in the app view 2015-02-25 21:57:54 -05:00
Cameron Gutman 90209f2ca2 Update iml files generated by Android Studio 1.1 2015-02-25 21:07:59 -05:00
Cameron Gutman 2681036c32 Update common 2015-02-25 21:07:44 -05:00
Cameron Gutman ee58071ff1 Fix huge performance issues when dealing with large app lists 2015-02-25 21:07:35 -05:00
Cameron Gutman e222f2f6c3 Fix fast polling 2015-02-22 18:34:28 -05:00
Cameron Gutman 0b7becb161 Remove unused function 2015-02-22 18:10:08 -05:00
Cameron Gutman bf795ab7a5 Fix removal of apps in app list updates 2015-02-22 18:04:42 -05:00
Cameron Gutman 59df38ae8a Cancel app icon requests when the view is recycled 2015-02-22 17:49:52 -05:00
Cameron Gutman e04ff048b8 Implement a fast polling method to speed up polling. Save the old MAC address if it's empty. 2015-02-11 17:04:31 -05:00
Cameron Gutman 7d25d07c6d Update version to 3.1.1 2015-02-11 00:55:47 -05:00
Cameron Gutman 7b0ddfae42 Update iml files 2015-02-11 00:34:56 -05:00
Cameron Gutman 43fa1a7245 Update common to fix null app name issue 2015-02-09 00:12:29 -05:00
Cameron Gutman 057530eed0 Correctly identify computers that are the same 2015-02-08 23:46:03 -05:00
Cameron Gutman aee34f6365 Remove redundant null checks 2015-02-08 23:44:33 -05:00
Cameron Gutman 5519d92243 Disable the start key shortcut to start the keyboard because the keyboard can't receive input after it's started 2015-02-07 13:58:53 -05:00
Cameron Gutman 3d95ac1f93 Fix keyboard dismissal on Fire TV devices 2015-02-07 13:42:49 -05:00
Cameron Gutman 5c938535be Fix app list focus issues with remotes/gamepads 2015-02-07 13:20:01 -05:00
Cameron Gutman 2fdecc551a Tabs -> Spaces 2015-02-07 11:54:46 -05:00
Cameron Gutman 10204afdb4 Only add PCs to the computer list when they have been polled once to get a UUID for equality comparison. Fix equality comparison in PcView to avoid duplicate PCs enumerated over mDNS. 2015-02-07 11:44:56 -05:00
Cameron Gutman 55c800c2a5 Fade in box art when scrolling 2015-02-07 06:52:28 -05:00
Cameron Gutman 265b3f9963 Use image alpha to make images transparent while loading 2015-02-07 06:23:35 -05:00
Cameron Gutman a8bf2cd1cf Fix UI dropped frames when loading images 2015-02-07 06:08:00 -05:00
Cameron Gutman 4fcd8b3dfe Replace unpair option with delete PC 2015-02-07 05:57:30 -05:00
Cameron Gutman e1a1a6344d Fill the whole height with the list view 2015-02-06 13:38:32 -05:00
Cameron Gutman a095c10a25 Increment version to 3.1 and update build files 2015-02-05 16:15:16 -05:00
Cameron Gutman b1ea487e22 Use the mode (power) button on the Asus Nexus Player Gamepad as a select button 2015-02-05 16:06:55 -05:00
Cameron Gutman 47265d0d10 Add another SELinux policy change needed on Nexus 9 2015-02-05 16:06:22 -05:00
Cameron Gutman 6a41b41a38 Merge pull request #55 from Ansa89/italian-translation
Italian translation: update
2015-02-05 13:34:03 -05:00
Cameron Gutman 2247e43a48 Remove unused imports 2015-02-05 13:23:01 -05:00
Cameron Gutman d3986080a3 Tighten up a bunch of declarations to make Lint happier 2015-02-05 13:21:04 -05:00
Cameron Gutman 07277e1a5b Fix a few Lint warnings 2015-02-05 13:01:35 -05:00
Ansa89 39d7fc748f Italian translation: update 2015-02-03 01:06:36 +01:00
Cameron Gutman 4d3a69cf6a Fix GFE 2.1.x controller regression 2015-02-02 18:10:18 -05:00
Cameron Gutman b806522751 Unassign the controller number when a device is removed 2015-02-02 02:13:27 -05:00
Cameron Gutman 256fa897a7 Fix build issues 2015-02-01 18:31:34 -05:00
Cameron Gutman 5c812eed6c Beta 2 version update 2015-02-01 18:20:55 -05:00
Cameron Gutman f0b22f9119 Don't use small mode on TVs 2015-02-01 18:20:39 -05:00
Cameron Gutman 7e1884acb5 Trap Shield's back button as controller 0 2015-02-01 18:07:03 -05:00
Cameron Gutman 9512521783 Update common 2015-02-01 15:06:27 -05:00
Cameron Gutman da7904a767 Add multiple controller support 2015-02-01 15:06:18 -05:00
47 changed files with 1615 additions and 815 deletions

No files matched your search

+11 -13
View File
@@ -9,11 +9,12 @@
<facet type="android" name="Android"> <facet type="android" name="Android">
<configuration> <configuration>
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" /> <option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" /> <option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" /> <option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" /> <option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugAndroidTest" />
<option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" /> <option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" />
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugTestSources" /> <option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugAndroidTestSources" />
<option name="ALLOW_USER_CONFIGURATION" value="false" /> <option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" /> <option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" /> <option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
@@ -24,6 +25,7 @@
</component> </component>
<component name="NewModuleRootManager" inherit-compiler-output="false"> <component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" /> <output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/nonRoot/debug" />
<exclude-output /> <exclude-output />
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
@@ -39,12 +41,12 @@
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/java" isTestSource="false" /> <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/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/test/nonRoot/debug" isTestSource="true" generated="true" /> <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/test/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/test/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/test/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/test/nonRoot/debug" type="java-test-resource" /> <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/test/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$/src/nonRoot/res" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
@@ -101,17 +103,13 @@
<excludeFolder url="file://$MODULE_DIR$/build/outputs" /> <excludeFolder url="file://$MODULE_DIR$/build/outputs" />
<excludeFolder url="file://$MODULE_DIR$/build/tmp" /> <excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content> </content>
<orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" /> <orderEntry type="jdk" jdkName="Android API 22 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" /> <orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="androidasync-2.0.5" level="project" />
<orderEntry type="library" exported="" name="gson-2.3.1" level="project" />
<orderEntry type="library" exported="" name="support-v4-r7" level="project" />
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" /> <orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" /> <orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="tinyrtsp" level="project" /> <orderEntry type="library" exported="" name="tinyrtsp" level="project" />
<orderEntry type="library" exported="" name="limelight-common" level="project" /> <orderEntry type="library" exported="" name="limelight-common" level="project" />
<orderEntry type="library" exported="" name="ion-2.0.5" level="project" />
<orderEntry type="library" exported="" name="okhttp-2.2.0" 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="jcodec-0.1.9" level="project" />
<orderEntry type="library" exported="" name="okio-1.2.0" level="project" /> <orderEntry type="library" exported="" name="okio-1.2.0" level="project" />
+9 -13
View File
@@ -4,15 +4,15 @@ import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
compileSdkVersion 21 compileSdkVersion 22
buildToolsVersion "21.1.2" buildToolsVersion "21.1.2"
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 21 targetSdkVersion 22
versionName "3.1-beta1" versionName "3.1.3"
versionCode = 50 versionCode = 58
} }
productFlavors { productFlavors {
@@ -62,17 +62,13 @@ android {
} }
dependencies { dependencies {
compile group: 'org.jcodec', name: 'jcodec', version: '+' compile group: 'org.jcodec', name: 'jcodec', version: '0.1.9'
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '+' compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '+' compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
compile group: 'com.google.android', name: 'support-v4', version:'+' compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.2.0'
compile group: 'com.koushikdutta.ion', name: 'ion', version:'+' compile group: 'com.squareup.okio', name:'okio', version:'1.2.0'
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/jmdns-fixed.jar')
compile files('libs/limelight-common.jar') compile files('libs/limelight-common.jar')
Binary file not shown.
+119 -129
View File
@@ -1,9 +1,6 @@
package com.limelight; package com.limelight;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader; import java.io.StringReader;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
@@ -11,10 +8,7 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.UUID; import java.util.UUID;
import org.xmlpull.v1.XmlPullParserException;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.binding.crypto.AndroidCryptoProvider;
import com.limelight.computers.ComputerManagerListener; import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService; import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.AppGridAdapter; import com.limelight.grid.AppGridAdapter;
@@ -27,6 +21,7 @@ import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks; import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.CacheHelper; import com.limelight.utils.CacheHelper;
import com.limelight.utils.Dialog; import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.SpinnerDialog; import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper; import com.limelight.utils.UiHelper;
@@ -48,7 +43,6 @@ import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AbsListView; import android.widget.AbsListView;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.AdapterContextMenuInfo;
@@ -61,9 +55,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
private ComputerManagerService.ApplistPoller poller; private ComputerManagerService.ApplistPoller poller;
private SpinnerDialog blockingLoadSpinner; private SpinnerDialog blockingLoadSpinner;
private String lastRawApplist; private String lastRawApplist;
private int lastRunningAppId;
private int consecutiveAppListFailures = 0; private boolean suspendGridUpdates;
private final static int CONSECUTIVE_FAILURE_LIMIT = 3;
private final static int START_OR_RESUME_ID = 1; private final static int START_OR_RESUME_ID = 1;
private final static int QUIT_ID = 2; private final static int QUIT_ID = 2;
@@ -74,7 +67,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
public final static String UUID_EXTRA = "UUID"; public final static String UUID_EXTRA = "UUID";
private ComputerManagerService.ComputerManagerBinder managerBinder; private ComputerManagerService.ComputerManagerBinder managerBinder;
private ServiceConnection serviceConnection = new ServiceConnection() { private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) { public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder = final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder); ((ComputerManagerService.ComputerManagerBinder)binder);
@@ -121,11 +114,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
} }
}; };
private InetAddress getAddress() {
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp : computer.remoteIp;
}
private void startComputerUpdates() { private void startComputerUpdates() {
if (managerBinder == null) { if (managerBinder == null) {
return; return;
@@ -134,15 +122,17 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
managerBinder.startPolling(new ComputerManagerListener() { managerBinder.startPolling(new ComputerManagerListener() {
@Override @Override
public void notifyComputerUpdated(ComputerDetails details) { public void notifyComputerUpdated(ComputerDetails details) {
// Don't care about other computers // Do nothing if updates are suspended
if (details != computer) { if (suspendGridUpdates) {
return; return;
} }
if (details.state != ComputerDetails.State.ONLINE) { // Don't care about other computers
consecutiveAppListFailures++; if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
return;
}
if (consecutiveAppListFailures >= CONSECUTIVE_FAILURE_LIMIT) { if (details.state == ComputerDetails.State.OFFLINE) {
// The PC is unreachable now // The PC is unreachable now
AppView.this.runOnUiThread(new Runnable() { AppView.this.runOnUiThread(new Runnable() {
@Override @Override
@@ -152,27 +142,34 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
finish(); finish();
} }
}); });
}
return; return;
} }
consecutiveAppListFailures = 0; // App list is the same or empty
// App list is the same or empty; nothing to do
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) { 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; return;
} }
lastRunningAppId = details.runningGameId;
lastRawApplist = details.rawAppList;
try { try {
lastRawApplist = details.rawAppList;
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList))); updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
if (blockingLoadSpinner != null) { if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss(); blockingLoadSpinner.dismiss();
blockingLoadSpinner = null; blockingLoadSpinner = null;
} }
} catch (Exception e) {} } catch (Exception ignored) {}
} }
}); });
@@ -190,6 +187,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
if (managerBinder != null) { if (managerBinder != null) {
managerBinder.stopPolling(); managerBinder.stopPolling();
} }
if (appGridAdapter != null) {
appGridAdapter.cancelQueuedOperations();
}
} }
@Override @Override
@@ -272,10 +273,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
int runningAppId = -1; int runningAppId = -1;
for (int i = 0; i < appGridAdapter.getCount(); i++) { for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject app = (AppObject) appGridAdapter.getItem(i); AppObject app = (AppObject) appGridAdapter.getItem(i);
if (app.app == null) {
continue;
}
if (app.app.getIsRunning()) { if (app.app.getIsRunning()) {
runningAppId = app.app.getAppId(); runningAppId = app.app.getAppId();
break; break;
@@ -290,10 +287,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position); AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
if (selectedApp == null || selectedApp.app == null) {
return;
}
int runningAppId = getRunningAppId(); int runningAppId = getRunningAppId();
if (runningAppId != -1) { if (runningAppId != -1) {
if (runningAppId == selectedApp.app.getAppId()) { if (runningAppId == selectedApp.app.getAppId()) {
@@ -311,33 +304,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
public void onContextMenuClosed(Menu menu) { public void onContextMenuClosed(Menu menu) {
} }
private void displayQuitConfirmationDialog(final Runnable onYes, final Runnable onNo) {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which){
case DialogInterface.BUTTON_POSITIVE:
if (onYes != null) {
onYes.run();
}
break;
case DialogInterface.BUTTON_NEGATIVE:
if (onNo != null) {
onNo.run();
}
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(getResources().getString(R.string.applist_quit_confirmation))
.setPositiveButton(getResources().getString(R.string.yes), dialogClickListener)
.setNegativeButton(getResources().getString(R.string.no), dialogClickListener)
.show();
}
@Override @Override
public boolean onContextItemSelected(MenuItem item) { public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
@@ -345,25 +311,37 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
switch (item.getItemId()) { switch (item.getItemId()) {
case START_WTIH_QUIT: case START_WTIH_QUIT:
// Display a confirmation dialog first // Display a confirmation dialog first
displayQuitConfirmationDialog(new Runnable() { UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override @Override
public void run() { public void run() {
doStart(app.app); ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
} }
}, null); }, null);
return true; return true;
case START_OR_RESUME_ID: case START_OR_RESUME_ID:
// Resume is the same as start for us // Resume is the same as start for us
doStart(app.app); ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
return true; return true;
case QUIT_ID: case QUIT_ID:
// Display a confirmation dialog first // Display a confirmation dialog first
displayQuitConfirmationDialog(new Runnable() { UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override @Override
public void run() { public void run() {
doQuit(app.app); 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();
}
}
});
} }
}, null); }, null);
return true; return true;
@@ -376,26 +354,61 @@ 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) { private void updateUiWithAppList(final List<NvApp> appList) {
AppView.this.runOnUiThread(new Runnable() { AppView.this.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
boolean updated = false; boolean updated = false;
// First handle app updates and additions
for (NvApp app : appList) { for (NvApp app : appList) {
boolean foundExistingApp = false; boolean foundExistingApp = false;
// Try to update an existing app in the list first // Try to update an existing app in the list first
for (int i = 0; i < appGridAdapter.getCount(); i++) { for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject existingApp = (AppObject) appGridAdapter.getItem(i); AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
if (existingApp.app == null) {
continue;
}
if (existingApp.app.getAppId() == app.getAppId()) { if (existingApp.app.getAppId() == app.getAppId()) {
// Found the app; update its properties // Found the app; update its properties
if (existingApp.app.getIsRunning() != app.getIsRunning()) { if (existingApp.app.getIsRunning() != app.getIsRunning()) {
existingApp.app.setIsRunningBoolean(app.getIsRunning()); existingApp.app.setIsRunning(app.getIsRunning());
updated = true; updated = true;
} }
if (!existingApp.app.getAppName().equals(app.getAppName())) { if (!existingApp.app.getAppName().equals(app.getAppName())) {
@@ -415,6 +428,34 @@ 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) { if (updated) {
appGridAdapter.notifyDataSetChanged(); appGridAdapter.notifyDataSetChanged();
} }
@@ -422,58 +463,6 @@ 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 @Override
public int getAdapterFragmentLayoutId() { public int getAdapterFragmentLayoutId() {
return PreferenceConfiguration.readPreferences(this).listMode ? return PreferenceConfiguration.readPreferences(this).listMode ?
@@ -489,25 +478,26 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
public void onItemClick(AdapterView<?> arg0, View arg1, int pos, public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) { long id) {
AppObject app = (AppObject) appGridAdapter.getItem(pos); 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 // Only open the context menu if something is running, otherwise start it
if (getRunningAppId() != -1) { if (getRunningAppId() != -1) {
openContextMenu(arg1); openContextMenu(arg1);
} else { } else {
doStart(app.app); ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
} }
} }
}); });
registerForContextMenu(listView); registerForContextMenu(listView);
listView.requestFocus();
} }
public class AppObject { public class AppObject {
public NvApp app; public final NvApp app;
public AppObject(NvApp app) { public AppObject(NvApp app) {
if (app == null) {
throw new IllegalArgumentException("app must not be null");
}
this.app = app; this.app = app;
} }
+23 -8
View File
@@ -13,6 +13,7 @@ import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.NvConnectionListener; import com.limelight.nvstream.NvConnectionListener;
import com.limelight.nvstream.StreamConfiguration; import com.limelight.nvstream.StreamConfiguration;
import com.limelight.nvstream.av.video.VideoDecoderRenderer; import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.input.KeyboardPacket; import com.limelight.nvstream.input.KeyboardPacket;
import com.limelight.nvstream.input.MouseButtonPacket; import com.limelight.nvstream.input.MouseButtonPacket;
import com.limelight.preferences.PreferenceConfiguration; import com.limelight.preferences.PreferenceConfiguration;
@@ -25,6 +26,7 @@ import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.Point; import android.graphics.Point;
import android.hardware.input.InputManager;
import android.media.AudioManager; import android.media.AudioManager;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.wifi.WifiManager; import android.net.wifi.WifiManager;
@@ -59,7 +61,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private int lastButtonState = 0; private int lastButtonState = 0;
// Only 2 touches are supported // Only 2 touches are supported
private TouchContext[] touchContextMap = new TouchContext[2]; private final TouchContext[] touchContextMap = new TouchContext[2];
private long threeFingerDownTime = 0; private long threeFingerDownTime = 0;
private static final int THREE_FINGER_TAP_THRESHOLD = 300; private static final int THREE_FINGER_TAP_THRESHOLD = 300;
@@ -68,7 +70,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private KeyboardTranslator keybTranslator; private KeyboardTranslator keybTranslator;
private PreferenceConfiguration prefConfig; private PreferenceConfiguration prefConfig;
private Point screenSize = new Point(0, 0); private final Point screenSize = new Point(0, 0);
private NvConnection conn; private NvConnection conn;
private SpinnerDialog spinner; private SpinnerDialog spinner;
@@ -88,7 +90,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private int drFlags = 0; private int drFlags = 0;
public static final String EXTRA_HOST = "Host"; public static final String EXTRA_HOST = "Host";
public static final String EXTRA_APP = "App"; public static final String EXTRA_APP_NAME = "AppName";
public static final String EXTRA_APP_ID = "AppId";
public static final String EXTRA_UNIQUEID = "UniqueId"; public static final String EXTRA_UNIQUEID = "UniqueId";
public static final String EXTRA_STREAMING_REMOTE = "Remote"; public static final String EXTRA_STREAMING_REMOTE = "Remote";
@@ -170,17 +173,23 @@ public class Game extends Activity implements SurfaceHolder.Callback,
wifiLock.acquire(); wifiLock.acquire();
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST); String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
String app = Game.this.getIntent().getStringExtra(EXTRA_APP); String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME);
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID); String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false); boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
if (appId == StreamConfiguration.INVALID_APP_ID) {
finish();
return;
}
decoderRenderer = new ConfigurableDecoderRenderer(); decoderRenderer = new ConfigurableDecoderRenderer();
decoderRenderer.initializeWithFlags(drFlags); decoderRenderer.initializeWithFlags(drFlags);
StreamConfiguration config = new StreamConfiguration.Builder() StreamConfiguration config = new StreamConfiguration.Builder()
.setResolution(prefConfig.width, prefConfig.height) .setResolution(prefConfig.width, prefConfig.height)
.setRefreshRate(prefConfig.fps) .setRefreshRate(prefConfig.fps)
.setApp(app) .setApp(new NvApp(appName, appId))
.setBitrate(prefConfig.bitrate * 1000) .setBitrate(prefConfig.bitrate * 1000)
.setEnableSops(prefConfig.enableSops) .setEnableSops(prefConfig.enableSops)
.enableAdaptiveResolution((decoderRenderer.getCapabilities() & .enableAdaptiveResolution((decoderRenderer.getCapabilities() &
@@ -193,7 +202,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Initialize the connection // Initialize the connection
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this)); conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
keybTranslator = new KeyboardTranslator(conn); keybTranslator = new KeyboardTranslator(conn);
controllerHandler = new ControllerHandler(conn, this, prefConfig.deadzonePercentage); controllerHandler = new ControllerHandler(conn, this, prefConfig.multiController, prefConfig.deadzonePercentage);
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
inputManager.registerInputDeviceListener(controllerHandler, null);
SurfaceHolder sh = sv.getHolder(); SurfaceHolder sh = sv.getHolder();
if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) { if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) {
@@ -242,7 +254,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
} }
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
private Runnable hideSystemUi = new Runnable() { private final Runnable hideSystemUi = new Runnable() {
@Override @Override
public void run() { public void run() {
// Use immersive mode on 4.4+ or standard low profile on previous builds // Use immersive mode on 4.4+ or standard low profile on previous builds
@@ -278,6 +290,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
SpinnerDialog.closeDialogs(this); SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs(); Dialog.closeDialogs();
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
inputManager.unregisterInputDeviceListener(controllerHandler);
displayedFailureDialog = true; displayedFailureDialog = true;
stopConnection(); stopConnection();
@@ -308,7 +323,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
wifiLock.release(); wifiLock.release();
} }
private Runnable toggleGrab = new Runnable() { private final Runnable toggleGrab = new Runnable() {
@Override @Override
public void run() { public void run() {
+54 -10
View File
@@ -12,6 +12,7 @@ import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService; import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.PcGridAdapter; import com.limelight.grid.PcGridAdapter;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.http.PairingManager; import com.limelight.nvstream.http.PairingManager;
import com.limelight.nvstream.http.PairingManager.PairState; import com.limelight.nvstream.http.PairingManager.PairState;
@@ -22,10 +23,10 @@ import com.limelight.preferences.StreamSettings;
import com.limelight.ui.AdapterFragment; import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks; import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.Dialog; import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.UiHelper; import com.limelight.utils.UiHelper;
import android.app.Activity; import android.app.Activity;
import android.app.FragmentTransaction;
import android.app.Service; import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Intent; import android.content.Intent;
@@ -53,7 +54,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
private PcGridAdapter pcGridAdapter; private PcGridAdapter pcGridAdapter;
private ComputerManagerService.ComputerManagerBinder managerBinder; private ComputerManagerService.ComputerManagerBinder managerBinder;
private boolean freezeUpdates, runningPolling; private boolean freezeUpdates, runningPolling;
private ServiceConnection serviceConnection = new ServiceConnection() { private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) { public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder = final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder); ((ComputerManagerService.ComputerManagerBinder)binder);
@@ -94,6 +95,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
private final static int UNPAIR_ID = 3; private final static int UNPAIR_ID = 3;
private final static int WOL_ID = 4; private final static int WOL_ID = 4;
private final static int DELETE_ID = 5; private final static int DELETE_ID = 5;
private final static int RESUME_ID = 6;
private final static int QUIT_ID = 7;
private void initializeViews() { private void initializeViews() {
setContentView(R.layout.activity_pc_view); setContentView(R.layout.activity_pc_view);
@@ -238,8 +241,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position); ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
if (computer == null || computer.details == null || if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
startComputerUpdates(); startComputerUpdates();
return; return;
} }
@@ -254,8 +256,16 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc)); menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
} }
else { else {
menu.add(Menu.NONE, APP_LIST_ID, 1, getResources().getString(R.string.pcview_menu_app_list)); if (computer.details.runningGameId != 0) {
menu.add(Menu.NONE, UNPAIR_ID, 2, getResources().getString(R.string.pcview_menu_unpair_pc)); 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));
} }
} }
@@ -476,9 +486,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
@Override @Override
public boolean onContextItemSelected(MenuItem item) { public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position); final ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
switch (item.getItemId()) switch (item.getItemId()) {
{
case PAIR_ID: case PAIR_ID:
doPair(computer.details); doPair(computer.details);
return true; return true;
@@ -504,6 +513,32 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
doAppList(computer.details); doAppList(computer.details);
return true; 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: default:
return super.onContextItemSelected(item); return super.onContextItemSelected(item);
} }
@@ -516,6 +551,12 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
if (details.equals(computer.details)) { if (details.equals(computer.details)) {
pcGridAdapter.removeComputer(computer); pcGridAdapter.removeComputer(computer);
pcGridAdapter.notifyDataSetChanged(); pcGridAdapter.notifyDataSetChanged();
if (pcGridAdapter.getCount() == 0) {
// Show the "Discovery in progress" view
noPcFoundLayout.setVisibility(View.VISIBLE);
}
break; break;
} }
} }
@@ -528,7 +569,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i); ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
// Check if this is the same computer // Check if this is the same computer
if (details.equals(computer.details)) { if (details.uuid.equals(computer.details.uuid)) {
existingEntry = computer; existingEntry = computer;
break; break;
} }
@@ -585,6 +626,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
public ComputerDetails details; public ComputerDetails details;
public ComputerObject(ComputerDetails details) { public ComputerObject(ComputerDetails details) {
if (details == null) {
throw new IllegalArgumentException("details must not be null");
}
this.details = details; this.details = details;
} }
@@ -9,7 +9,7 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
public class AndroidAudioRenderer implements AudioRenderer { public class AndroidAudioRenderer implements AudioRenderer {
public static final int FRAME_SIZE = 960; private static final int FRAME_SIZE = 960;
private AudioTrack track; private AudioTrack track;
@@ -45,8 +45,8 @@ import com.limelight.nvstream.http.LimelightCryptoProvider;
public class AndroidCryptoProvider implements LimelightCryptoProvider { public class AndroidCryptoProvider implements LimelightCryptoProvider {
private File certFile; private final File certFile;
private File keyFile; private final File keyFile;
private X509Certificate cert; private X509Certificate cert;
private RSAPrivateKey key; private RSAPrivateKey key;
@@ -1,7 +1,9 @@
package com.limelight.binding.input; package com.limelight.binding.input;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map;
import android.hardware.input.InputManager;
import android.os.SystemClock; import android.os.SystemClock;
import android.view.InputDevice; import android.view.InputDevice;
import android.view.KeyEvent; import android.view.KeyEvent;
@@ -13,26 +15,10 @@ import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.ui.GameGestures; import com.limelight.ui.GameGestures;
import com.limelight.utils.Vector2d; import com.limelight.utils.Vector2d;
public class ControllerHandler { public class ControllerHandler implements InputManager.InputDeviceListener {
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 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 START_DOWN_TIME_KEYB_MS = 750;
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25; private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
@@ -43,19 +29,23 @@ public class ControllerHandler {
private static final int EMULATED_SPECIAL_UP_DELAY_MS = 100; private static final int EMULATED_SPECIAL_UP_DELAY_MS = 100;
private static final int EMULATED_SELECT_UP_DELAY_MS = 30; private static final int EMULATED_SELECT_UP_DELAY_MS = 30;
private Vector2d inputVector = new Vector2d(); private final Vector2d inputVector = new Vector2d();
private HashMap<String, ControllerMapping> mappings = new HashMap<String, ControllerMapping>(); private final HashMap<String, ControllerContext> contexts = new HashMap<String, ControllerContext>();
private NvConnection conn; private final NvConnection conn;
private double stickDeadzone; private final double stickDeadzone;
private final ControllerMapping defaultMapping = new ControllerMapping(); private final ControllerContext defaultContext = new ControllerContext();
private GameGestures gestures; private final GameGestures gestures;
private boolean hasGameController; private boolean hasGameController;
public ControllerHandler(NvConnection conn, GameGestures gestures, int deadzonePercentage) { private final boolean multiControllerEnabled;
private short currentControllers;
public ControllerHandler(NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
this.conn = conn; this.conn = conn;
this.gestures = gestures; this.gestures = gestures;
this.multiControllerEnabled = multiControllerEnabled;
// HACK: For now we're hardcoding a 10% deadzone. Some deadzone // HACK: For now we're hardcoding a 10% deadzone. Some deadzone
// is required for controller batching support to work. // is required for controller batching support to work.
@@ -82,15 +72,16 @@ public class ControllerHandler {
this.stickDeadzone = (double)deadzonePercentage / 100.0; this.stickDeadzone = (double)deadzonePercentage / 100.0;
// Initialize the default mapping for events with no device // Initialize the default context for events with no device
defaultMapping.leftStickXAxis = MotionEvent.AXIS_X; defaultContext.leftStickXAxis = MotionEvent.AXIS_X;
defaultMapping.leftStickYAxis = MotionEvent.AXIS_Y; defaultContext.leftStickYAxis = MotionEvent.AXIS_Y;
defaultMapping.leftStickDeadzoneRadius = (float) stickDeadzone; defaultContext.leftStickDeadzoneRadius = (float) stickDeadzone;
defaultMapping.rightStickXAxis = MotionEvent.AXIS_Z; defaultContext.rightStickXAxis = MotionEvent.AXIS_Z;
defaultMapping.rightStickYAxis = MotionEvent.AXIS_RZ; defaultContext.rightStickYAxis = MotionEvent.AXIS_RZ;
defaultMapping.rightStickDeadzoneRadius = (float) stickDeadzone; defaultContext.rightStickDeadzoneRadius = (float) stickDeadzone;
defaultMapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE; defaultContext.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
defaultMapping.rightTriggerAxis = MotionEvent.AXIS_GAS; defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
defaultContext.controllerNumber = (short) 0;
} }
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) { private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
@@ -106,19 +97,63 @@ public class ControllerHandler {
return range; return range;
} }
private ControllerMapping createMappingForDevice(InputDevice dev) { private short assignNewControllerNumber() {
ControllerMapping mapping = new ControllerMapping(); for (short i = 0; i < 4; i++) {
if ((currentControllers & (1 << i)) == 0) {
// Found an unused controller value
currentControllers |= (1 << i);
return i;
}
}
return 0;
}
@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().controllerNumber);
contexts.remove(device.getKey());
return;
}
}
}
@Override
public void onInputDeviceChanged(int deviceId) {
// Remove and re-add
onInputDeviceRemoved(deviceId);
onInputDeviceAdded(deviceId);
}
private void releaseControllerNumber(int controllerNumber) {
LimeLog.info("Controller number "+controllerNumber+" is now available");
currentControllers &= ~(1 << controllerNumber);
}
private ControllerContext createContextForDevice(InputDevice dev) {
ControllerContext context = new ControllerContext();
String devName = dev.getName(); String devName = dev.getName();
LimeLog.info("Creating controller mapping for device: "+devName); LimeLog.info("Creating controller context for device: "+devName);
mapping.leftStickXAxis = MotionEvent.AXIS_X; context.name = devName;
mapping.leftStickYAxis = MotionEvent.AXIS_Y; context.id = dev.getId();
if (getMotionRangeForJoystickAxis(dev, mapping.leftStickXAxis) != null &&
getMotionRangeForJoystickAxis(dev, mapping.leftStickYAxis) != null) { context.leftStickXAxis = MotionEvent.AXIS_X;
context.leftStickYAxis = MotionEvent.AXIS_Y;
if (getMotionRangeForJoystickAxis(dev, context.leftStickXAxis) != null &&
getMotionRangeForJoystickAxis(dev, context.leftStickYAxis) != null) {
// This is a gamepad // This is a gamepad
hasGameController = true; hasGameController = true;
mapping.hasJoystickAxes = true; context.hasJoystickAxes = true;
} }
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER); InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
@@ -128,14 +163,14 @@ public class ControllerHandler {
if (leftTriggerRange != null && rightTriggerRange != null) if (leftTriggerRange != null && rightTriggerRange != null)
{ {
// Some controllers use LTRIGGER and RTRIGGER (like Ouya) // Some controllers use LTRIGGER and RTRIGGER (like Ouya)
mapping.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER; context.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER;
mapping.rightTriggerAxis = MotionEvent.AXIS_RTRIGGER; context.rightTriggerAxis = MotionEvent.AXIS_RTRIGGER;
} }
else if (brakeRange != null && gasRange != null) else if (brakeRange != null && gasRange != null)
{ {
// Others use GAS and BRAKE (like Moga) // Others use GAS and BRAKE (like Moga)
mapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE; context.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
mapping.rightTriggerAxis = MotionEvent.AXIS_GAS; context.rightTriggerAxis = MotionEvent.AXIS_GAS;
} }
else else
{ {
@@ -144,34 +179,34 @@ public class ControllerHandler {
if (rxRange != null && ryRange != null && devName != null) { if (rxRange != null && ryRange != null && devName != null) {
if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) { if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) {
// Xbox controllers use RX and RY for right stick // Xbox controllers use RX and RY for right stick
mapping.rightStickXAxis = MotionEvent.AXIS_RX; context.rightStickXAxis = MotionEvent.AXIS_RX;
mapping.rightStickYAxis = MotionEvent.AXIS_RY; context.rightStickYAxis = MotionEvent.AXIS_RY;
// Xbox controllers use Z and RZ for triggers // Xbox controllers use Z and RZ for triggers
mapping.leftTriggerAxis = MotionEvent.AXIS_Z; context.leftTriggerAxis = MotionEvent.AXIS_Z;
mapping.rightTriggerAxis = MotionEvent.AXIS_RZ; context.rightTriggerAxis = MotionEvent.AXIS_RZ;
mapping.triggersIdleNegative = true; context.triggersIdleNegative = true;
mapping.isXboxController = true; context.isXboxController = true;
} }
else { else {
// DS4 controller uses RX and RY for triggers // DS4 controller uses RX and RY for triggers
mapping.leftTriggerAxis = MotionEvent.AXIS_RX; context.leftTriggerAxis = MotionEvent.AXIS_RX;
mapping.rightTriggerAxis = MotionEvent.AXIS_RY; context.rightTriggerAxis = MotionEvent.AXIS_RY;
mapping.triggersIdleNegative = true; context.triggersIdleNegative = true;
mapping.isDualShock4 = true; context.isDualShock4 = true;
} }
} }
} }
if (mapping.rightStickXAxis == -1 && mapping.rightStickYAxis == -1) { if (context.rightStickXAxis == -1 && context.rightStickYAxis == -1) {
InputDevice.MotionRange zRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z); InputDevice.MotionRange zRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z);
InputDevice.MotionRange rzRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ); InputDevice.MotionRange rzRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ);
// Most other controllers use Z and RZ for the right stick // Most other controllers use Z and RZ for the right stick
if (zRange != null && rzRange != null) { if (zRange != null && rzRange != null) {
mapping.rightStickXAxis = MotionEvent.AXIS_Z; context.rightStickXAxis = MotionEvent.AXIS_Z;
mapping.rightStickYAxis = MotionEvent.AXIS_RZ; context.rightStickYAxis = MotionEvent.AXIS_RZ;
} }
else { else {
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX); InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
@@ -179,8 +214,8 @@ public class ControllerHandler {
// Try RX and RY now // Try RX and RY now
if (rxRange != null && ryRange != null) { if (rxRange != null && ryRange != null) {
mapping.rightStickXAxis = MotionEvent.AXIS_RX; context.rightStickXAxis = MotionEvent.AXIS_RX;
mapping.rightStickYAxis = MotionEvent.AXIS_RY; context.rightStickYAxis = MotionEvent.AXIS_RY;
} }
} }
} }
@@ -189,30 +224,30 @@ public class ControllerHandler {
InputDevice.MotionRange hatXRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_X); InputDevice.MotionRange hatXRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_X);
InputDevice.MotionRange hatYRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_Y); InputDevice.MotionRange hatYRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_Y);
if (hatXRange != null && hatYRange != null) { if (hatXRange != null && hatYRange != null) {
mapping.hatXAxis = MotionEvent.AXIS_HAT_X; context.hatXAxis = MotionEvent.AXIS_HAT_X;
mapping.hatYAxis = MotionEvent.AXIS_HAT_Y; context.hatYAxis = MotionEvent.AXIS_HAT_Y;
} }
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) { if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
mapping.leftStickDeadzoneRadius = (float) stickDeadzone; context.leftStickDeadzoneRadius = (float) stickDeadzone;
} }
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) { if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
mapping.rightStickDeadzoneRadius = (float) stickDeadzone; context.rightStickDeadzoneRadius = (float) stickDeadzone;
} }
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) { if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, mapping.leftTriggerAxis); InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, context.leftTriggerAxis);
InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, mapping.rightTriggerAxis); InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, context.rightTriggerAxis);
// It's important to have a valid deadzone so controller packet batching works properly // It's important to have a valid deadzone so controller packet batching works properly
mapping.triggerDeadzone = Math.max(Math.abs(ltRange.getFlat()), Math.abs(rtRange.getFlat())); context.triggerDeadzone = Math.max(Math.abs(ltRange.getFlat()), Math.abs(rtRange.getFlat()));
// For triggers without (valid) deadzones, we'll use 13% (around XInput's default) // For triggers without (valid) deadzones, we'll use 13% (around XInput's default)
if (mapping.triggerDeadzone < 0.13f || if (context.triggerDeadzone < 0.13f ||
mapping.triggerDeadzone > 0.30f) context.triggerDeadzone > 0.30f)
{ {
mapping.triggerDeadzone = 0.13f; context.triggerDeadzone = 0.13f;
} }
} }
@@ -226,71 +261,86 @@ public class ControllerHandler {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { 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); boolean[] hasStartKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_MENU, 0);
if (!hasStartKey[0] && !hasStartKey[1]) { if (!hasStartKey[0] && !hasStartKey[1]) {
mapping.backIsStart = true; context.backIsStart = true;
context.modeIsSelect = true;
} }
} }
// The ASUS Gamepad has triggers that sit far forward and are prone to false presses // 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 // so we increase the deadzone on them to minimize this
mapping.triggerDeadzone = 0.30f; context.triggerDeadzone = 0.30f;
} }
// Classify this device as a remote by name // Classify this device as a remote by name
else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) { else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) {
// It's only a remote if it doesn't any sticks // It's only a remote if it doesn't any sticks
if (!mapping.hasJoystickAxes) { if (!context.hasJoystickAxes) {
mapping.isRemote = true; context.isRemote = true;
} }
} }
// NYKO Playpad has a fake hat that mimics the left stick for some reason // SHIELD controllers will use small stick deadzones
else if (devName.contains("NYKO PLAYPAD")) { else if (devName.contains("SHIELD")) {
mapping.hatXAxis = -1; context.leftStickDeadzoneRadius = 0.07f;
mapping.hatYAxis = -1; context.rightStickDeadzoneRadius = 0.07f;
} }
} }
LimeLog.info("Analog stick deadzone: "+mapping.leftStickDeadzoneRadius+" "+mapping.rightStickDeadzoneRadius); LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius);
LimeLog.info("Trigger deadzone: "+mapping.triggerDeadzone); LimeLog.info("Trigger deadzone: "+context.triggerDeadzone);
return mapping; if (devName != null && devName.equals("gpio-keys")) {
// This is the back button on Shield portable consoles
context.controllerNumber = 0;
}
else if (multiControllerEnabled && context.hasJoystickAxes) {
context.controllerNumber = assignNewControllerNumber();
}
else {
context.controllerNumber = 0;
}
LimeLog.info("Assigned as controller "+context.controllerNumber);
return context;
} }
private ControllerMapping getMappingForDevice(InputDevice dev) { private ControllerContext getContextForDevice(InputDevice dev) {
// Unknown devices use the default mapping // Unknown devices use the default context
if (dev == null) { if (dev == null) {
return defaultMapping; return defaultContext;
} }
String descriptor = dev.getDescriptor(); String descriptor = dev.getDescriptor();
// Return the existing mapping if it exists // Return the existing context if it exists
ControllerMapping mapping = mappings.get(descriptor); ControllerContext context = contexts.get(descriptor);
if (mapping != null) { if (context != null) {
return mapping; return context;
} }
// Otherwise create a new mapping // Otherwise create a new context
mapping = createMappingForDevice(dev); context = createContextForDevice(dev);
mappings.put(descriptor, mapping); contexts.put(descriptor, context);
return mapping; return context;
} }
private void sendControllerInputPacket() { private void sendControllerInputPacket(ControllerContext context) {
conn.sendControllerInput(inputMap, leftTrigger, rightTrigger, conn.sendControllerInput(context.controllerNumber, context.inputMap,
leftStickX, leftStickY, rightStickX, rightStickY); context.leftTrigger, context.rightTrigger,
context.leftStickX, context.leftStickY,
context.rightStickX, context.rightStickY);
} }
// Return a valid keycode, 0 to consume, or -1 to not consume the event // Return a valid keycode, 0 to consume, or -1 to not consume the event
// Device MAY BE NULL // Device MAY BE NULL
private int handleRemapping(ControllerMapping mapping, KeyEvent event) { private int handleRemapping(ControllerContext context, KeyEvent event) {
// For remotes, don't capture the back button // For remotes, don't capture the back button
if (mapping.isRemote) { if (context.isRemote) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
return -1; return -1;
} }
} }
if (mapping.isDualShock4) { if (context.isDualShock4) {
switch (event.getKeyCode()) { switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_BUTTON_Y: case KeyEvent.KEYCODE_BUTTON_Y:
return KeyEvent.KEYCODE_BUTTON_L1; return KeyEvent.KEYCODE_BUTTON_L1;
@@ -329,7 +379,7 @@ public class ControllerHandler {
} }
} }
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) { if (context.hatXAxis != -1 && context.hatYAxis != -1) {
switch (event.getKeyCode()) { switch (event.getKeyCode()) {
// These are duplicate dpad events for hat input // These are duplicate dpad events for hat input
case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_LEFT:
@@ -340,9 +390,9 @@ public class ControllerHandler {
return 0; return 0;
} }
} }
else if (mapping.hatXAxis == -1 && else if (context.hatXAxis == -1 &&
mapping.hatYAxis == -1 && context.hatYAxis == -1 &&
mapping.isXboxController && context.isXboxController &&
event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) { event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
// If there's not a proper Xbox controller mapping, we'll translate the raw d-pad // If there's not a proper Xbox controller mapping, we'll translate the raw d-pad
// scan codes into proper key codes // scan codes into proper key codes
@@ -375,12 +425,20 @@ public class ControllerHandler {
if (keyCode == KeyEvent.KEYCODE_BUTTON_START || if (keyCode == KeyEvent.KEYCODE_BUTTON_START ||
keyCode == KeyEvent.KEYCODE_MENU) { keyCode == KeyEvent.KEYCODE_MENU) {
// Ensure that we never use back as start if we have a real start // Ensure that we never use back as start if we have a real start
mapping.backIsStart = false; context.backIsStart = false;
} }
else if (mapping.backIsStart && keyCode == KeyEvent.KEYCODE_BACK) { 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) {
// Emulate the start button with back // Emulate the start button with back
return KeyEvent.KEYCODE_BUTTON_START; 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; return keyCode;
} }
@@ -402,101 +460,101 @@ public class ControllerHandler {
// evaluates the deadzone. // evaluates the deadzone.
} }
private void handleAxisSet(ControllerMapping mapping, float lsX, float lsY, float rsX, private void handleAxisSet(ControllerContext context, float lsX, float lsY, float rsX,
float rsY, float lt, float rt, float hatX, float hatY) { float rsY, float lt, float rt, float hatX, float hatY) {
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) { if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
Vector2d leftStickVector = populateCachedVector(lsX, lsY); Vector2d leftStickVector = populateCachedVector(lsX, lsY);
handleDeadZone(leftStickVector, mapping.leftStickDeadzoneRadius); handleDeadZone(leftStickVector, context.leftStickDeadzoneRadius);
leftStickX = (short) (leftStickVector.getX() * 0x7FFE); context.leftStickX = (short) (leftStickVector.getX() * 0x7FFE);
leftStickY = (short) (-leftStickVector.getY() * 0x7FFE); context.leftStickY = (short) (-leftStickVector.getY() * 0x7FFE);
} }
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) { if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
Vector2d rightStickVector = populateCachedVector(rsX, rsY); Vector2d rightStickVector = populateCachedVector(rsX, rsY);
handleDeadZone(rightStickVector, mapping.rightStickDeadzoneRadius); handleDeadZone(rightStickVector, context.rightStickDeadzoneRadius);
rightStickX = (short) (rightStickVector.getX() * 0x7FFE); context.rightStickX = (short) (rightStickVector.getX() * 0x7FFE);
rightStickY = (short) (-rightStickVector.getY() * 0x7FFE); context.rightStickY = (short) (-rightStickVector.getY() * 0x7FFE);
} }
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) { if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
if (mapping.triggersIdleNegative) { if (context.triggersIdleNegative) {
lt = (lt + 1) / 2; lt = (lt + 1) / 2;
rt = (rt + 1) / 2; rt = (rt + 1) / 2;
} }
if (lt <= mapping.triggerDeadzone) { if (lt <= context.triggerDeadzone) {
lt = 0; lt = 0;
} }
if (rt <= mapping.triggerDeadzone) { if (rt <= context.triggerDeadzone) {
rt = 0; rt = 0;
} }
leftTrigger = (byte)(lt * 0xFF); context.leftTrigger = (byte)(lt * 0xFF);
rightTrigger = (byte)(rt * 0xFF); context.rightTrigger = (byte)(rt * 0xFF);
} }
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) { if (context.hatXAxis != -1 && context.hatYAxis != -1) {
inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG); context.inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
if (hatX < -0.5) { if (hatX < -0.5) {
inputMap |= ControllerPacket.LEFT_FLAG; context.inputMap |= ControllerPacket.LEFT_FLAG;
} }
else if (hatX > 0.5) { else if (hatX > 0.5) {
inputMap |= ControllerPacket.RIGHT_FLAG; context.inputMap |= ControllerPacket.RIGHT_FLAG;
} }
inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG); context.inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
if (hatY < -0.5) { if (hatY < -0.5) {
inputMap |= ControllerPacket.UP_FLAG; context.inputMap |= ControllerPacket.UP_FLAG;
} }
else if (hatY > 0.5) { else if (hatY > 0.5) {
inputMap |= ControllerPacket.DOWN_FLAG; context.inputMap |= ControllerPacket.DOWN_FLAG;
} }
} }
sendControllerInputPacket(); sendControllerInputPacket(context);
} }
public boolean handleMotionEvent(MotionEvent event) { public boolean handleMotionEvent(MotionEvent event) {
ControllerMapping mapping = getMappingForDevice(event.getDevice()); ControllerContext context = getContextForDevice(event.getDevice());
float lsX = 0, lsY = 0, rsX = 0, rsY = 0, rt = 0, lt = 0, hatX = 0, hatY = 0; 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 // We purposefully ignore the historical values in the motion event as it makes
// the controller feel sluggish for some users. // the controller feel sluggish for some users.
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) { if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
lsX = event.getAxisValue(mapping.leftStickXAxis); lsX = event.getAxisValue(context.leftStickXAxis);
lsY = event.getAxisValue(mapping.leftStickYAxis); lsY = event.getAxisValue(context.leftStickYAxis);
} }
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) { if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
rsX = event.getAxisValue(mapping.rightStickXAxis); rsX = event.getAxisValue(context.rightStickXAxis);
rsY = event.getAxisValue(mapping.rightStickYAxis); rsY = event.getAxisValue(context.rightStickYAxis);
} }
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) { if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
lt = event.getAxisValue(mapping.leftTriggerAxis); lt = event.getAxisValue(context.leftTriggerAxis);
rt = event.getAxisValue(mapping.rightTriggerAxis); rt = event.getAxisValue(context.rightTriggerAxis);
} }
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) { if (context.hatXAxis != -1 && context.hatYAxis != -1) {
hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X); hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y); hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
} }
handleAxisSet(mapping, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY); handleAxisSet(context, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY);
return true; return true;
} }
public boolean handleButtonUp(KeyEvent event) { public boolean handleButtonUp(KeyEvent event) {
ControllerMapping mapping = getMappingForDevice(event.getDevice()); ControllerContext context = getContextForDevice(event.getDevice());
int keyCode = handleRemapping(mapping, event); int keyCode = handleRemapping(context, event);
if (keyCode == 0) { if (keyCode == 0) {
return true; return true;
} }
@@ -515,78 +573,80 @@ public class ControllerHandler {
switch (keyCode) { switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_MODE: case KeyEvent.KEYCODE_BUTTON_MODE:
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG; context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_START: case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU: case KeyEvent.KEYCODE_MENU:
if (SystemClock.uptimeMillis() - startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) { if (SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) {
gestures.showKeyboard(); // 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();
} }
inputMap &= ~ControllerPacket.PLAY_FLAG; context.inputMap &= ~ControllerPacket.PLAY_FLAG;
break; break;
case KeyEvent.KEYCODE_BACK: case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_BUTTON_SELECT: case KeyEvent.KEYCODE_BUTTON_SELECT:
inputMap &= ~ControllerPacket.BACK_FLAG; context.inputMap &= ~ControllerPacket.BACK_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_LEFT:
inputMap &= ~ControllerPacket.LEFT_FLAG; context.inputMap &= ~ControllerPacket.LEFT_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_RIGHT: case KeyEvent.KEYCODE_DPAD_RIGHT:
inputMap &= ~ControllerPacket.RIGHT_FLAG; context.inputMap &= ~ControllerPacket.RIGHT_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_UP:
inputMap &= ~ControllerPacket.UP_FLAG; context.inputMap &= ~ControllerPacket.UP_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_DOWN:
inputMap &= ~ControllerPacket.DOWN_FLAG; context.inputMap &= ~ControllerPacket.DOWN_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_B: case KeyEvent.KEYCODE_BUTTON_B:
inputMap &= ~ControllerPacket.B_FLAG; context.inputMap &= ~ControllerPacket.B_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_BUTTON_A: case KeyEvent.KEYCODE_BUTTON_A:
inputMap &= ~ControllerPacket.A_FLAG; context.inputMap &= ~ControllerPacket.A_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_X: case KeyEvent.KEYCODE_BUTTON_X:
inputMap &= ~ControllerPacket.X_FLAG; context.inputMap &= ~ControllerPacket.X_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_Y: case KeyEvent.KEYCODE_BUTTON_Y:
inputMap &= ~ControllerPacket.Y_FLAG; context.inputMap &= ~ControllerPacket.Y_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_L1: case KeyEvent.KEYCODE_BUTTON_L1:
inputMap &= ~ControllerPacket.LB_FLAG; context.inputMap &= ~ControllerPacket.LB_FLAG;
lastLbUpTime = SystemClock.uptimeMillis(); context.lastLbUpTime = SystemClock.uptimeMillis();
break; break;
case KeyEvent.KEYCODE_BUTTON_R1: case KeyEvent.KEYCODE_BUTTON_R1:
inputMap &= ~ControllerPacket.RB_FLAG; context.inputMap &= ~ControllerPacket.RB_FLAG;
lastRbUpTime = SystemClock.uptimeMillis(); context.lastRbUpTime = SystemClock.uptimeMillis();
break; break;
case KeyEvent.KEYCODE_BUTTON_THUMBL: case KeyEvent.KEYCODE_BUTTON_THUMBL:
inputMap &= ~ControllerPacket.LS_CLK_FLAG; context.inputMap &= ~ControllerPacket.LS_CLK_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_THUMBR: case KeyEvent.KEYCODE_BUTTON_THUMBR:
inputMap &= ~ControllerPacket.RS_CLK_FLAG; context.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_L2: case KeyEvent.KEYCODE_BUTTON_L2:
leftTrigger = 0; context.leftTrigger = 0;
break; break;
case KeyEvent.KEYCODE_BUTTON_R2: case KeyEvent.KEYCODE_BUTTON_R2:
rightTrigger = 0; context.rightTrigger = 0;
break; break;
default: default:
return false; return false;
} }
// Check if we're emulating the select button // Check if we're emulating the select button
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0) if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
{ {
// If either start or LB is up, select comes up too // If either start or LB is up, select comes up too
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 || if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
(inputMap & ControllerPacket.LB_FLAG) == 0) (context.inputMap & ControllerPacket.LB_FLAG) == 0)
{ {
inputMap &= ~ControllerPacket.BACK_FLAG; context.inputMap &= ~ControllerPacket.BACK_FLAG;
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT; context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT;
try { try {
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS); Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
@@ -595,16 +655,16 @@ public class ControllerHandler {
} }
// Check if we're emulating the special button // Check if we're emulating the special button
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0) if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0)
{ {
// If either start or select and RB is up, the special button comes up too // If either start or select and RB is up, the special button comes up too
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 || if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
((inputMap & ControllerPacket.BACK_FLAG) == 0 && ((context.inputMap & ControllerPacket.BACK_FLAG) == 0 &&
(inputMap & ControllerPacket.RB_FLAG) == 0)) (context.inputMap & ControllerPacket.RB_FLAG) == 0))
{ {
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG; context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL; context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL;
try { try {
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS); Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
@@ -612,112 +672,115 @@ public class ControllerHandler {
} }
} }
sendControllerInputPacket(); sendControllerInputPacket(context);
return true; return true;
} }
public boolean handleButtonDown(KeyEvent event) { public boolean handleButtonDown(KeyEvent event) {
ControllerMapping mapping = getMappingForDevice(event.getDevice()); ControllerContext context = getContextForDevice(event.getDevice());
int keyCode = handleRemapping(mapping, event); int keyCode = handleRemapping(context, event);
if (keyCode == 0) { if (keyCode == 0) {
return true; return true;
} }
switch (keyCode) { switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_MODE: case KeyEvent.KEYCODE_BUTTON_MODE:
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG; context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_START: case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU: case KeyEvent.KEYCODE_MENU:
if (event.getRepeatCount() == 0) { if (event.getRepeatCount() == 0) {
startDownTime = SystemClock.uptimeMillis(); context.startDownTime = SystemClock.uptimeMillis();
} }
inputMap |= ControllerPacket.PLAY_FLAG; context.inputMap |= ControllerPacket.PLAY_FLAG;
break; break;
case KeyEvent.KEYCODE_BACK: case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_BUTTON_SELECT: case KeyEvent.KEYCODE_BUTTON_SELECT:
inputMap |= ControllerPacket.BACK_FLAG; context.inputMap |= ControllerPacket.BACK_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_LEFT:
inputMap |= ControllerPacket.LEFT_FLAG; context.inputMap |= ControllerPacket.LEFT_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_RIGHT: case KeyEvent.KEYCODE_DPAD_RIGHT:
inputMap |= ControllerPacket.RIGHT_FLAG; context.inputMap |= ControllerPacket.RIGHT_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_UP:
inputMap |= ControllerPacket.UP_FLAG; context.inputMap |= ControllerPacket.UP_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_DOWN:
inputMap |= ControllerPacket.DOWN_FLAG; context.inputMap |= ControllerPacket.DOWN_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_B: case KeyEvent.KEYCODE_BUTTON_B:
inputMap |= ControllerPacket.B_FLAG; context.inputMap |= ControllerPacket.B_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_BUTTON_A: case KeyEvent.KEYCODE_BUTTON_A:
inputMap |= ControllerPacket.A_FLAG; context.inputMap |= ControllerPacket.A_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_X: case KeyEvent.KEYCODE_BUTTON_X:
inputMap |= ControllerPacket.X_FLAG; context.inputMap |= ControllerPacket.X_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_Y: case KeyEvent.KEYCODE_BUTTON_Y:
inputMap |= ControllerPacket.Y_FLAG; context.inputMap |= ControllerPacket.Y_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_L1: case KeyEvent.KEYCODE_BUTTON_L1:
inputMap |= ControllerPacket.LB_FLAG; context.inputMap |= ControllerPacket.LB_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_R1: case KeyEvent.KEYCODE_BUTTON_R1:
inputMap |= ControllerPacket.RB_FLAG; context.inputMap |= ControllerPacket.RB_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_THUMBL: case KeyEvent.KEYCODE_BUTTON_THUMBL:
inputMap |= ControllerPacket.LS_CLK_FLAG; context.inputMap |= ControllerPacket.LS_CLK_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_THUMBR: case KeyEvent.KEYCODE_BUTTON_THUMBR:
inputMap |= ControllerPacket.RS_CLK_FLAG; context.inputMap |= ControllerPacket.RS_CLK_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_L2: case KeyEvent.KEYCODE_BUTTON_L2:
leftTrigger = (byte)0xFF; context.leftTrigger = (byte)0xFF;
break; break;
case KeyEvent.KEYCODE_BUTTON_R2: case KeyEvent.KEYCODE_BUTTON_R2:
rightTrigger = (byte)0xFF; context.rightTrigger = (byte)0xFF;
break; break;
default: default:
return false; return false;
} }
// Start+LB acts like select for controllers with one button // Start+LB acts like select for controllers with one button
if ((inputMap & ControllerPacket.PLAY_FLAG) != 0 && if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
((inputMap & ControllerPacket.LB_FLAG) != 0 || ((context.inputMap & ControllerPacket.LB_FLAG) != 0 ||
SystemClock.uptimeMillis() - lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS)) SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
{ {
inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG); context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
inputMap |= ControllerPacket.BACK_FLAG; context.inputMap |= ControllerPacket.BACK_FLAG;
emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT; context.emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
} }
// We detect select+start or start+RB as the special button combo // We detect select+start or start+RB as the special button combo
if (((inputMap & ControllerPacket.RB_FLAG) != 0 || if (((context.inputMap & ControllerPacket.RB_FLAG) != 0 ||
(SystemClock.uptimeMillis() - lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) || (SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
(inputMap & ControllerPacket.BACK_FLAG) != 0) && (context.inputMap & ControllerPacket.BACK_FLAG) != 0) &&
(inputMap & ControllerPacket.PLAY_FLAG) != 0) (context.inputMap & ControllerPacket.PLAY_FLAG) != 0)
{ {
inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG); context.inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG; context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL; context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
} }
// Send a new input packet if this is the first instance of a button down event // Send a new input packet if this is the first instance of a button down event
// or anytime if we're emulating a button // or anytime if we're emulating a button
if (event.getRepeatCount() == 0 || emulatingButtonFlags != 0) { if (event.getRepeatCount() == 0 || context.emulatingButtonFlags != 0) {
sendControllerInputPacket(); sendControllerInputPacket(context);
} }
return true; return true;
} }
class ControllerMapping { class ControllerContext {
public String name;
public int id;
public int leftStickXAxis = -1; public int leftStickXAxis = -1;
public int leftStickYAxis = -1; public int leftStickYAxis = -1;
public float leftStickDeadzoneRadius; public float leftStickDeadzoneRadius;
@@ -737,7 +800,29 @@ public class ControllerHandler {
public boolean isDualShock4; public boolean isDualShock4;
public boolean isXboxController; public boolean isXboxController;
public boolean backIsStart; public boolean backIsStart;
public boolean modeIsSelect;
public boolean isRemote; public boolean isRemote;
public boolean hasJoystickAxes; public boolean hasJoystickAxes;
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 * GFE's prefix for every key code
*/ */
public static final short KEY_PREFIX = (short) 0x80; private static final short KEY_PREFIX = (short) 0x80;
public static final int VK_0 = 48; public static final int VK_0 = 48;
public static final int VK_9 = 57; public static final int VK_9 = 57;
@@ -11,9 +11,10 @@ public class TouchContext {
private long originalTouchTime = 0; private long originalTouchTime = 0;
private boolean cancelled; private boolean cancelled;
private NvConnection conn; private final NvConnection conn;
private int actionIndex; private final int actionIndex;
private double xFactor, yFactor; private final double xFactor;
private final double yFactor;
private static final int TAP_MOVEMENT_THRESHOLD = 10; private static final int TAP_MOVEMENT_THRESHOLD = 10;
private static final int TAP_TIME_THRESHOLD = 250; private static final int TAP_TIME_THRESHOLD = 250;
@@ -29,9 +29,9 @@ public class EvdevEvent {
/* Keys */ /* Keys */
public static final short KEY_Q = 16; public static final short KEY_Q = 16;
public short type; public final short type;
public short code; public final short code;
public int value; public final int value;
public EvdevEvent(short type, short code, int value) { public EvdevEvent(short type, short code, int value) {
this.type = type; this.type = type;
@@ -7,12 +7,12 @@ import com.limelight.LimeLog;
public class EvdevHandler { public class EvdevHandler {
private String absolutePath; private final String absolutePath;
private EvdevListener listener; private final EvdevListener listener;
private boolean shutdown = false; private boolean shutdown = false;
private int fd = -1; private int fd = -1;
private Thread handlerThread = new Thread() { private final Thread handlerThread = new Thread() {
@Override @Override
public void run() { public void run() {
// All the finally blocks here make this code look like a mess // All the finally blocks here make this code look like a mess
@@ -26,7 +26,7 @@ public class EvdevReader {
// 4.4 and later to do live SELinux policy changes. // 4.4 and later to do live SELinux policy changes.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
EvdevShell shell = EvdevShell.getInstance(); EvdevShell shell = EvdevShell.getInstance();
shell.runCommand("supolicy --live \"allow untrusted_app input_device dir { getattr read search }\" " + shell.runCommand("supolicy --live \"allow untrusted_app input_device dir { open getattr read search }\" " +
"\"allow untrusted_app input_device chr_file { open read write ioctl }\""); "\"allow untrusted_app input_device chr_file { open read write ioctl }\"");
} }
} }
@@ -4,7 +4,7 @@ import android.view.KeyEvent;
public class EvdevTranslator { public class EvdevTranslator {
public static final short EVDEV_KEY_CODES[] = { private static final short[] EVDEV_KEY_CODES = {
0, //KeyEvent.VK_RESERVED 0, //KeyEvent.VK_RESERVED
KeyEvent.KEYCODE_ESCAPE, KeyEvent.KEYCODE_ESCAPE,
KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_1,
@@ -36,7 +36,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
private int totalFrames; private int totalFrames;
private long totalTimeMs; private long totalTimeMs;
private int cpuCount = Runtime.getRuntime().availableProcessors(); private final int cpuCount = Runtime.getRuntime().availableProcessors();
@SuppressWarnings("unused") @SuppressWarnings("unused")
private int findOptimalPerformanceLevel() { private int findOptimalPerformanceLevel() {
@@ -178,7 +178,6 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
@Override @Override
public void run() { public void run() {
long nextFrameTime = System.currentTimeMillis(); long nextFrameTime = System.currentTimeMillis();
DecodeUnit du;
while (!isInterrupted()) while (!isInterrupted())
{ {
long diff = nextFrameTime - System.currentTimeMillis(); long diff = nextFrameTime - System.currentTimeMillis();
@@ -232,7 +231,8 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) { if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
decoderBuffer.clear(); decoderBuffer.clear();
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) { for (ByteBufferDescriptor bbd = decodeUnit.getBufferHead();
bbd != null; bbd = bbd.nextDescriptor) {
decoderBuffer.put(bbd.data, bbd.offset, bbd.length); decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
} }
@@ -242,7 +242,8 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()]; data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
int offset = 0; int offset = 0;
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) { for (ByteBufferDescriptor bbd = decodeUnit.getBufferHead();
bbd != null; bbd = bbd.nextDescriptor) {
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length); System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
offset += bbd.length; offset += bbd.length;
} }
@@ -1,5 +1,6 @@
package com.limelight.binding.video; package com.limelight.binding.video;
import com.limelight.nvstream.av.DecodeUnit;
import com.limelight.nvstream.av.video.VideoDecoderRenderer; import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer; import com.limelight.nvstream.av.video.VideoDepacketizer;
@@ -55,6 +56,11 @@ public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer {
return decoderRenderer.getCapabilities(); return decoderRenderer.getCapabilities();
} }
@Override
public void directSubmitDecodeUnit(DecodeUnit du) {
decoderRenderer.directSubmitDecodeUnit(du);
}
@Override @Override
public int getAverageDecoderLatency() { public int getAverageDecoderLatency() {
if (decoderRenderer != null) { if (decoderRenderer != null) {
@@ -2,6 +2,6 @@ package com.limelight.binding.video;
import com.limelight.nvstream.av.video.VideoDecoderRenderer; import com.limelight.nvstream.av.video.VideoDecoderRenderer;
public abstract class EnhancedDecoderRenderer implements VideoDecoderRenderer { public abstract class EnhancedDecoderRenderer extends VideoDecoderRenderer {
public abstract String getDecoderName(); public abstract String getDecoderName();
} }
@@ -28,9 +28,9 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
private ByteBuffer[] videoDecoderInputBuffers; private ByteBuffer[] videoDecoderInputBuffers;
private MediaCodec videoDecoder; private MediaCodec videoDecoder;
private Thread rendererThread; private Thread rendererThread;
private boolean needsSpsBitstreamFixup, isExynos4; private final boolean needsSpsBitstreamFixup, isExynos4;
private VideoDepacketizer depacketizer; private VideoDepacketizer depacketizer;
private boolean adaptivePlayback; private final boolean adaptivePlayback, directSubmit;
private int initialWidth, initialHeight; private int initialWidth, initialHeight;
private boolean needsBaselineSpsHack; private boolean needsBaselineSpsHack;
@@ -46,8 +46,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
private int numPpsIn; private int numPpsIn;
private int numIframeIn; private int numIframeIn;
private static final boolean ENABLE_ASYNC_RENDERER = false;
@TargetApi(Build.VERSION_CODES.KITKAT) @TargetApi(Build.VERSION_CODES.KITKAT)
public MediaCodecDecoderRenderer() { public MediaCodecDecoderRenderer() {
//dumpDecoders(); //dumpDecoders();
@@ -58,12 +56,15 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
} }
if (decoder == null) { if (decoder == null) {
// This case is handled later in setup() // This case is handled later in setup()
needsSpsBitstreamFixup = isExynos4 =
adaptivePlayback = directSubmit = false;
return; return;
} }
decoderName = decoder.getName(); decoderName = decoder.getName();
// Set decoder-specific attributes // Set decoder-specific attributes
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(decoderName, decoder);
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder); adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder); needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder); needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder);
@@ -77,9 +78,11 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
if (isExynos4) { if (isExynos4) {
LimeLog.info("Decoder "+decoderName+" is on Exynos 4"); LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
} }
if (directSubmit) {
LimeLog.info("Decoder "+decoderName+" will use direct submit");
}
} }
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override @Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) { public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.initialWidth = width; this.initialWidth = width;
@@ -107,52 +110,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height); 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.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT); videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
@@ -162,7 +119,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
} }
@TargetApi(Build.VERSION_CODES.LOLLIPOP) @TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void handleDecoderException(MediaCodecDecoderRenderer dr, Exception e, ByteBuffer buf, int codecFlags) { private void handleDecoderException(Exception e, ByteBuffer buf, int codecFlags) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (e instanceof CodecException) { if (e instanceof CodecException) {
CodecException codecExc = (CodecException) e; CodecException codecExc = (CodecException) e;
@@ -178,14 +135,92 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
} }
if (buf != null || codecFlags != 0) { if (buf != null || codecFlags != 0) {
throw new RendererException(dr, e, buf, codecFlags); throw new RendererException(this, e, buf, codecFlags);
} }
else { else {
throw new RendererException(dr, e); throw new RendererException(this, e);
} }
} }
private void startRendererThread() 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()
{ {
rendererThread = new Thread() { rendererThread = new Thread() {
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@@ -194,6 +229,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
BufferInfo info = new BufferInfo(); BufferInfo info = new BufferInfo();
DecodeUnit du = null; DecodeUnit du = null;
int inputIndex = -1; int inputIndex = -1;
long lastDuDequeueTime = 0;
while (!isInterrupted()) while (!isInterrupted())
{ {
// In order to get as much data to the decoder as early as possible, // In order to get as much data to the decoder as early as possible,
@@ -201,8 +237,12 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
if (inputIndex == -1 && du == null) { if (inputIndex == -1 && du == null) {
try { try {
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
inputIndex = videoDecoder.dequeueInputBuffer(0); inputIndex = dequeueInputBuffer(false, false);
du = depacketizer.pollNextDecodeUnit(); du = depacketizer.pollNextDecodeUnit();
if (du != null) {
lastDuDequeueTime = System.currentTimeMillis();
notifyDuReceived(du);
}
// Stop if we can't get a DU or input buffer // Stop if we can't get a DU or input buffer
if (du == null || inputIndex == -1) { if (du == null || inputIndex == -1) {
@@ -216,7 +256,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
} }
} catch (Exception e) { } catch (Exception e) {
inputIndex = -1; inputIndex = -1;
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); handleDecoderException(e, null, 0);
} }
} }
@@ -231,21 +271,30 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
// If we've got a DU waiting to be given to the decoder, // If we've got a DU waiting to be given to the decoder,
// wait a full 3 ms for an input buffer. Otherwise // wait a full 3 ms for an input buffer. Otherwise
// just see if we can get one immediately. // just see if we can get one immediately.
inputIndex = videoDecoder.dequeueInputBuffer(du != null ? 3000 : 0); inputIndex = dequeueInputBuffer(du != null, false);
} catch (Exception e) { } catch (Exception e) {
inputIndex = -1; inputIndex = -1;
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); handleDecoderException(e, null, 0);
} }
} }
// Grab a decode unit if we don't have one already // Grab a decode unit if we don't have one already
if (du == null) { if (du == null) {
du = depacketizer.pollNextDecodeUnit(); 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 // If we've got both a decode unit and an input buffer, we'll
// submit now. Otherwise, we wait until we have one. // submit now. Otherwise, we wait until we have one.
if (du != null && inputIndex >= 0) { 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); submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
// DU and input buffer have both been consumed // DU and input buffer have both been consumed
@@ -298,7 +347,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
} }
} }
} catch (Exception e) { } catch (Exception e) {
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); handleDecoderException(e, null, 0);
} }
} }
} }
@@ -316,11 +365,15 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
// Start the decoder // Start the decoder
videoDecoder.start(); 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(); videoDecoderInputBuffers = videoDecoder.getInputBuffers();
startRendererThread();
if (directSubmit) {
startDirectSubmitRendererThread();
} }
else {
startLegacyRendererThread();
}
return true; return true;
} }
@@ -357,7 +410,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
timestampUs, codecFlags); timestampUs, codecFlags);
break; break;
} catch (Exception e) { } catch (Exception e) {
handleDecoderException(this, e, null, codecFlags); handleDecoderException(e, null, codecFlags);
lastException = e; lastException = e;
} }
} }
@@ -369,14 +422,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) { private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
long currentTime = System.currentTimeMillis(); long timestampUs = System.currentTimeMillis() * 1000;
long delta = currentTime-decodeUnit.getReceiveTimestamp();
if (delta >= 0 && delta < 1000) {
totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp();
totalFrames++;
}
long timestampUs = currentTime * 1000;
if (timestampUs <= lastTimestampUs) { if (timestampUs <= lastTimestampUs) {
// We can't submit multiple buffers with the same timestamp // We can't submit multiple buffers with the same timestamp
// so bump it up by one before queuing // so bump it up by one before queuing
@@ -400,7 +446,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
boolean needsSpsReplay = false; boolean needsSpsReplay = false;
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) { if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
ByteBufferDescriptor header = decodeUnit.getBufferList().get(0); ByteBufferDescriptor header = decodeUnit.getBufferHead();
if (header.data[header.offset+4] == 0x67) { if (header.data[header.offset+4] == 0x67) {
numSpsIn++; numSpsIn++;
@@ -485,8 +531,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
} }
// Copy data from our buffer list into the input buffer // Copy data from our buffer list into the input buffer
for (ByteBufferDescriptor desc : decodeUnit.getBufferList()) for (ByteBufferDescriptor desc = decodeUnit.getBufferHead();
{ desc != null; desc = desc.nextDescriptor) {
buf.put(desc.data, desc.offset, desc.length); buf.put(desc.data, desc.offset, desc.length);
} }
@@ -502,7 +548,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
} }
private void replaySps() { private void replaySps() {
int inputIndex = videoDecoder.dequeueInputBuffer(-1); int inputIndex = dequeueInputBuffer(true, true);
ByteBuffer inputBuffer = videoDecoderInputBuffers[inputIndex]; ByteBuffer inputBuffer = videoDecoderInputBuffers[inputIndex];
inputBuffer.clear(); inputBuffer.clear();
@@ -530,8 +576,15 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
@Override @Override
public int getCapabilities() { public int getCapabilities() {
return adaptivePlayback ? int caps = 0;
caps |= adaptivePlayback ?
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0; VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0;
caps |= directSubmit ?
VideoDecoderRenderer.CAPABILITY_DIRECT_SUBMIT : 0;
return caps;
} }
@Override @Override
@@ -555,11 +608,40 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
return decoderName; 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 { public class RendererException extends RuntimeException {
private static final long serialVersionUID = 8985937536997012406L; private static final long serialVersionUID = 8985937536997012406L;
private Exception originalException; private final Exception originalException;
private MediaCodecDecoderRenderer renderer; private final MediaCodecDecoderRenderer renderer;
private ByteBuffer currentBuffer; private ByteBuffer currentBuffer;
private int currentCodecFlags; private int currentCodecFlags;
@@ -20,12 +20,26 @@ import com.limelight.LimeLog;
public class MediaCodecHelper { public class MediaCodecHelper {
public static final List<String> preferredDecoders; private static final List<String> preferredDecoders;
public static final List<String> blacklistedDecoderPrefixes; private static final List<String> blacklistedDecoderPrefixes;
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes; private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
public static final List<String> whitelistedAdaptiveResolutionPrefixes; private static final List<String> whitelistedAdaptiveResolutionPrefixes;
public static final List<String> baselineProfileHackPrefixes; private static final List<String> baselineProfileHackPrefixes;
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");
}
static { static {
preferredDecoders = new LinkedList<String>(); preferredDecoders = new LinkedList<String>();
@@ -98,6 +112,10 @@ public class MediaCodecHelper {
return false; return false;
} }
public static boolean decoderCanDirectSubmit(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device();
}
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) { public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName); return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
} }
@@ -146,7 +164,7 @@ public class MediaCodecHelper {
return str; return str;
} }
public static MediaCodecInfo findPreferredDecoder() { private static MediaCodecInfo findPreferredDecoder() {
// This is a different algorithm than the other findXXXDecoder functions, // This is a different algorithm than the other findXXXDecoder functions,
// because we want to evaluate the decoders in our list's order // because we want to evaluate the decoders in our list's order
// rather than MediaCodecList's order // rather than MediaCodecList's order
@@ -217,7 +235,7 @@ public class MediaCodecHelper {
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly // since some bad decoders can throw IllegalArgumentExceptions unexpectedly
// and we want to be sure all callers are handling this possibility // and we want to be sure all callers are handling this possibility
@SuppressWarnings("RedundantThrows") @SuppressWarnings("RedundantThrows")
public static MediaCodecInfo findKnownSafeDecoder() throws Exception { private static MediaCodecInfo findKnownSafeDecoder() throws Exception {
for (MediaCodecInfo codecInfo : getMediaCodecList()) { for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders // Skip encoders
if (codecInfo.isEncoder()) { if (codecInfo.isEncoder()) {
@@ -1,10 +1,11 @@
package com.limelight.computers; package com.limelight.computers;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader; import java.io.StringReader;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@@ -30,18 +31,21 @@ import android.os.IBinder;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
public class ComputerManagerService extends Service { public class ComputerManagerService extends Service {
private static final int POLLING_PERIOD_MS = 3000; private static final int SERVERINFO_POLLING_PERIOD_MS = 3000;
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
private static final int MDNS_QUERY_PERIOD_MS = 1000; 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 ComputerManagerBinder binder = new ComputerManagerBinder(); private final ComputerManagerBinder binder = new ComputerManagerBinder();
private ComputerDatabaseManager dbManager; private ComputerDatabaseManager dbManager;
private AtomicInteger dbRefCount = new AtomicInteger(0); private final AtomicInteger dbRefCount = new AtomicInteger(0);
private IdentityManager idManager; private IdentityManager idManager;
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>(); private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
private ComputerManagerListener listener = null; private ComputerManagerListener listener = null;
private AtomicInteger activePolls = new AtomicInteger(0); private final AtomicInteger activePolls = new AtomicInteger(0);
private boolean pollingActive = false; private boolean pollingActive = false;
private DiscoveryService.DiscoveryBinder discoveryBinder; private DiscoveryService.DiscoveryBinder discoveryBinder;
@@ -65,10 +69,7 @@ public class ComputerManagerService extends Service {
}; };
// Returns true if the details object was modified // Returns true if the details object was modified
private boolean runPoll(ComputerDetails details) private boolean runPoll(ComputerDetails details, boolean newPc, int offlineCount) throws InterruptedException {
{
boolean newPc = details.name.isEmpty();
if (!getLocalDatabaseReference()) { if (!getLocalDatabaseReference()) {
return false; return false;
} }
@@ -76,12 +77,22 @@ public class ComputerManagerService extends Service {
activePolls.incrementAndGet(); activePolls.incrementAndGet();
// Poll the machine // Poll the machine
if (!doPollMachine(details)) { try {
if (!pollComputer(details)) {
if (!newPc && offlineCount < OFFLINE_POLL_TRIES) {
// Return without calling the listener
return false;
}
details.state = ComputerDetails.State.OFFLINE; details.state = ComputerDetails.State.OFFLINE;
details.reachability = ComputerDetails.Reachability.OFFLINE; details.reachability = ComputerDetails.Reachability.OFFLINE;
} }
} catch (InterruptedException e) {
releaseLocalDatabaseReference();
throw e;
} finally {
activePolls.decrementAndGet(); activePolls.decrementAndGet();
}
// If it's online, update our persistent state // If it's online, update our persistent state
if (details.state == ComputerDetails.State.ONLINE) { if (details.state == ComputerDetails.State.ONLINE) {
@@ -91,7 +102,7 @@ public class ComputerManagerService extends Service {
if (dbManager.getComputerByName(details.name) == null) { if (dbManager.getComputerByName(details.name) == null) {
// It's gone // It's gone
releaseLocalDatabaseReference(); releaseLocalDatabaseReference();
return true; return false;
} }
} }
@@ -111,13 +122,21 @@ public class ComputerManagerService extends Service {
Thread t = new Thread() { Thread t = new Thread() {
@Override @Override
public void run() { public void run() {
int offlineCount = 0;
while (!isInterrupted() && pollingActive) { while (!isInterrupted() && pollingActive) {
try {
// Check if this poll has modified the details // Check if this poll has modified the details
runPoll(details); if (!runPoll(details, false, offlineCount)) {
LimeLog.warning(details.name + " is offline (try " + offlineCount + ")");
offlineCount++;
}
else {
offlineCount = 0;
}
// Wait until the next polling interval // Wait until the next polling interval
try { Thread.sleep(SERVERINFO_POLLING_PERIOD_MS / ((offlineCount > 0) ? 2 : 1));
Thread.sleep(POLLING_PERIOD_MS);
} catch (InterruptedException e) { } catch (InterruptedException e) {
break; break;
} }
@@ -177,10 +196,6 @@ public class ComputerManagerService extends Service {
return ComputerManagerService.this.addComputerBlocking(addr); return ComputerManagerService.this.addComputerBlocking(addr);
} }
public void addComputer(InetAddress addr) {
ComputerManagerService.this.addComputer(addr);
}
public void removeComputer(String name) { public void removeComputer(String name) {
ComputerManagerService.this.removeComputer(name); ComputerManagerService.this.removeComputer(name);
} }
@@ -239,7 +254,7 @@ public class ComputerManagerService extends Service {
@Override @Override
public void notifyComputerAdded(MdnsComputer computer) { public void notifyComputerAdded(MdnsComputer computer) {
// Kick off a serverinfo poll on this machine // Kick off a serverinfo poll on this machine
addComputer(computer.getAddress()); addComputerBlocking(computer.getAddress());
} }
@Override @Override
@@ -255,28 +270,11 @@ 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) { private void addTuple(ComputerDetails details) {
synchronized (pollingTuples) { synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) { for (PollingTuple tuple : pollingTuples) {
// Check if this is the same computer // Check if this is the same computer
if (tuple.computer == details || if (tuple.computer.uuid.equals(details.uuid)) {
// If there's no name on one of these computers, compare with the local IP
((details.name.isEmpty() || tuple.computer.name.isEmpty()) &&
tuple.computer.localIp.equals(details.localIp)) ||
// If there is a name on both computers, compare with name
((!details.name.isEmpty() && !tuple.computer.name.isEmpty()) &&
tuple.computer.name.equals(details.name))) {
// Update details anyway in case this machine has been re-added by IP // Update details anyway in case this machine has been re-added by IP
// after not being reachable by our existing information // after not being reachable by our existing information
tuple.computer.localIp = details.localIp; tuple.computer.localIp = details.localIp;
@@ -307,13 +305,18 @@ public class ComputerManagerService extends Service {
ComputerDetails fakeDetails = new ComputerDetails(); ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr; fakeDetails.localIp = addr;
fakeDetails.remoteIp = addr; fakeDetails.remoteIp = addr;
fakeDetails.name = "";
// Block while we try to fill the details // Block while we try to fill the details
runPoll(fakeDetails); try {
runPoll(fakeDetails, true, 0);
} catch (InterruptedException e) {
return false;
}
// If the machine is reachable, it was successful // If the machine is reachable, it was successful
if (fakeDetails.state == ComputerDetails.State.ONLINE) { if (fakeDetails.state == ComputerDetails.State.ONLINE) {
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
// Start a polling thread for this machine // Start a polling thread for this machine
addTuple(fakeDetails); addTuple(fakeDetails);
return true; return true;
@@ -384,14 +387,91 @@ public class ComputerManagerService extends Service {
} }
} }
private boolean pollComputer(ComputerDetails details, boolean localFirst) { // 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 {
ComputerDetails polledDetails; ComputerDetails polledDetails;
ComputerDetails.Reachability reachability;
// If the local address is routable across the Internet, // If the local address is routable across the Internet,
// always consider this PC remote to be conservative // always consider this PC remote to be conservative
if (details.localIp.equals(details.remoteIp)) { if (details.localIp.equals(details.remoteIp)) {
localFirst = false; reachability = ComputerDetails.Reachability.REMOTE;
} }
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) { if (localFirst) {
polledDetails = tryPollIp(details, details.localIp); polledDetails = tryPollIp(details, details.localIp);
@@ -425,24 +505,19 @@ public class ComputerManagerService extends Service {
return false; return false;
} }
// Save the old MAC address
String savedMacAddress = details.macAddress;
// If we got here, it's reachable // If we got here, it's reachable
details.update(polledDetails); details.update(polledDetails);
return true;
// 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;
} }
private boolean doPollMachine(ComputerDetails details) { return true;
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 @Override
@@ -491,8 +566,8 @@ public class ComputerManagerService extends Service {
public class ApplistPoller { public class ApplistPoller {
private Thread thread; private Thread thread;
private ComputerDetails computer; private final ComputerDetails computer;
private Object pollEvent = new Object(); private final Object pollEvent = new Object();
public ApplistPoller(ComputerDetails computer) { public ApplistPoller(ComputerDetails computer) {
this.computer = computer; this.computer = computer;
@@ -507,7 +582,7 @@ public class ComputerManagerService extends Service {
private boolean waitPollingDelay() { private boolean waitPollingDelay() {
try { try {
synchronized (pollEvent) { synchronized (pollEvent) {
pollEvent.wait(POLLING_PERIOD_MS); pollEvent.wait(APPLIST_POLLING_PERIOD_MS);
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
return false; return false;
@@ -552,9 +627,19 @@ public class ComputerManagerService extends Service {
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList)); List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
if (appList != null && !appList.isEmpty() && !list.isEmpty()) { if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
// Open the cache file // Open the cache file
FileOutputStream cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString()); OutputStream cacheOut = null;
try {
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
CacheHelper.writeStringToOutputStream(cacheOut, appList); CacheHelper.writeStringToOutputStream(cacheOut, appList);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (cacheOut != null) {
cacheOut.close(); cacheOut.close();
}
} catch (IOException e) {}
}
// Update the computer // Update the computer
computer.rawAppList = appList; computer.rawAppList = appList;
@@ -593,7 +678,7 @@ public class ComputerManagerService extends Service {
class PollingTuple { class PollingTuple {
public Thread thread; public Thread thread;
public ComputerDetails computer; public final ComputerDetails computer;
public PollingTuple(ComputerDetails computer, Thread thread) { public PollingTuple(ComputerDetails computer, Thread thread) {
this.computer = computer; this.computer = computer;
@@ -70,7 +70,7 @@ public class DiscoveryService extends Service {
}); });
} }
private DiscoveryBinder binder = new DiscoveryBinder(); private final DiscoveryBinder binder = new DiscoveryBinder();
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
@@ -1,118 +1,69 @@
package com.limelight.grid; package com.limelight.grid;
import android.content.Context; import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import com.koushikdutta.async.future.FutureCallback;
import com.koushikdutta.ion.ImageViewBitmapInfo;
import com.koushikdutta.ion.Ion;
import com.limelight.AppView; import com.limelight.AppView;
import com.limelight.LimeLog; import com.limelight.LimeLog;
import com.limelight.R; import com.limelight.R;
import com.limelight.binding.PlatformBinding; 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.nvstream.http.ComputerDetails; 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.KeyManagementException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
import java.util.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> { 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 ComputerDetails computer; private final CachedAppAssetLoader loader;
private String uniqueId;
private LimelightCryptoProvider cryptoProvider;
private SSLContext sslContext;
private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>();
public AppGridAdapter(Context context, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException { public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws KeyManagementException, NoSuchAlgorithmException {
super(context, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading); super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
this.computer = computer; int dpi = activity.getResources().getDisplayMetrics().densityDpi;
this.uniqueId = uniqueId; int dp;
cryptoProvider = PlatformBinding.getCryptoProvider(context); if (small) {
dp = SMALL_WIDTH_DP;
sslContext = SSLContext.getInstance("SSL"); }
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom()); else {
dp = LARGE_WIDTH_DP;
} }
TrustManager[] trustAllCerts = new TrustManager[] { double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160));
new X509TrustManager() { if (scalingDivisor < 1.0) {
public X509Certificate[] getAcceptedIssuers() { // We don't want to make them bigger before draw-time
return new X509Certificate[0]; scalingDivisor = 1.0;
} }
public void checkClientTrusted(X509Certificate[] certs, String authType) {} LimeLog.info("Art scaling divisor: " + scalingDivisor);
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}};
KeyManager[] ourKeyman = new KeyManager[] { BitmapFactory.Options options = new BitmapFactory.Options();
new X509KeyManager() { options.inSampleSize = (int) scalingDivisor;
public String chooseClientAlias(String[] keyTypes,
Principal[] issuers, Socket socket) { this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
return "Limelight-RSA"; new NetworkAssetLoader(context, uniqueId),
new MemoryAssetLoader(),
new DiskAssetLoader(context.getCacheDir()),
BitmapFactory.decodeResource(activity.getResources(),
R.drawable.image_loading, options));
} }
public String chooseServerAlias(String keyType, Principal[] issuers, public void cancelQueuedOperations() {
Socket socket) { loader.cancelForegroundLoads();
return null; loader.cancelBackgroundLoads();
loader.freeCacheMemory();
} }
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() { private void sortList() {
Collections.sort(itemList, new Comparator<AppView.AppObject>() { Collections.sort(itemList, new Comparator<AppView.AppObject>() {
@Override @Override
@@ -122,62 +73,22 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
}); });
} }
private InetAddress getCurrentAddress() {
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
return computer.localIp;
}
else {
return computer.remoteIp;
}
}
public void addApp(AppView.AppObject app) { public void addApp(AppView.AppObject app) {
// Queue a request to fetch this bitmap into cache
loader.queueCacheLoad(app.app);
// Add the app to our sorted list
itemList.add(app); itemList.add(app);
sortList(); sortList();
} }
public void abortPendingRequests() { public void removeApp(AppView.AppObject app) {
HashMap<ImageView, Future> tempMap; itemList.remove(app);
synchronized (pendingRequests) {
// Copy the pending requests under a lock
tempMap = new HashMap<ImageView, Future>(pendingRequests);
} }
for (Future f : tempMap.values()) { public boolean populateImageView(ImageView imgView, AppView.AppObject obj) {
if (!f.isCancelled() && !f.isDone()) { // Let the cached asset loader handle it
f.cancel(true); loader.populateImageView(obj.app, imgView);
}
}
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; return true;
} }
@@ -201,84 +112,4 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
// No overlay // No overlay
return false; 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; import java.util.ArrayList;
public abstract class GenericGridAdapter<T> extends BaseAdapter { public abstract class GenericGridAdapter<T> extends BaseAdapter {
protected Context context; protected final Context context;
protected int defaultImageRes; protected final int defaultImageRes;
protected int layoutId; protected final int layoutId;
protected ArrayList<T> itemList = new ArrayList<T>(); protected final ArrayList<T> itemList = new ArrayList<T>();
protected LayoutInflater inflater; protected final LayoutInflater inflater;
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) { public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
this.context = context; this.context = context;
@@ -0,0 +1,327 @@
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.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);
// 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()+")";
}
}
}
@@ -0,0 +1,67 @@
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) {}
}
}
}
}
@@ -0,0 +1,37 @@
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();
}
}
@@ -0,0 +1,49 @@
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,6 +14,7 @@ import com.limelight.utils.UiHelper;
import android.app.Activity; import android.app.Activity;
import android.app.Service; import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.content.res.Configuration; import android.content.res.Configuration;
@@ -21,15 +22,16 @@ import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
public class AddComputerManually extends Activity { public class AddComputerManually extends Activity {
private TextView hostText; private TextView hostText;
private ComputerManagerService.ComputerManagerBinder managerBinder; private ComputerManagerService.ComputerManagerBinder managerBinder;
private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>(); private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
private Thread addThread; private Thread addThread;
private ServiceConnection serviceConnection = new ServiceConnection() { private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, final IBinder binder) { public void onServiceConnected(ComponentName className, final IBinder binder) {
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder); managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
startAddThread(); startAddThread();
@@ -161,6 +163,12 @@ public class AddComputerManually extends Activity {
computersToAdd.add(hostText.getText().toString().trim()); computersToAdd.add(hostText.getText().toString().trim());
} }
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
// This is how the Fire TV dismisses the keyboard
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(hostText.getWindowToken(), 0);
return false;
}
return false; return false;
} }
@@ -2,6 +2,7 @@ package com.limelight.preferences;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
public class PreferenceConfiguration { public class PreferenceConfiguration {
@@ -16,6 +17,7 @@ public class PreferenceConfiguration {
private static final String LANGUAGE_PREF_STRING = "list_languages"; private static final String LANGUAGE_PREF_STRING = "list_languages";
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode"; private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode"; private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
private static final int BITRATE_DEFAULT_720_30 = 5; private static final int BITRATE_DEFAULT_720_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10; private static final int BITRATE_DEFAULT_720_60 = 10;
@@ -32,6 +34,7 @@ public class PreferenceConfiguration {
private static final int DEFAULT_DEADZONE = 15; private static final int DEFAULT_DEADZONE = 15;
public static final String DEFAULT_LANGUAGE = "default"; public static final String DEFAULT_LANGUAGE = "default";
private static final boolean DEFAULT_LIST_MODE = false; private static final boolean DEFAULT_LIST_MODE = false;
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
public static final int FORCE_HARDWARE_DECODER = -1; public static final int FORCE_HARDWARE_DECODER = -1;
public static final int AUTOSELECT_DECODER = 0; public static final int AUTOSELECT_DECODER = 0;
@@ -43,7 +46,7 @@ public class PreferenceConfiguration {
public int deadzonePercentage; public int deadzonePercentage;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings; public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language; public String language;
public boolean listMode, smallIconMode; public boolean listMode, smallIconMode, multiController;
public static int getDefaultBitrate(String resFpsString) { public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("720p30")) { if (resFpsString.equals("720p30")) {
@@ -65,6 +68,12 @@ public class PreferenceConfiguration {
} }
public static boolean getDefaultSmallMode(Context context) { public static boolean getDefaultSmallMode(Context context) {
PackageManager manager = context.getPackageManager();
if (manager != null && manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
// TVs shouldn't use small mode by default
return false;
}
// Use small mode on anything smaller than a 7" tablet // Use small mode on anything smaller than a 7" tablet
return context.getResources().getConfiguration().smallestScreenWidthDp < 600; return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
} }
@@ -156,6 +165,7 @@ public class PreferenceConfiguration {
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO); config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE); config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context)); config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
return config; return config;
} }
@@ -20,10 +20,14 @@ public class SeekBarPreference extends DialogPreference
private SeekBar seekBar; private SeekBar seekBar;
private TextView valueText; private TextView valueText;
private Context context; private final Context context;
private String dialogMessage, suffix; private final String dialogMessage;
private int defaultValue, maxValue, minValue, currentValue; private final String suffix;
private final int defaultValue;
private final int maxValue;
private final int minValue;
private int currentValue;
public SeekBarPreference(Context context, AttributeSet attrs) { public SeekBarPreference(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
@@ -127,13 +131,6 @@ public class SeekBarPreference extends DialogPreference
} }
} }
public void setMax(int max) {
this.maxValue = max;
}
public int getMax() {
return this.maxValue;
}
public void setProgress(int progress) { public void setProgress(int progress) {
this.currentValue = progress; this.currentValue = progress;
if (seekBar != null) { if (seekBar != null) {
@@ -1,5 +1,7 @@
package com.limelight.utils; package com.limelight.utils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@@ -9,8 +11,6 @@ import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.Reader; import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.Scanner;
public class CacheHelper { public class CacheHelper {
private static File openPath(boolean createPath, File root, String... path) { private static File openPath(boolean createPath, File root, String... path) {
@@ -30,12 +30,25 @@ public class CacheHelper {
return f; return f;
} }
public static FileInputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException { public static boolean cacheFileExists(File root, String... path) {
return new FileInputStream(openPath(false, root, path)); return openPath(false, root, path).exists();
} }
public static FileOutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException { public static InputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
return new FileOutputStream(openPath(true, root, path)); 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 String readInputStreamToString(InputStream in) throws IOException { public static String readInputStreamToString(InputStream in) throws IOException {
@@ -7,15 +7,16 @@ import android.app.AlertDialog;
import android.content.DialogInterface; import android.content.DialogInterface;
public class Dialog implements Runnable { public class Dialog implements Runnable {
private String title, message; private final String title;
private Activity activity; private final String message;
private boolean endAfterDismiss; private final Activity activity;
private final boolean endAfterDismiss;
private AlertDialog alert; private AlertDialog alert;
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>(); private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
public Dialog(Activity activity, String title, String message, boolean endAfterDismiss) private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
{ {
this.activity = activity; this.activity = activity;
this.title = title; this.title = title;
@@ -0,0 +1,90 @@
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,14 +9,15 @@ import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener; import android.content.DialogInterface.OnCancelListener;
public class SpinnerDialog implements Runnable,OnCancelListener { public class SpinnerDialog implements Runnable,OnCancelListener {
private String title, message; private final String title;
private Activity activity; private final String message;
private final Activity activity;
private ProgressDialog progress; private ProgressDialog progress;
private boolean finish; private final boolean finish;
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>(); private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
public SpinnerDialog(Activity activity, String title, String message, boolean finish) private SpinnerDialog(Activity activity, String title, String message, boolean finish)
{ {
this.activity = activity; this.activity = activity;
this.title = title; this.title = title;
@@ -1,11 +1,15 @@
package com.limelight.utils; package com.limelight.utils;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog;
import android.app.UiModeManager; import android.app.UiModeManager;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.view.View; import android.view.View;
import com.limelight.R;
public class UiHelper { public class UiHelper {
// Values from https://developer.android.com/training/tv/start/layouts.html // Values from https://developer.android.com/training/tv/start/layouts.html
@@ -28,4 +32,31 @@ public class UiHelper {
horizontalPaddingPixels, verticalPaddingPixels); 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();
}
} }
+2 -2
View File
@@ -1,4 +1,4 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -10,4 +10,4 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center" /> android:layout_gravity="center" />
</FrameLayout> </merge>
@@ -9,5 +9,7 @@
android:numColumns="auto_fit" android:numColumns="auto_fit"
android:columnWidth="160dp" android:columnWidth="160dp"
android:stretchMode="spacingWidth" android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/> android:gravity="center"/>
</LinearLayout> </LinearLayout>
@@ -9,5 +9,7 @@
android:numColumns="auto_fit" android:numColumns="auto_fit"
android:columnWidth="105dp" android:columnWidth="105dp"
android:stretchMode="spacingWidth" android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/> android:gravity="center"/>
</LinearLayout> </LinearLayout>
+3 -1
View File
@@ -6,10 +6,12 @@
<ListView <ListView
android:id="@+id/fragmentView" android:id="@+id/fragmentView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:background="@drawable/list_view_unselected" android:background="@drawable/list_view_unselected"
android:fastScrollEnabled="true" android:fastScrollEnabled="true"
android:longClickable="false" android:longClickable="false"
android:focusable="true"
android:focusableInTouchMode="true"
android:stackFromBottom="false" > android:stackFromBottom="false" >
</ListView> </ListView>
+2
View File
@@ -8,5 +8,7 @@
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:numColumns="auto_fit" android:numColumns="auto_fit"
android:columnWidth="160dp" android:columnWidth="160dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/> android:gravity="center"/>
</LinearLayout> </LinearLayout>
@@ -8,5 +8,7 @@
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:numColumns="auto_fit" android:numColumns="auto_fit"
android:columnWidth="105dp" android:columnWidth="105dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/> android:gravity="center"/>
</LinearLayout> </LinearLayout>
+2
View File
@@ -95,6 +95,8 @@
<string name="summary_checkbox_disable_warnings">Disabilita i messaggi di warning sullo schermo durante lo streaming</string> <string name="summary_checkbox_disable_warnings">Disabilita i messaggi di warning sullo schermo durante lo streaming</string>
<string name="category_gamepad_settings">Impostazioni Gamepad</string> <string name="category_gamepad_settings">Impostazioni Gamepad</string>
<string name="title_checkbox_multi_controller">Supporto controller multipli</string>
<string name="summary_checkbox_multi_controller">Quando disabilitato, tutti i controllers appaiono come uno solo</string>
<string name="title_seekbar_deadzone">Aggiusta deadzone degli stick analogici</string> <string name="title_seekbar_deadzone">Aggiusta deadzone degli stick analogici</string>
<string name="suffix_seekbar_deadzone">%</string> <string name="suffix_seekbar_deadzone">%</string>
+4 -2
View File
@@ -44,9 +44,9 @@
<string name="conn_establishing_title">Establishing Connection</string> <string name="conn_establishing_title">Establishing Connection</string>
<string name="conn_establishing_msg">Starting 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_metered">Warning: Your active network connection is metered!</string>
<string name="conn_client_latency">Average client-side frame latency:</string> <string name="conn_client_latency">Average frame decoding latency:</string>
<string name="conn_client_latency_hw">hardware decoder latency:</string> <string name="conn_client_latency_hw">hardware decoder latency:</string>
<string name="conn_hardware_latency">Average hardware decoder latency:</string> <string name="conn_hardware_latency">Average hardware decoding latency:</string>
<string name="conn_starting">Starting</string> <string name="conn_starting">Starting</string>
<string name="conn_error_title">Connection Error</string> <string name="conn_error_title">Connection Error</string>
<string name="conn_error_msg">Failed to start</string> <string name="conn_error_msg">Failed to start</string>
@@ -95,6 +95,8 @@
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string> <string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
<string name="category_gamepad_settings">Gamepad Settings</string> <string name="category_gamepad_settings">Gamepad Settings</string>
<string name="title_checkbox_multi_controller">Multiple controller support</string>
<string name="summary_checkbox_multi_controller">When unchecked, all controllers appear as one</string>
<string name="title_seekbar_deadzone">Adjust analog stick deadzone</string> <string name="title_seekbar_deadzone">Adjust analog stick deadzone</string>
<string name="suffix_seekbar_deadzone">%</string> <string name="suffix_seekbar_deadzone">%</string>
+9 -4
View File
@@ -26,14 +26,19 @@
android:summary="@string/summary_checkbox_disable_warnings" android:summary="@string/summary_checkbox_disable_warnings"
android:defaultValue="false" /> android:defaultValue="false" />
</PreferenceCategory> </PreferenceCategory>
<!--PreferenceCategory android:title="@string/category_gamepad_settings"> <PreferenceCategory android:title="@string/category_gamepad_settings">
<com.limelight.preferences.SeekBarPreference <!--com.limelight.preferences.SeekBarPreference
android:key="seekbar_deadzone" android:key="seekbar_deadzone"
android:defaultValue="15" android:defaultValue="15"
android:max="50" android:max="50"
android:text="@string/suffix_seekbar_deadzone" android:text="@string/suffix_seekbar_deadzone"
android:title="@string/title_seekbar_deadzone"/> android:title="@string/title_seekbar_deadzone"/-->
</PreferenceCategory--> <CheckBoxPreference
android:key="checkbox_multi_controller"
android:title="@string/title_checkbox_multi_controller"
android:summary="@string/summary_checkbox_multi_controller"
android:defaultValue="true" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_host_settings"> <PreferenceCategory android:title="@string/category_host_settings">
<CheckBoxPreference <CheckBoxPreference
android:key="checkbox_enable_sops" android:key="checkbox_enable_sops"
+1 -1
View File
@@ -4,7 +4,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:1.0.0' classpath 'com.android.tools.build:gradle:1.1.0'
} }
} }
+1 -3
View File
@@ -7,9 +7,7 @@
</configuration> </configuration>
</facet> </facet>
</component> </component>
<component name="NewModuleRootManager" inherit-compiler-output="false"> <component name="NewModuleRootManager" inherit-compiler-output="true">
<output url="file://$MODULE_DIR$/build/classes/main" />
<output-test url="file://$MODULE_DIR$/build/classes/test" />
<exclude-output /> <exclude-output />
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" /> <excludeFolder url="file://$MODULE_DIR$/.gradle" />