Compare commits

..
94 Commits
Author SHA1 Message Date
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
Cameron Gutman 3a0c1db168 Merge pull request #54 from Ansa89/italian-translation
Italian translation: update
2015-02-01 00:44:17 -05:00
Cameron Gutman bd21692323 Properly center text on the app view 2015-02-01 00:43:57 -05:00
Cameron Gutman 5ae245bdca Trim spaces from the IP address 2015-02-01 00:39:47 -05:00
Cameron Gutman d3052cd97d Set small icon by default on phones 2015-02-01 00:33:43 -05:00
Cameron Gutman 336f85a31c Fix loading bugs with uncached images 2015-01-31 22:14:12 -05:00
Cameron Gutman b01f7c796e Fix duplicated fragments 2015-01-31 17:19:54 -05:00
Cameron Gutman 56f438fe47 Fix some crashes and caching issues 2015-01-31 17:01:46 -05:00
Cameron Gutman baa5199b83 Load cached images in the background to avoid stalling the UI thread 2015-01-31 16:59:45 -05:00
Cameron Gutman 23ca62b304 Fix dp constant 2015-01-31 14:00:47 -05:00
Cameron Gutman 2c3511195c Use small mode by default on things smaller than 7 inch tablets 2015-01-31 13:21:21 -05:00
Ansa89 d31ef481f3 Italian translation: update 2015-01-31 10:03:37 +01:00
Cameron Gutman a490da5e5c Fix some caching bugs 2015-01-31 00:13:51 -05:00
Cameron Gutman 72d3576257 Fix a crash and a hang in the new computer manager code 2015-01-30 19:33:42 -05:00
Cameron Gutman ebd93a55a0 Fix small icon mode 2015-01-30 19:17:00 -05:00
Cameron Gutman 4d01e1afe6 Stub icon scaling and allow background updating of the applist 2015-01-30 18:49:01 -05:00
Cameron Gutman 9ff1386751 Add a quit confirmation dialog 2015-01-27 15:31:01 -05:00
Cameron Gutman 5fca35f0b1 Sort app list alphabetically 2015-01-26 20:58:33 -05:00
Cameron Gutman d23c763441 Remove unused imports 2015-01-26 20:50:14 -05:00
Cameron Gutman fa058c4783 Merge pull request #53 from Ansa89/italian-translation
Italian translation: update
2015-01-26 20:47:00 -05:00
Ansa89 e0ddd5f045 Italian translation: update 2015-01-26 10:53:30 +01:00
Cameron Gutman b7443451a4 Fix release build failure for beta 2015-01-25 23:38:33 -05:00
Cameron Gutman e90e4a22c4 Increment version 2015-01-25 23:35:24 -05:00
Cameron Gutman 3a53172145 Apply list mode preference immediately 2015-01-25 23:28:13 -05:00
Cameron Gutman 1dfcb7bc29 Fix root input device capture on the Nexus 9 2015-01-25 23:16:32 -05:00
Cameron Gutman 897bb76858 Forgot this file 2015-01-25 22:58:17 -05:00
Cameron Gutman bcc67269ab Add gestures to bring up the software keyboard - Long press start or tap with 3 fingers 2015-01-25 22:55:12 -05:00
Cameron Gutman 4d24c654b9 Remove the old fragment when adding the new one 2015-01-25 22:11:38 -05:00
Cameron Gutman cba44b091b Add common with GFE 2.1.x backwards compatibility 2015-01-25 21:35:03 -05:00
Cameron Gutman f2d8f8a41b Update preference text 2015-01-25 21:04:27 -05:00
Cameron Gutman 4b1c7e7e3c Fix state loss crashes 2015-01-25 21:04:13 -05:00
Cameron Gutman 1cba278876 Cache box art locally 2015-01-25 21:00:34 -05:00
Cameron Gutman 766898fdf9 Add list support back for users that don't like the grid 2015-01-25 20:23:35 -05:00
Cameron Gutman 13e91d594b Fix Lint and build issues 2015-01-25 18:50:31 -05:00
Cameron Gutman ca0a0da19f Fix fusion of computers that were re-added after becoming unreachable 2015-01-25 18:41:44 -05:00
Cameron Gutman 82cabce86e Merge pull request #33 from Ansa89/language_chooser
Add language chooser
2015-01-25 16:46:10 -05:00
Cameron Gutman 51a630995a Update common for GFE 2.2.2+ support 2015-01-22 15:29:58 -05:00
Cameron Gutman 3a74f0726c Updated libs 2015-01-22 15:29:46 -05:00
Cameron Gutman efa6c7bba0 Fix build of root package 2015-01-22 15:29:16 -05:00
Michelle Bergeron b8141542f8 Add wiki link 2015-01-17 18:44:31 -08:00
Cameron Gutman 8fc9a90207 Switch back to Maven repos of ion and androidasync packages 2014-12-13 20:41:32 -08:00
Cameron Gutman 13d707d98d Use final release of Gradle 1.0 2014-12-09 01:13:13 -08:00
Cameron Gutman aae0ff6e7a Migrate the project to Android Studio 1.0 RC4 2014-12-08 00:12:37 -08:00
Cameron Gutman 69c7b5a0d5 Update version 2014-12-03 20:52:52 -08:00
Cameron Gutman d1ad3115fa Add remote to stream config 2014-12-02 00:55:46 -08:00
Cameron Gutman 770af402a4 Reduce default 1080p60 bitrate to 20 Mbps 2014-12-02 00:55:31 -08:00
Cameron Gutman 3236c0b93a Lower the level_idc of the SPS to the minimum required for streaming at a given resolution 2014-12-01 22:58:52 -08:00
Cameron Gutman 51aacc3f38 Remove extra newlines 2014-12-01 22:39:17 -08:00
Cameron Gutman 397c6f46f9 Fix a security issue which caused input devices to remain world readable after the stream is ended 2014-12-01 22:29:16 -08:00
Cameron Gutman d00f78f859 Revert square to circle analog work since it seems to be handled correctly already 2014-12-01 22:27:02 -08:00
Cameron Gutman 29fec2e0de Add initial support for rooted devices running Lollipop with SELinux set to enforcing. This should really be improved in the future since we're modifying policies for untrusted_app. 2014-12-01 22:26:35 -08:00
Cameron Gutman 88d28665ef Attempt to fix IndexOutOfBoundsException (index 0 size 0) reported by a couple users 2014-11-30 18:34:34 -06:00
Cameron Gutman de1f4da258 Apply the square to circle plane mapping before evaluating the deadzone. Cleanup some dead code. 2014-11-30 15:52:49 -06:00
Cameron Gutman 7985be57ab Translate the analog stick values of controllers with "square" analog stick planes (DS3, DS4, and others) to the circular plane that XInput programs expect 2014-11-30 15:35:20 -06:00
Cameron Gutman a835e7aaa2 Increase DS4 controller responsiveness by ignoring historical values again 2014-11-30 12:34:30 -06:00
Ansa89 22958cfbb1 Language chooser: use constants 2014-11-29 14:40:03 +01:00
Cameron Gutman c4dc5eb9e1 Update common for faster IDR recovery 2014-11-28 22:17:42 -06:00
Cameron Gutman db758f386e Comment out unused variable 2014-11-28 22:16:46 -06:00
Cameron Gutman 3fb3eefa94 Fix Nyko Playpad input issue 2014-11-28 22:16:33 -06:00
Ansa89 9340dff45d PreferenceConfiguration.java: add language preference 2014-11-28 10:26:14 +01:00
Cameron Gutman 2d6c756e70 Always consider a PC to be remote if localIP == remoteIP 2014-11-27 21:56:20 -06:00
Cameron Gutman 03e965d449 Merge pull request #34 from Ansa89/italian-translation
Italian translation: better wording
2014-11-27 20:35:57 -06:00
Ansa89 4cea483a87 Italian translation: better wording 2014-11-24 11:53:18 +01:00
Ansa89 99aa616188 Add language chooser
Implement limelight-stream/limelight-android#32.
2014-11-24 11:47:02 +01:00
61 changed files with 1793 additions and 614 deletions

No files matched your search

+2
View File
@@ -8,6 +8,8 @@ whether in your own home or over the internet.
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development. [Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development.
Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for more detailed information or a troubleshooting guide.
##Features ##Features
* Streams any of your games from your PC to your Android device * Streams any of your games from your PC to your Android device
+7 -5
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="limelight-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4"> <module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" type="JAVA_MODULE" version="4">
<component name="FacetManager"> <component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle"> <facet type="android-gradle" name="Android-Gradle">
<configuration> <configuration>
@@ -31,6 +31,7 @@
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/nonRoot/debug" isTestSource="false" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/nonRoot/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/nonRoot/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" />
@@ -43,6 +44,7 @@
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/nonRoot/debug" isTestSource="true" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/nonRoot/debug" type="java-test-resource" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/test/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
@@ -102,17 +104,17 @@
<orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" /> <orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="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="gson-2.3.1" level="project" />
<orderEntry type="library" exported="" name="support-v4-r7" 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="okhttp-2.1.0" 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="okio-1.0.1" level="project" /> <orderEntry type="library" exported="" name="ion-2.0.5" level="project" />
<orderEntry type="library" exported="" name="androidasync-e1dfb4" 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="ion-2f46fa" level="project" /> <orderEntry type="library" exported="" name="okio-1.2.0" level="project" />
</component> </component>
</module> </module>
+14 -15
View File
@@ -5,14 +5,14 @@ apply plugin: 'com.android.application'
android { android {
compileSdkVersion 21 compileSdkVersion 21
buildToolsVersion "21.1.1" buildToolsVersion "21.1.2"
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 21 targetSdkVersion 21
versionName "3.0.1" versionName "3.1.1"
versionCode = 47 versionCode = 54
} }
productFlavors { productFlavors {
@@ -27,7 +27,7 @@ android {
buildTypes { buildTypes {
release { release {
runProguard false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
} }
} }
@@ -62,19 +62,18 @@ 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: 'bcpkix-jdk15on', version: '+'
compile group: 'com.google.android', name: 'support-v4', version:'+'
// FIXME: Pending resolution of issue #346 using custom build compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
//compile group: 'com.koushikdutta.ion', name: 'ion', version:'+' compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
compile group: 'com.google.code.gson', name: 'gson', version:'+'
compile files('libs/androidasync-e1dfb4.jar') compile group: 'com.google.android', name: 'support-v4', version:'r7'
compile files('libs/ion-2f46fa.jar') compile group: 'com.koushikdutta.ion', name: 'ion', version:'2.0.5'
compile group: 'com.google.code.gson', name: 'gson', version:'2.3.1'
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.2.0'
compile group: 'com.squareup.okio', name:'okio', version:'1.2.0'
compile 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')
compile files('libs/tinyrtsp.jar') compile files('libs/tinyrtsp.jar')
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -68,7 +68,7 @@
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" > android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.Connection" /> android:value="com.limelight.AppView" />
</activity> </activity>
<service <service
android:name=".discovery.DiscoveryService" android:name=".discovery.DiscoveryService"
+311 -109
View File
@@ -1,109 +1,237 @@
package com.limelight; package com.limelight;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.StringReader;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.List; import java.util.List;
import java.util.Locale;
import org.xmlpull.v1.XmlPullParserException; import java.util.UUID;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.AppGridAdapter; import com.limelight.grid.AppGridAdapter;
import com.limelight.nvstream.http.GfeHttpResponseException; import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp; import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.NvHTTP;
import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.CacheHelper;
import com.limelight.utils.Dialog; import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog; import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper; import com.limelight.utils.UiHelper;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog;
import android.app.Service;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AbsListView;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.GridView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.AdapterContextMenuInfo;
public class AppView extends Activity { public class AppView extends Activity implements AdapterFragmentCallbacks {
private AppGridAdapter appGridAdapter; private AppGridAdapter appGridAdapter;
private InetAddress ipAddress; private String uuidString;
private String uniqueId;
private boolean remote;
private boolean firstLoad = true;
private final static int RESUME_ID = 1; private ComputerDetails computer;
private ComputerManagerService.ApplistPoller poller;
private SpinnerDialog blockingLoadSpinner;
private String lastRawApplist;
private int consecutiveAppListFailures = 0;
private final static int CONSECUTIVE_FAILURE_LIMIT = 3;
private final static int START_OR_RESUME_ID = 1;
private final static int QUIT_ID = 2; private final static int QUIT_ID = 2;
private final static int CANCEL_ID = 3; private final static int CANCEL_ID = 3;
private final static int START_WTIH_QUIT = 4;
public final static String ADDRESS_EXTRA = "Address";
public final static String UNIQUEID_EXTRA = "UniqueId";
public final static String NAME_EXTRA = "Name"; public final static String NAME_EXTRA = "Name";
public final static String REMOTE_EXTRA = "Remote"; public final static String UUID_EXTRA = "UUID";
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder);
// Wait in a separate thread to avoid stalling the UI
new Thread() {
@Override
public void run() {
// Wait for the binder to be ready
localBinder.waitForReady();
// Now make the binder visible
managerBinder = localBinder;
// Get the computer object
computer = managerBinder.getComputer(UUID.fromString(uuidString));
try {
appGridAdapter = new AppGridAdapter(AppView.this,
PreferenceConfiguration.readPreferences(AppView.this).listMode,
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
computer, managerBinder.getUniqueId());
} catch (Exception e) {
e.printStackTrace();
finish();
return;
}
// Start updates
startComputerUpdates();
// Load the app grid with cached data (if possible)
populateAppGridWithCache();
getFragmentManager().beginTransaction()
.replace(R.id.appFragmentContainer, new AdapterFragment())
.commitAllowingStateLoss();
}
}.start();
}
public void onServiceDisconnected(ComponentName className) {
managerBinder = null;
}
};
private InetAddress getAddress() {
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp : computer.remoteIp;
}
private void startComputerUpdates() {
if (managerBinder == null) {
return;
}
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(ComputerDetails details) {
// Don't care about other computers
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
return;
}
if (details.state != ComputerDetails.State.ONLINE) {
consecutiveAppListFailures++;
if (consecutiveAppListFailures >= CONSECUTIVE_FAILURE_LIMIT) {
// The PC is unreachable now
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
// Display a toast to the user and quit the activity
Toast.makeText(AppView.this, getResources().getText(R.string.lost_connection), Toast.LENGTH_SHORT).show();
finish();
}
});
}
return;
}
consecutiveAppListFailures = 0;
// App list is the same or empty; nothing to do
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
return;
}
try {
lastRawApplist = details.rawAppList;
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
} catch (Exception ignored) {}
}
});
if (poller == null) {
poller = managerBinder.createAppListPoller(computer);
}
poller.start();
}
private void stopComputerUpdates() {
if (poller != null) {
poller.stop();
}
if (managerBinder != null) {
managerBinder.stopPolling();
}
}
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
setContentView(R.layout.activity_app_view); setContentView(R.layout.activity_app_view);
UiHelper.notifyNewRootView(this); UiHelper.notifyNewRootView(this);
byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA); uuidString = getIntent().getStringExtra(UUID_EXTRA);
uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA);
remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false);
if (address == null || uniqueId == null) {
finish();
return;
}
String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA); String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA);
TextView label = (TextView) findViewById(R.id.appListText); TextView label = (TextView) findViewById(R.id.appListText);
setTitle(labelText); setTitle(labelText);
label.setText(labelText); label.setText(labelText);
try { // Bind to the computer manager service
ipAddress = InetAddress.getByAddress(address); bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
} catch (UnknownHostException e) { Service.BIND_AUTO_CREATE);
e.printStackTrace();
finish();
return;
} }
// Setup the list view private void populateAppGridWithCache() {
GridView appGrid = (GridView) findViewById(R.id.appGridView);
try { try {
appGridAdapter = new AppGridAdapter(this, ipAddress, uniqueId); // Try to load from cache
lastRawApplist = CacheHelper.readInputStreamToString(CacheHelper.openCacheFileForInput(getCacheDir(), "applist", uuidString));
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
updateUiWithAppList(applist);
LimeLog.info("Loaded applist from cache");
} catch (Exception e) { } catch (Exception e) {
if (lastRawApplist != null) {
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
e.printStackTrace(); e.printStackTrace();
finish();
return;
} }
appGrid.setAdapter(appGridAdapter); LimeLog.info("Loading applist from the network");
appGrid.setOnItemClickListener(new OnItemClickListener() { // We'll need to load from the network
@Override loadAppsBlocking();
public void onItemClick(AdapterView<?> arg0, View arg1, int pos, }
long id) {
AppObject app = (AppObject) appGridAdapter.getItem(pos);
if (app == null || app.app == null) {
return;
} }
// Only open the context menu if something is running, otherwise start it private void loadAppsBlocking() {
if (getRunningAppId() != -1) { blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
openContextMenu(arg1); getResources().getString(R.string.applist_refresh_msg), true);
} else {
doStart(app.app);
}
}
});
registerForContextMenu(appGrid);
} }
@Override @Override
@@ -112,27 +240,30 @@ public class AppView extends Activity {
SpinnerDialog.closeDialogs(this); SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs(); Dialog.closeDialogs();
if (managerBinder != null) {
unbindService(serviceConnection);
}
} }
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
// Display the error message if it was the startComputerUpdates();
// first load, but just kill the activity }
// on subsequent errors
updateAppList(firstLoad); @Override
firstLoad = false; protected void onPause() {
super.onPause();
stopComputerUpdates();
} }
private int getRunningAppId() { private int getRunningAppId() {
int runningAppId = -1; int runningAppId = -1;
for (int i = 0; i < appGridAdapter.getCount(); i++) { for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject app = (AppObject) appGridAdapter.getItem(i); AppObject app = (AppObject) appGridAdapter.getItem(i);
if (app.app == null) {
continue;
}
if (app.app.getIsRunning()) { if (app.app.getIsRunning()) {
runningAppId = app.app.getAppId(); runningAppId = app.app.getAppId();
break; break;
@@ -147,18 +278,14 @@ public class AppView extends Activity {
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()) {
menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume)); menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit)); menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
} }
else { else {
menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_quit_and_start)); menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel)); menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
} }
} }
@@ -168,18 +295,61 @@ public class AppView extends Activity {
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();
AppObject app = (AppObject) appGridAdapter.getItem(info.position); final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
switch (item.getItemId()) { switch (item.getItemId()) {
case RESUME_ID: case START_WTIH_QUIT:
// Display a confirmation dialog first
displayQuitConfirmationDialog(new Runnable() {
@Override
public void run() {
doStart(app.app);
}
}, null);
return true;
case START_OR_RESUME_ID:
// Resume is the same as start for us // Resume is the same as start for us
doStart(app.app); doStart(app.app);
return true; return true;
case QUIT_ID: case QUIT_ID:
// Display a confirmation dialog first
displayQuitConfirmationDialog(new Runnable() {
@Override
public void run() {
doQuit(app.app); doQuit(app.app);
}
}, null);
return true; return true;
case CANCEL_ID: case CANCEL_ID:
@@ -190,61 +360,57 @@ public class AppView extends Activity {
} }
} }
private void updateAppList(final boolean displayError) { private void updateUiWithAppList(final List<NvApp> appList) {
final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
getResources().getString(R.string.applist_refresh_msg), true);
new Thread() {
@Override
public void run() {
NvHTTP httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
try {
final List<NvApp> appList = httpConn.getAppList();
AppView.this.runOnUiThread(new Runnable() { AppView.this.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
appGridAdapter.clear(); boolean updated = false;
for (NvApp app : appList) { for (NvApp app : appList) {
appGridAdapter.addApp(new AppObject(app)); boolean foundExistingApp = false;
// Try to update an existing app in the list first
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
if (existingApp.app.getAppId() == app.getAppId()) {
// Found the app; update its properties
if (existingApp.app.getIsRunning() != app.getIsRunning()) {
existingApp.app.setIsRunningBoolean(app.getIsRunning());
updated = true;
}
if (!existingApp.app.getAppName().equals(app.getAppName())) {
existingApp.app.setAppName(app.getAppName());
updated = true;
} }
foundExistingApp = true;
break;
}
}
if (!foundExistingApp) {
// This app must be new
appGridAdapter.addApp(new AppObject(app));
updated = true;
}
}
if (updated) {
appGridAdapter.notifyDataSetChanged(); appGridAdapter.notifyDataSetChanged();
} }
});
// Success case
return;
} catch (GfeHttpResponseException ignored) {
} catch (IOException ignored) {
} catch (XmlPullParserException ignored) {
} finally {
spinner.dismiss();
}
if (displayError) {
Dialog.displayDialog(AppView.this, getResources().getString(R.string.applist_refresh_error_title),
getResources().getString(R.string.applist_refresh_error_msg), true);
}
else {
// Just finish the activity immediately
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
finish();
} }
}); });
} }
}
}.start();
}
private void doStart(NvApp app) { private void doStart(NvApp app) {
Intent intent = new Intent(this, Game.class); Intent intent = new Intent(this, Game.class);
intent.putExtra(Game.EXTRA_HOST, ipAddress.getHostAddress()); 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_APP, app.getAppName());
intent.putExtra(Game.EXTRA_UNIQUEID, uniqueId); intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
intent.putExtra(Game.EXTRA_STREAMING_REMOTE, remote); intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
computer.reachability != ComputerDetails.Reachability.LOCAL);
startActivity(intent); startActivity(intent);
} }
@@ -256,20 +422,25 @@ public class AppView extends Activity {
NvHTTP httpConn; NvHTTP httpConn;
String message; String message;
try { try {
httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this)); httpConn = new NvHTTP(getAddress(),
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(AppView.this));
if (httpConn.quitApp()) { if (httpConn.quitApp()) {
message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName(); message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName();
} }
else { else {
message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName(); message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName();
} }
updateAppList(true);
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
message = getResources().getString(R.string.error_unknown_host); message = getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
message = getResources().getString(R.string.error_404); message = getResources().getString(R.string.error_404);
} catch (Exception e) { } catch (Exception e) {
message = e.getMessage(); message = e.getMessage();
} finally {
// Trigger a poll immediately
if (poller != null) {
poller.pollNow();
}
} }
final String toastMessage = message; final String toastMessage = message;
@@ -283,10 +454,41 @@ public class AppView extends Activity {
}).start(); }).start();
} }
@Override
public int getAdapterFragmentLayoutId() {
return PreferenceConfiguration.readPreferences(this).listMode ?
R.layout.list_view : (PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
R.layout.app_grid_view_small : R.layout.app_grid_view);
}
@Override
public void receiveAbsListView(AbsListView listView) {
listView.setAdapter(appGridAdapter);
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
AppObject app = (AppObject) appGridAdapter.getItem(pos);
// Only open the context menu if something is running, otherwise start it
if (getRunningAppId() != -1) {
openContextMenu(arg1);
} else {
doStart(app.app);
}
}
});
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;
} }
+61 -7
View File
@@ -16,18 +16,22 @@ import com.limelight.nvstream.av.video.VideoDecoderRenderer;
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;
import com.limelight.ui.GameGestures;
import com.limelight.utils.Dialog; import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog; import com.limelight.utils.SpinnerDialog;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
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;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.SystemClock;
import android.view.Display; import android.view.Display;
import android.view.InputDevice; import android.view.InputDevice;
import android.view.KeyEvent; import android.view.KeyEvent;
@@ -41,25 +45,31 @@ import android.view.View.OnTouchListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.Window; import android.view.Window;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast; import android.widget.Toast;
import java.util.Locale;
public class Game extends Activity implements SurfaceHolder.Callback, public class Game extends Activity implements SurfaceHolder.Callback,
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener, OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
OnSystemUiVisibilityChangeListener OnSystemUiVisibilityChangeListener, GameGestures
{ {
private int lastMouseX = Integer.MIN_VALUE; private int lastMouseX = Integer.MIN_VALUE;
private int lastMouseY = Integer.MIN_VALUE; private int lastMouseY = Integer.MIN_VALUE;
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 static final int THREE_FINGER_TAP_THRESHOLD = 300;
private ControllerHandler controllerHandler; private ControllerHandler controllerHandler;
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;
@@ -87,6 +97,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
// We don't want a title bar // We don't want a title bar
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
@@ -171,12 +188,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION) != 0) VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION) != 0)
.enableLocalAudioPlayback(prefConfig.playHostAudio) .enableLocalAudioPlayback(prefConfig.playHostAudio)
.setMaxPacketSize(remote ? 1024 : 1292) .setMaxPacketSize(remote ? 1024 : 1292)
.setRemote(remote)
.build(); .build();
// 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, 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()) {
@@ -225,7 +246,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
@@ -261,6 +282,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();
@@ -291,7 +315,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() {
@@ -467,6 +491,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
} }
} }
@Override
public void showKeyboard() {
LimeLog.info("Showing keyboard overlay");
InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY);
}
// Returns true if the event was consumed // Returns true if the event was consumed
private boolean handleMotionEvent(MotionEvent event) { private boolean handleMotionEvent(MotionEvent event) {
// Pass through keyboard input if we're not grabbing // Pass through keyboard input if we're not grabbing
@@ -490,6 +521,21 @@ public class Game extends Activity implements SurfaceHolder.Callback,
int eventX = (int)event.getX(actionIndex); int eventX = (int)event.getX(actionIndex);
int eventY = (int)event.getY(actionIndex); int eventY = (int)event.getY(actionIndex);
// Special handling for 3 finger gesture
if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN &&
event.getPointerCount() == 3) {
// Three fingers down
threeFingerDownTime = SystemClock.uptimeMillis();
// Cancel the first and second touches to avoid
// erroneous events
for (TouchContext aTouchContext : touchContextMap) {
aTouchContext.cancelTouch();
}
return true;
}
TouchContext context = getTouchContext(actionIndex); TouchContext context = getTouchContext(actionIndex);
if (context == null) { if (context == null) {
return false; return false;
@@ -503,8 +549,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
break; break;
case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_UP: case MotionEvent.ACTION_UP:
if (event.getPointerCount() == 1) {
// All fingers up
if (SystemClock.uptimeMillis() - threeFingerDownTime < THREE_FINGER_TAP_THRESHOLD) {
// This is a 3 finger tap to bring up the keyboard
showKeyboard();
return true;
}
}
context.touchUpEvent(eventX, eventY); context.touchUpEvent(eventX, eventY);
if (actionIndex == 0 && event.getPointerCount() > 1) { if (actionIndex == 0 && event.getPointerCount() > 1 && !context.isCancelled()) {
// The original secondary touch now becomes primary // The original secondary touch now becomes primary
context.touchDownEvent((int)event.getX(1), (int)event.getY(1)); context.touchDownEvent((int)event.getX(1), (int)event.getY(1));
} }
+63 -39
View File
@@ -4,6 +4,7 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.Locale;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.binding.crypto.AndroidCryptoProvider; import com.limelight.binding.crypto.AndroidCryptoProvider;
@@ -16,7 +17,10 @@ import com.limelight.nvstream.http.PairingManager;
import com.limelight.nvstream.http.PairingManager.PairState; import com.limelight.nvstream.http.PairingManager.PairState;
import com.limelight.nvstream.wol.WakeOnLanSender; import com.limelight.nvstream.wol.WakeOnLanSender;
import com.limelight.preferences.AddComputerManually; import com.limelight.preferences.AddComputerManually;
import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.preferences.StreamSettings; import com.limelight.preferences.StreamSettings;
import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.Dialog; import com.limelight.utils.Dialog;
import com.limelight.utils.UiHelper; import com.limelight.utils.UiHelper;
@@ -35,20 +39,20 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
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.GridView;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.Toast; import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.AdapterContextMenuInfo;
public class PcView extends Activity { public class PcView extends Activity implements AdapterFragmentCallbacks {
private RelativeLayout noPcFoundLayout; private RelativeLayout noPcFoundLayout;
private PcGridAdapter pcGridAdapter; private PcGridAdapter pcGridAdapter;
private ComputerManagerService.ComputerManagerBinder managerBinder; private ComputerManagerService.ComputerManagerBinder managerBinder;
private boolean freezeUpdates, runningPolling; private boolean freezeUpdates, runningPolling;
private ServiceConnection serviceConnection = new ServiceConnection() { private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) { public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder = final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder); ((ComputerManagerService.ComputerManagerBinder)binder);
@@ -102,27 +106,6 @@ public class PcView extends Activity {
ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton); ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton);
ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc); ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc);
GridView pcGrid = (GridView) findViewById(R.id.pcGridView);
pcGrid.setAdapter(pcGridAdapter);
pcGrid.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos);
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
// Do nothing
} else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
// Open the context menu if a PC is offline
openContextMenu(arg1);
} else if (computer.details.pairState != PairState.PAIRED) {
// Pair an unpaired machine by default
doPair(computer.details);
} else {
doAppList(computer.details);
}
}
});
registerForContextMenu(pcGrid);
settingsButton.setOnClickListener(new OnClickListener() { settingsButton.setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@@ -137,6 +120,10 @@ public class PcView extends Activity {
} }
}); });
getFragmentManager().beginTransaction()
.replace(R.id.pcFragmentContainer, new AdapterFragment())
.commitAllowingStateLoss();
noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout); noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout);
if (pcGridAdapter.getCount() == 0) { if (pcGridAdapter.getCount() == 0) {
noPcFoundLayout.setVisibility(View.VISIBLE); noPcFoundLayout.setVisibility(View.VISIBLE);
@@ -151,11 +138,20 @@ public class PcView extends Activity {
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
// Bind to the computer manager service // Bind to the computer manager service
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection, bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE); Service.BIND_AUTO_CREATE);
pcGridAdapter = new PcGridAdapter(this); pcGridAdapter = new PcGridAdapter(this,
PreferenceConfiguration.readPreferences(this).listMode,
PreferenceConfiguration.readPreferences(this).smallIconMode);
initializeViews(); initializeViews();
} }
@@ -241,8 +237,7 @@ public class PcView extends Activity {
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;
} }
@@ -258,7 +253,10 @@ public class PcView extends Activity {
} }
else { else {
menu.add(Menu.NONE, APP_LIST_ID, 1, getResources().getString(R.string.pcview_menu_app_list)); menu.add(Menu.NONE, APP_LIST_ID, 1, getResources().getString(R.string.pcview_menu_app_list));
menu.add(Menu.NONE, UNPAIR_ID, 2, getResources().getString(R.string.pcview_menu_unpair_pc));
// FIXME: We used to be able to unpair here but it's been broken since GFE 2.1.x, so I've replaced
// it with delete which actually work
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
} }
} }
@@ -472,16 +470,7 @@ public class PcView extends Activity {
Intent i = new Intent(this, AppView.class); Intent i = new Intent(this, AppView.class);
i.putExtra(AppView.NAME_EXTRA, computer.name); i.putExtra(AppView.NAME_EXTRA, computer.name);
i.putExtra(AppView.UNIQUEID_EXTRA, managerBinder.getUniqueId()); i.putExtra(AppView.UUID_EXTRA, computer.uuid.toString());
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
i.putExtra(AppView.ADDRESS_EXTRA, computer.localIp.getAddress());
i.putExtra(AppView.REMOTE_EXTRA, false);
}
else {
i.putExtra(AppView.ADDRESS_EXTRA, computer.remoteIp.getAddress());
i.putExtra(AppView.REMOTE_EXTRA, true);
}
startActivity(i); startActivity(i);
} }
@@ -527,6 +516,7 @@ public class PcView extends Activity {
if (details.equals(computer.details)) { if (details.equals(computer.details)) {
pcGridAdapter.removeComputer(computer); pcGridAdapter.removeComputer(computer);
pcGridAdapter.notifyDataSetChanged();
break; break;
} }
} }
@@ -539,7 +529,7 @@ public class PcView extends Activity {
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;
} }
@@ -561,10 +551,44 @@ public class PcView extends Activity {
pcGridAdapter.notifyDataSetChanged(); pcGridAdapter.notifyDataSetChanged();
} }
@Override
public int getAdapterFragmentLayoutId() {
return PreferenceConfiguration.readPreferences(this).listMode ?
R.layout.list_view : (PreferenceConfiguration.readPreferences(this).smallIconMode ?
R.layout.pc_grid_view_small : R.layout.pc_grid_view);
}
@Override
public void receiveAbsListView(AbsListView listView) {
listView.setAdapter(pcGridAdapter);
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos);
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
// Do nothing
} else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
// Open the context menu if a PC is offline
openContextMenu(arg1);
} else if (computer.details.pairState != PairState.PAIRED) {
// Pair an unpaired machine by default
doPair(computer.details);
} else {
doAppList(computer.details);
}
}
});
registerForContextMenu(listView);
}
public class ComputerObject { public class ComputerObject {
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;
@@ -10,27 +12,15 @@ import android.view.MotionEvent;
import com.limelight.LimeLog; import com.limelight.LimeLog;
import com.limelight.nvstream.NvConnection; import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.ControllerPacket; import com.limelight.nvstream.input.ControllerPacket;
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 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;
private static final int EMULATING_SPECIAL = 0x1; private static final int EMULATING_SPECIAL = 0x1;
@@ -39,18 +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 Vector2d normalizedInputVector = 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 final GameGestures gestures;
private boolean hasGameController; private boolean hasGameController;
public ControllerHandler(NvConnection conn, 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.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.
@@ -77,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) {
@@ -101,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);
@@ -123,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
{ {
@@ -139,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);
@@ -174,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;
} }
} }
} }
@@ -184,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;
} }
} }
@@ -221,66 +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
else if (devName.contains("NYKO PLAYPAD")) {
context.hatXAxis = -1;
context.hatYAxis = -1;
}
} }
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.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;
@@ -319,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:
@@ -330,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
@@ -365,171 +425,136 @@ 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;
} }
private Vector2d handleDeadZone(float x, float y, float deadzoneRadius) { private Vector2d populateCachedVector(float x, float y) {
// Reinitialize our cached Vector2d object // Reinitialize our cached Vector2d object
inputVector.initialize(x, y); inputVector.initialize(x, y);
if (inputVector.getMagnitude() <= deadzoneRadius) {
// Deadzone -- return the zero vector
return Vector2d.ZERO;
}
else {
/*
FIXME: We're not normalizing here because we let the computer handle the deadzones.
Normalizing can make the deadzones larger than they should be after the computer also
evaluates the deadzone
// Scale the input based on the distance from the deadzone
inputVector.getNormalized(normalizedInputVector);
normalizedInputVector.scalarMultiply((inputVector.getMagnitude() - deadzoneRadius) / (1.0f - deadzoneRadius));
// Bound the X value to -1.0 to 1.0
if (normalizedInputVector.getX() > 1.0f) {
normalizedInputVector.setX(1.0f);
}
else if (normalizedInputVector.getX() < -1.0f) {
normalizedInputVector.setX(-1.0f);
}
// Bound the Y value to -1.0 to 1.0
if (normalizedInputVector.getY() > 1.0f) {
normalizedInputVector.setY(1.0f);
}
else if (normalizedInputVector.getY() < -1.0f) {
normalizedInputVector.setY(-1.0f);
}
return normalizedInputVector;
*/
return inputVector; return inputVector;
} }
private void handleDeadZone(Vector2d stickVector, float deadzoneRadius) {
if (stickVector.getMagnitude() <= deadzoneRadius) {
// Deadzone
stickVector.initialize(0, 0);
} }
private void handleAxisSet(ControllerMapping mapping, float lsX, float lsY, float rsX, // We're not normalizing here because we let the computer handle the deadzones.
// Normalizing can make the deadzones larger than they should be after the computer also
// evaluates the deadzone.
}
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 = handleDeadZone(lsX, lsY, mapping.leftStickDeadzoneRadius); Vector2d leftStickVector = populateCachedVector(lsX, lsY);
leftStickX = (short) (leftStickVector.getX() * 0x7FFE); handleDeadZone(leftStickVector, context.leftStickDeadzoneRadius);
leftStickY = (short) (-leftStickVector.getY() * 0x7FFE);
context.leftStickX = (short) (leftStickVector.getX() * 0x7FFE);
context.leftStickY = (short) (-leftStickVector.getY() * 0x7FFE);
} }
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) { if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
Vector2d rightStickVector = handleDeadZone(rsX, rsY, mapping.rightStickDeadzoneRadius); Vector2d rightStickVector = populateCachedVector(rsX, rsY);
rightStickX = (short) (rightStickVector.getX() * 0x7FFE); handleDeadZone(rightStickVector, context.rightStickDeadzoneRadius);
rightStickY = (short) (-rightStickVector.getY() * 0x7FFE);
context.rightStickX = (short) (rightStickVector.getX() * 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;
// Replay the full history before getting the current values // We purposefully ignore the historical values in the motion event as it makes
for (int i = 0; i < event.getHistorySize(); i++) { // the controller feel sluggish for some users.
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
lsX = event.getHistoricalAxisValue(mapping.leftStickXAxis, i); if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
lsY = event.getHistoricalAxisValue(mapping.leftStickYAxis, i); lsX = event.getAxisValue(context.leftStickXAxis);
lsY = event.getAxisValue(context.leftStickYAxis);
} }
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) { if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
rsX = event.getHistoricalAxisValue(mapping.rightStickXAxis, i); rsX = event.getAxisValue(context.rightStickXAxis);
rsY = event.getHistoricalAxisValue(mapping.rightStickYAxis, i); rsY = event.getAxisValue(context.rightStickYAxis);
} }
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) { if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
lt = event.getHistoricalAxisValue(mapping.leftTriggerAxis, i); lt = event.getAxisValue(context.leftTriggerAxis);
rt = event.getHistoricalAxisValue(mapping.rightTriggerAxis, i); rt = event.getAxisValue(context.rightTriggerAxis);
} }
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) { if (context.hatXAxis != -1 && context.hatYAxis != -1) {
hatX = event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_X, i);
hatY = event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_Y, i);
}
handleAxisSet(mapping, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY);
}
// Now handle the current set of values
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
lsX = event.getAxisValue(mapping.leftStickXAxis);
lsY = event.getAxisValue(mapping.leftStickYAxis);
}
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
rsX = event.getAxisValue(mapping.rightStickXAxis);
rsY = event.getAxisValue(mapping.rightStickYAxis);
}
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
lt = event.getAxisValue(mapping.leftTriggerAxis);
rt = event.getAxisValue(mapping.rightTriggerAxis);
}
if (mapping.hatXAxis != -1 && mapping.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;
} }
@@ -548,75 +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:
inputMap &= ~ControllerPacket.PLAY_FLAG; if (SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) {
// FIXME: The stock keyboard doesn't have controller focus so isn't usable. I'm not enabling this shortcut
// until we have a custom keyboard or some other fix
//gestures.showKeyboard();
}
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);
@@ -625,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);
@@ -642,109 +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:
inputMap |= ControllerPacket.PLAY_FLAG; if (event.getRepeatCount() == 0) {
context.startDownTime = SystemClock.uptimeMillis();
}
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;
@@ -764,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;
@@ -9,10 +9,12 @@ public class TouchContext {
private int originalTouchX = 0; private int originalTouchX = 0;
private int originalTouchY = 0; private int originalTouchY = 0;
private long originalTouchTime = 0; private long originalTouchTime = 0;
private boolean cancelled;
private 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;
@@ -56,12 +58,17 @@ public class TouchContext {
originalTouchX = lastTouchX = eventX; originalTouchX = lastTouchX = eventX;
originalTouchY = lastTouchY = eventY; originalTouchY = lastTouchY = eventY;
originalTouchTime = System.currentTimeMillis(); originalTouchTime = System.currentTimeMillis();
cancelled = false;
return true; return true;
} }
public void touchUpEvent(int eventX, int eventY) public void touchUpEvent(int eventX, int eventY)
{ {
if (cancelled) {
return;
}
if (isTap()) if (isTap())
{ {
byte buttonIndex = getMouseButtonIndex(); byte buttonIndex = getMouseButtonIndex();
@@ -102,4 +109,12 @@ public class TouchContext {
return true; return true;
} }
public void cancelTouch() {
cancelled = true;
}
public boolean isCancelled() {
return cancelled;
}
} }
@@ -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
@@ -1,7 +1,7 @@
package com.limelight.binding.input.evdev; package com.limelight.binding.input.evdev;
import java.io.IOException; import android.os.Build;
import java.io.OutputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Locale; import java.util.Locale;
@@ -12,30 +12,32 @@ public class EvdevReader {
System.loadLibrary("evdev_reader"); System.loadLibrary("evdev_reader");
} }
public static void patchSeLinuxPolicies() {
//
// FIXME: We REALLY shouldn't being changing permissions on the input devices like this.
// We should probably do something clever with a separate daemon and talk via a localhost
// socket. We don't return the SELinux policies back to default after we're done which I feel
// bad about, but we do chmod the input devices back so I don't think any additional attack surface
// remains opened after streaming other than listing the /dev/input directory which you wouldn't
// normally be able to do with SELinux enforcing on Lollipop.
//
// We need to modify SELinux policies to allow us to capture input devices on Lollipop and possibly other
// more restrictive ROMs. Per Chainfire's SuperSU documentation, the supolicy binary is provided on
// 4.4 and later to do live SELinux policy changes.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
EvdevShell shell = EvdevShell.getInstance();
shell.runCommand("supolicy --live \"allow untrusted_app input_device dir { open getattr read search }\" " +
"\"allow untrusted_app input_device chr_file { open read write ioctl }\"");
}
}
// Requires root to chmod /dev/input/eventX // Requires root to chmod /dev/input/eventX
public static boolean setPermissions(String[] files, int octalPermissions) { public static void setPermissions(String[] files, int octalPermissions) {
ProcessBuilder builder = new ProcessBuilder("su"); EvdevShell shell = EvdevShell.getInstance();
try {
Process p = builder.start();
OutputStream stdin = p.getOutputStream();
for (String file : files) { for (String file : files) {
stdin.write(String.format((Locale)null, "chmod %o %s\n", octalPermissions, file).getBytes("UTF-8")); shell.runCommand(String.format((Locale)null, "chmod %o %s", octalPermissions, file));
} }
stdin.write("exit\n".getBytes("UTF-8"));
stdin.flush();
p.waitFor();
p.destroy();
return true;
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
} }
// Returns the fd to be passed to other function or -1 on error // Returns the fd to be passed to other function or -1 on error
@@ -0,0 +1,116 @@
package com.limelight.binding.input.evdev;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Scanner;
import java.util.UUID;
public class EvdevShell {
private OutputStream stdin;
private InputStream stdout;
private Process shell;
private final String uuidString = UUID.randomUUID().toString();
private static final EvdevShell globalShell = new EvdevShell();
public static EvdevShell getInstance() {
return globalShell;
}
public void startShell() {
ProcessBuilder builder = new ProcessBuilder("su");
try {
// Redirect stderr to stdout
builder.redirectErrorStream(true);
shell = builder.start();
stdin = shell.getOutputStream();
stdout = shell.getInputStream();
} catch (IOException e) {
// This is unexpected
e.printStackTrace();
// Kill the shell if it spawned
if (stdin != null) {
try {
stdin.close();
} catch (IOException e1) {
e1.printStackTrace();
} finally {
stdin = null;
}
}
if (stdout != null) {
try {
stdout.close();
} catch (IOException e1) {
e1.printStackTrace();
} finally {
stdout = null;
}
}
if (shell != null) {
shell.destroy();
shell = null;
}
}
}
public void runCommand(String command) {
if (shell == null) {
// Shell never started
return;
}
try {
// Write the command followed by an echo with our UUID
stdin.write((command+'\n').getBytes("UTF-8"));
stdin.write(("echo "+uuidString+'\n').getBytes("UTF-8"));
stdin.flush();
// This is the only command in flight so we can use a scanner
// without worrying about it eating too many characters
Scanner scanner = new Scanner(stdout);
while (scanner.hasNext()) {
if (scanner.next().contains(uuidString)) {
// Our command ran
return;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void stopShell() throws InterruptedException {
boolean exitWritten = false;
if (shell == null) {
// Shell never started
return;
}
try {
stdin.write("exit\n".getBytes("UTF-8"));
exitWritten = true;
} catch (IOException e) {
// We'll destroy the process without
// waiting for it to terminate since
// we don't know whether our exit command made it
e.printStackTrace();
}
if (exitWritten) {
try {
shell.waitFor();
} finally {
shell.destroy();
}
}
else {
shell.destroy();
}
}
}
@@ -4,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,
@@ -20,6 +20,8 @@ public class EvdevWatcher {
private EvdevListener listener; private EvdevListener listener;
private Thread startThread; private Thread startThread;
private static boolean patchedSeLinuxPolicies = false;
private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) { private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) {
@Override @Override
public void onEvent(int event, String fileName) { public void onEvent(int event, String fileName) {
@@ -117,6 +119,15 @@ public class EvdevWatcher {
startThread = new Thread() { startThread = new Thread() {
@Override @Override
public void run() { public void run() {
// Initialize the root shell
EvdevShell.getInstance().startShell();
// Patch SELinux policies (if needed)
if (!patchedSeLinuxPolicies) {
EvdevReader.patchSeLinuxPolicies();
patchedSeLinuxPolicies = true;
}
// List all files and allow us access // List all files and allow us access
File[] files = rundownWithPermissionsChange(0666); File[] files = rundownWithPermissionsChange(0666);
@@ -139,7 +150,12 @@ public class EvdevWatcher {
} }
// Giveup eventX permissions // Giveup eventX permissions
rundownWithPermissionsChange(066); rundownWithPermissionsChange(0660);
// Kill the root shell
try {
EvdevShell.getInstance().stopShell();
} catch (InterruptedException e) {}
} }
}; };
startThread.start(); startThread.start();
@@ -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() {
@@ -411,6 +411,23 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
SeqParameterSet sps = SeqParameterSet.read(spsBuf); SeqParameterSet sps = SeqParameterSet.read(spsBuf);
// Some decoders rely on H264 level to decide how many buffers are needed
// Since we only need one frame buffered, we'll set the level as low as we can
// for known resolution combinations
if (initialWidth == 1280 && initialHeight == 720) {
// Max 5 buffered frames at 1280x720x60
LimeLog.info("Patching level_idc to 32");
sps.level_idc = 32;
}
else if (initialWidth == 1920 && initialHeight == 1080) {
// Max 4 buffered frames at 1920x1080x64
LimeLog.info("Patching level_idc to 42");
sps.level_idc = 42;
}
else {
// Leave the profile alone (currently 5.0)
}
// TI OMAP4 requires a reference frame count of 1 to decode successfully. Exynos 4 // TI OMAP4 requires a reference frame count of 1 to decode successfully. Exynos 4
// also requires this fixup. // also requires this fixup.
// //
@@ -541,8 +558,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
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,12 @@ import com.limelight.LimeLog;
public class MediaCodecHelper { public class MediaCodecHelper {
public static final List<String> preferredDecoders; private static final List<String> preferredDecoders;
public static final List<String> blacklistedDecoderPrefixes; private static final List<String> blacklistedDecoderPrefixes;
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes; private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
public static final List<String> whitelistedAdaptiveResolutionPrefixes; private static final List<String> whitelistedAdaptiveResolutionPrefixes;
public static final List<String> baselineProfileHackPrefixes; private static final List<String> baselineProfileHackPrefixes;
static { static {
preferredDecoders = new LinkedList<String>(); preferredDecoders = new LinkedList<String>();
@@ -146,7 +146,7 @@ public class MediaCodecHelper {
return str; return str;
} }
public static MediaCodecInfo findPreferredDecoder() { private static MediaCodecInfo findPreferredDecoder() {
// This is a different algorithm than the other findXXXDecoder functions, // This is a different algorithm than the other findXXXDecoder functions,
// because we want to evaluate the decoders in our list's order // because we want to evaluate the decoders in our list's order
// rather than MediaCodecList's order // rather than MediaCodecList's order
@@ -217,7 +217,7 @@ public class MediaCodecHelper {
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly // since some bad decoders can throw IllegalArgumentExceptions unexpectedly
// and we want to be sure all callers are handling this possibility // and we want to be sure all callers are handling this possibility
@SuppressWarnings("RedundantThrows") @SuppressWarnings("RedundantThrows")
public static MediaCodecInfo findKnownSafeDecoder() throws Exception { private static MediaCodecInfo findKnownSafeDecoder() throws Exception {
for (MediaCodecInfo codecInfo : getMediaCodecList()) { for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders // Skip encoders
if (codecInfo.isEncoder()) { if (codecInfo.isEncoder()) {
@@ -1,16 +1,23 @@
package com.limelight.computers; package com.limelight.computers;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import com.limelight.LimeLog; import com.limelight.LimeLog;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.discovery.DiscoveryService; import com.limelight.discovery.DiscoveryService;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.mdns.MdnsComputer; import com.limelight.nvstream.mdns.MdnsComputer;
import com.limelight.nvstream.mdns.MdnsDiscoveryListener; import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
import com.limelight.utils.CacheHelper;
import android.app.Service; import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
@@ -19,19 +26,21 @@ import android.content.ServiceConnection;
import android.os.Binder; import android.os.Binder;
import android.os.IBinder; import android.os.IBinder;
import org.xmlpull.v1.XmlPullParserException;
public class ComputerManagerService extends Service { public class ComputerManagerService extends Service {
private static final int POLLING_PERIOD_MS = 3000; private static final int POLLING_PERIOD_MS = 3000;
private static final int MDNS_QUERY_PERIOD_MS = 1000; private static final int MDNS_QUERY_PERIOD_MS = 1000;
private ComputerManagerBinder binder = new ComputerManagerBinder(); private final ComputerManagerBinder binder = new ComputerManagerBinder();
private ComputerDatabaseManager dbManager; private ComputerDatabaseManager dbManager;
private AtomicInteger dbRefCount = new AtomicInteger(0); private final AtomicInteger dbRefCount = new AtomicInteger(0);
private IdentityManager idManager; private IdentityManager idManager;
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>(); private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
private ComputerManagerListener listener = null; private ComputerManagerListener listener = null;
private AtomicInteger activePolls = new AtomicInteger(0); private final AtomicInteger activePolls = new AtomicInteger(0);
private boolean pollingActive = false; private boolean pollingActive = false;
private DiscoveryService.DiscoveryBinder discoveryBinder; private DiscoveryService.DiscoveryBinder discoveryBinder;
@@ -55,10 +64,8 @@ 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)
{ {
boolean newPc = details.name.isEmpty();
if (!getLocalDatabaseReference()) { if (!getLocalDatabaseReference()) {
return false; return false;
} }
@@ -103,7 +110,7 @@ public class ComputerManagerService extends Service {
public void run() { public void run() {
while (!isInterrupted() && pollingActive) { while (!isInterrupted() && pollingActive) {
// Check if this poll has modified the details // Check if this poll has modified the details
runPoll(details); runPoll(details, false);
// Wait until the next polling interval // Wait until the next polling interval
try { try {
@@ -167,10 +174,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);
} }
@@ -180,9 +183,25 @@ public class ComputerManagerService extends Service {
ComputerManagerService.this.onUnbind(null); ComputerManagerService.this.onUnbind(null);
} }
public ApplistPoller createAppListPoller(ComputerDetails computer) {
return new ApplistPoller(computer);
}
public String getUniqueId() { public String getUniqueId() {
return idManager.getUniqueId(); return idManager.getUniqueId();
} }
public ComputerDetails getComputer(UUID uuid) {
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
if (uuid.equals(tuple.computer.uuid)) {
return tuple.computer;
}
}
}
return null;
}
} }
@Override @Override
@@ -213,7 +232,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
@@ -229,27 +248,15 @@ 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 // Update details anyway in case this machine has been re-added by IP
((details.name.isEmpty() || tuple.computer.name.isEmpty()) && // after not being reachable by our existing information
tuple.computer.localIp.equals(details.localIp)) || tuple.computer.localIp = details.localIp;
// If there is a name on both computers, compare with name tuple.computer.remoteIp = details.remoteIp;
((!details.name.isEmpty() && !tuple.computer.name.isEmpty()) &&
tuple.computer.name.equals(details.name))) {
// Start a polling thread if polling is active // Start a polling thread if polling is active
if (pollingActive && tuple.thread == null) { if (pollingActive && tuple.thread == null) {
@@ -276,13 +283,14 @@ 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); runPoll(fakeDetails, true);
// 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;
@@ -356,6 +364,12 @@ public class ComputerManagerService extends Service {
private boolean pollComputer(ComputerDetails details, boolean localFirst) { private boolean pollComputer(ComputerDetails details, boolean localFirst) {
ComputerDetails polledDetails; ComputerDetails polledDetails;
// If the local address is routable across the Internet,
// always consider this PC remote to be conservative
if (details.localIp.equals(details.remoteIp)) {
localFirst = false;
}
if (localFirst) { if (localFirst) {
polledDetails = tryPollIp(details, details.localIp); polledDetails = tryPollIp(details, details.localIp);
} }
@@ -451,11 +465,112 @@ public class ComputerManagerService extends Service {
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
return binder; return binder;
} }
public class ApplistPoller {
private Thread thread;
private final ComputerDetails computer;
private final Object pollEvent = new Object();
public ApplistPoller(ComputerDetails computer) {
this.computer = computer;
}
public void pollNow() {
synchronized (pollEvent) {
pollEvent.notify();
}
}
private boolean waitPollingDelay() {
try {
synchronized (pollEvent) {
pollEvent.wait(POLLING_PERIOD_MS);
}
} catch (InterruptedException e) {
return false;
}
return thread != null && !thread.isInterrupted();
}
public void start() {
thread = new Thread() {
@Override
public void run() {
do {
InetAddress selectedAddr;
// Can't poll if it's not online
if (computer.state != ComputerDetails.State.ONLINE) {
if (listener != null) {
listener.notifyComputerUpdated(computer);
}
continue;
}
// Can't poll if there's no UUID yet
if (computer.uuid == null) {
continue;
}
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
selectedAddr = computer.localIp;
}
else {
selectedAddr = computer.remoteIp;
}
NvHTTP http = new NvHTTP(selectedAddr, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
try {
// Query the app list from the server
String appList = http.getAppListRaw();
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
// Open the cache file
FileOutputStream cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
CacheHelper.writeStringToOutputStream(cacheOut, appList);
cacheOut.close();
// Update the computer
computer.rawAppList = appList;
// Notify that the app list has been updated
// and ensure that the thread is still active
if (listener != null && thread != null) {
listener.notifyComputerUpdated(computer);
}
}
else {
LimeLog.warning("Empty app list received from "+computer.uuid);
}
} catch (IOException e) {
e.printStackTrace();
} catch (XmlPullParserException e) {
e.printStackTrace();
}
} while (waitPollingDelay());
}
};
thread.start();
}
public void stop() {
if (thread != null) {
thread.interrupt();
// Don't join here because we might be blocked on network I/O
thread = null;
}
}
}
} }
class PollingTuple { 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,16 +1,25 @@
package com.limelight.grid; package com.limelight.grid;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import com.koushikdutta.async.future.FutureCallback; import com.koushikdutta.async.future.FutureCallback;
import com.koushikdutta.ion.Ion; import com.koushikdutta.ion.Ion;
import com.limelight.AppView; import com.limelight.AppView;
import com.limelight.LimeLog;
import com.limelight.R; import com.limelight.R;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.LimelightCryptoProvider; import com.limelight.nvstream.http.LimelightCryptoProvider;
import com.limelight.utils.CacheHelper;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.Socket; import java.net.Socket;
import java.security.KeyManagementException; import java.security.KeyManagementException;
@@ -18,7 +27,10 @@ import java.security.NoSuchAlgorithmException;
import java.security.Principal; import java.security.Principal;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
@@ -32,16 +44,16 @@ import java.security.cert.X509Certificate;
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> { public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
private InetAddress address; private final ComputerDetails computer;
private String uniqueId; private final String uniqueId;
private LimelightCryptoProvider cryptoProvider; private final LimelightCryptoProvider cryptoProvider;
private SSLContext sslContext; private final SSLContext sslContext;
private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>(); private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>();
public AppGridAdapter(Context context, InetAddress address, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException { public AppGridAdapter(Context context, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException {
super(context, R.layout.app_grid_item, R.drawable.image_loading); super(context, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
this.address = address; this.computer = computer;
this.uniqueId = uniqueId; this.uniqueId = uniqueId;
cryptoProvider = PlatformBinding.getCryptoProvider(context); cryptoProvider = PlatformBinding.getCryptoProvider(context);
@@ -50,7 +62,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom()); sslContext.init(ourKeyman, trustAllCerts, new SecureRandom());
} }
TrustManager[] trustAllCerts = new TrustManager[] { private final TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() { new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() { public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0]; return new X509Certificate[0];
@@ -59,7 +71,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
public void checkServerTrusted(X509Certificate[] certs, String authType) {} public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}}; }};
KeyManager[] ourKeyman = new KeyManager[] { private final KeyManager[] ourKeyman = new KeyManager[] {
new X509KeyManager() { new X509KeyManager() {
public String chooseClientAlias(String[] keyTypes, public String chooseClientAlias(String[] keyTypes,
Principal[] issuers, Socket socket) { Principal[] issuers, Socket socket) {
@@ -94,8 +106,27 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
public boolean verify(String hostname, SSLSession session) { return true; } public boolean verify(String hostname, SSLSession session) { return true; }
}; };
private void sortList() {
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
@Override
public int compare(AppView.AppObject lhs, AppView.AppObject rhs) {
return lhs.app.getAppName().compareTo(rhs.app.getAppName());
}
});
}
private InetAddress getCurrentAddress() {
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
return computer.localIp;
}
else {
return computer.remoteIp;
}
}
public void addApp(AppView.AppObject app) { public void addApp(AppView.AppObject app) {
itemList.add(app); itemList.add(app);
sortList();
} }
public void abortPendingRequests() { public void abortPendingRequests() {
@@ -107,8 +138,10 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
} }
for (Future f : tempMap.values()) { for (Future f : tempMap.values()) {
if (!f.isCancelled() && !f.isDone()) {
f.cancel(true); f.cancel(true);
} }
}
synchronized (pendingRequests) { synchronized (pendingRequests) {
// Remove cancelled requests // Remove cancelled requests
@@ -118,30 +151,25 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
} }
} }
@Override // TODO: Handle pruning of bitmap cache
public boolean populateImageView(final ImageView imgView, AppView.AppObject obj) { 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();
}
}
// Set SSL contexts correctly to allow us to authenticate
Ion.getDefault(imgView.getContext()).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts);
Ion.getDefault(imgView.getContext()).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext);
// Set off the deferred image load
synchronized (pendingRequests) {
Future f = Ion.with(imgView)
.placeholder(defaultImageRes)
.error(defaultImageRes)
.load("https://" + address.getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
obj.app.getAppId() + "&AssetType=2&AssetIdx=0")
.setCallback(new FutureCallback<ImageView>() {
@Override @Override
public void onCompleted(Exception e, ImageView result) { public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) {
synchronized (pendingRequests) { // Clear existing contents of the image view
pendingRequests.remove(imgView); imgView.setAlpha(0.0f);
}
} // Check the on-disk cache
}); new ImageCacheRequest(imgView, obj.app.getAppId()).execute();
pendingRequests.put(imgView, f);
}
return true; return true;
} }
@@ -166,4 +194,96 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
// No overlay // No overlay
return false; return false;
} }
private class ImageCacheRequest extends AsyncTask<Void, Void, Bitmap> {
private final ImageView view;
private final int appId;
public ImageCacheRequest(ImageView view, int appId) {
this.view = view;
this.appId = appId;
}
@Override
protected Bitmap doInBackground(Void... v) {
InputStream in = null;
try {
in = CacheHelper.openCacheFileForInput(context.getCacheDir(), "boxart", computer.uuid.toString(), appId + ".png");
return BitmapFactory.decodeStream(in);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ignored) {}
}
}
return null;
}
private void fadeInImage(ImageView view) {
view.animate().alpha(1.0f).setDuration(250).start();
}
@Override
protected void onPostExecute(Bitmap result) {
if (result != null) {
// Disk cache was read successfully
LimeLog.info("Image disk cache hit for (" + computer.uuid + ", " + appId + ")");
view.setImageBitmap(result);
fadeInImage(view);
}
else {
LimeLog.info("Image disk cache miss for ("+computer.uuid+", "+appId+")");
LimeLog.info("Requesting: "+"https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
appId + "&AssetType=2&AssetIdx=0");
// Load the placeholder image
view.setImageResource(defaultImageRes);
fadeInImage(view);
// Set SSL contexts correctly to allow us to authenticate
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts);
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext);
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setHostnameVerifier(hv);
// Kick off the deferred image load
synchronized (pendingRequests) {
Future<Bitmap> f = Ion.with(context)
.load("https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
appId + "&AssetType=2&AssetIdx=0")
.asBitmap()
.setCallback(new FutureCallback<Bitmap>() {
@Override
public void onCompleted(Exception e, final Bitmap result) {
synchronized (pendingRequests) {
pendingRequests.remove(view);
}
if (result != null) {
// Make the view visible now
view.setImageBitmap(result);
fadeInImage(view);
// Populate the disk cache if we got an image back.
// We do it in a new thread because it can be very expensive, especially
// when we do the initial load where lots of disk I/O is happening at once.
new Thread() {
@Override
public void run() {
populateBitmapCache(computer.uuid, appId, result);
}
}.start();
}
else {
// Leave the loading icon as is (probably should change this eventually...)
}
}
});
pendingRequests.put(view, f);
}
}
}
}
} }
@@ -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;
@@ -60,18 +60,22 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay); ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay);
TextView txtView = (TextView) convertView.findViewById(R.id.grid_text); TextView txtView = (TextView) convertView.findViewById(R.id.grid_text);
if (imgView != null) {
if (!populateImageView(imgView, itemList.get(i))) { if (!populateImageView(imgView, itemList.get(i))) {
imgView.setImageResource(defaultImageRes); imgView.setImageResource(defaultImageRes);
} }
}
if (!populateTextView(txtView, itemList.get(i))) { if (!populateTextView(txtView, itemList.get(i))) {
txtView.setText(itemList.get(i).toString()); txtView.setText(itemList.get(i).toString());
} }
if (overlayView != null) {
if (!populateOverlayView(overlayView, itemList.get(i))) { if (!populateOverlayView(overlayView, itemList.get(i))) {
overlayView.setVisibility(View.INVISIBLE); overlayView.setVisibility(View.INVISIBLE);
} }
else { else {
overlayView.setVisibility(View.VISIBLE); overlayView.setVisibility(View.VISIBLE);
} }
}
return convertView; return convertView;
} }
@@ -8,14 +8,27 @@ import com.limelight.PcView;
import com.limelight.R; import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
import java.util.Collections;
import java.util.Comparator;
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> { public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
public PcGridAdapter(Context context) { public PcGridAdapter(Context context, boolean listMode, boolean small) {
super(context, R.layout.pc_grid_item, R.drawable.computer); super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item), R.drawable.computer);
} }
public void addComputer(PcView.ComputerObject computer) { public void addComputer(PcView.ComputerObject computer) {
itemList.add(computer); itemList.add(computer);
sortList();
}
private void sortList() {
Collections.sort(itemList, new Comparator<PcView.ComputerObject>() {
@Override
public int compare(PcView.ComputerObject lhs, PcView.ComputerObject rhs) {
return lhs.details.name.compareTo(rhs.details.name);
}
});
} }
public boolean removeComputer(PcView.ComputerObject computer) { public boolean removeComputer(PcView.ComputerObject computer) {
@@ -2,6 +2,7 @@ package com.limelight.preferences;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.Locale;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import com.limelight.computers.ComputerManagerService; import com.limelight.computers.ComputerManagerService;
@@ -13,21 +14,24 @@ import com.limelight.utils.UiHelper;
import android.app.Activity; import android.app.Activity;
import android.app.Service; import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.view.KeyEvent; import android.view.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();
@@ -132,6 +136,13 @@ public class AddComputerManually extends Activity {
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
setContentView(R.layout.activity_add_computer_manually); setContentView(R.layout.activity_add_computer_manually);
UiHelper.notifyNewRootView(this); UiHelper.notifyNewRootView(this);
@@ -150,7 +161,13 @@ public class AddComputerManually extends Activity {
return true; return true;
} }
computersToAdd.add(hostText.getText().toString()); 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 {
@@ -13,11 +14,15 @@ public class PreferenceConfiguration {
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings"; private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio"; private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio";
private static final String DEADZONE_PREF_STRING = "seekbar_deadzone"; private static final String DEADZONE_PREF_STRING = "seekbar_deadzone";
private static final String LANGUAGE_PREF_STRING = "list_languages";
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
private static final int BITRATE_DEFAULT_720_30 = 5; private static final int BITRATE_DEFAULT_720_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10; private static final int BITRATE_DEFAULT_720_60 = 10;
private static final int BITRATE_DEFAULT_1080_30 = 10; private static final int BITRATE_DEFAULT_1080_30 = 10;
private static final int BITRATE_DEFAULT_1080_60 = 30; private static final int BITRATE_DEFAULT_1080_60 = 20;
private static final String DEFAULT_RES_FPS = "720p60"; private static final String DEFAULT_RES_FPS = "720p60";
private static final String DEFAULT_DECODER = "auto"; private static final String DEFAULT_DECODER = "auto";
@@ -27,6 +32,9 @@ public class PreferenceConfiguration {
private static final boolean DEFAULT_DISABLE_TOASTS = false; private static final boolean DEFAULT_DISABLE_TOASTS = false;
private static final boolean DEFAULT_HOST_AUDIO = false; private static final boolean DEFAULT_HOST_AUDIO = false;
private static final int DEFAULT_DEADZONE = 15; private static final int DEFAULT_DEADZONE = 15;
public static final String DEFAULT_LANGUAGE = "default";
private static final boolean DEFAULT_LIST_MODE = false;
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
public static final int FORCE_HARDWARE_DECODER = -1; public static final int FORCE_HARDWARE_DECODER = -1;
public static final int AUTOSELECT_DECODER = 0; public static final int AUTOSELECT_DECODER = 0;
@@ -37,6 +45,8 @@ public class PreferenceConfiguration {
public int decoder; public int decoder;
public int deadzonePercentage; public int deadzonePercentage;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings; public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language;
public boolean listMode, smallIconMode, multiController;
public static int getDefaultBitrate(String resFpsString) { public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("720p30")) { if (resFpsString.equals("720p30")) {
@@ -57,6 +67,17 @@ public class PreferenceConfiguration {
} }
} }
public static boolean getDefaultSmallMode(Context context) {
PackageManager manager = context.getPackageManager();
if (manager != null && manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
// TVs shouldn't use small mode by default
return false;
}
// Use small mode on anything smaller than a 7" tablet
return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
}
public static int getDefaultBitrate(Context context) { public static int getDefaultBitrate(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
@@ -135,11 +156,16 @@ public class PreferenceConfiguration {
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE); config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
config.language = prefs.getString(LANGUAGE_PREF_STRING, DEFAULT_LANGUAGE);
// Checkbox preferences // Checkbox preferences
config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS); config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS);
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS); config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH); config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO); config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
return config; return config;
} }
@@ -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) {
@@ -0,0 +1,21 @@
package com.limelight.preferences;
import android.content.Context;
import android.content.res.TypedArray;
import android.preference.CheckBoxPreference;
import android.util.AttributeSet;
public class SmallIconCheckboxPreference extends CheckBoxPreference {
public SmallIconCheckboxPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SmallIconCheckboxPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
return PreferenceConfiguration.getDefaultSmallMode(getContext());
}
}
@@ -1,20 +1,32 @@
package com.limelight.preferences; package com.limelight.preferences;
import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.app.Activity; import android.app.Activity;
import android.preference.Preference; import android.preference.Preference;
import android.preference.PreferenceFragment; import android.preference.PreferenceFragment;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import com.limelight.PcView;
import com.limelight.R; import com.limelight.R;
import com.limelight.utils.UiHelper; import com.limelight.utils.UiHelper;
import java.util.Locale;
public class StreamSettings extends Activity { public class StreamSettings extends Activity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
setContentView(R.layout.activity_stream_settings); setContentView(R.layout.activity_stream_settings);
getFragmentManager().beginTransaction().replace( getFragmentManager().beginTransaction().replace(
R.id.stream_settings, new SettingsFragment() R.id.stream_settings, new SettingsFragment()
@@ -23,6 +35,16 @@ public class StreamSettings extends Activity {
UiHelper.notifyNewRootView(this); UiHelper.notifyNewRootView(this);
} }
@Override
public void onBackPressed() {
finish();
// Restart the PC view to apply UI changes
Intent intent = new Intent(this, PcView.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent, null);
}
public static class SettingsFragment extends PreferenceFragment { public static class SettingsFragment extends PreferenceFragment {
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@@ -0,0 +1,35 @@
package com.limelight.ui;
import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import com.limelight.R;
public class AdapterFragment extends Fragment {
private AdapterFragmentCallbacks callbacks;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
callbacks = (AdapterFragmentCallbacks) activity;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(callbacks.getAdapterFragmentLayoutId(), container, false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
callbacks.receiveAbsListView((AbsListView) getView().findViewById(R.id.fragmentView));
}
}
@@ -0,0 +1,8 @@
package com.limelight.ui;
import android.widget.AbsListView;
public interface AdapterFragmentCallbacks {
public int getAdapterFragmentLayoutId();
public void receiveAbsListView(AbsListView gridView);
}
@@ -0,0 +1,5 @@
package com.limelight.ui;
public interface GameGestures {
public void showKeyboard();
}
@@ -0,0 +1,55 @@
package com.limelight.utils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
public class CacheHelper {
private static File openPath(boolean createPath, File root, String... path) {
File f = root;
for (int i = 0; i < path.length; i++) {
String component = path[i];
if (i == path.length - 1) {
// This is the file component so now we create parent directories
if (createPath) {
f.mkdirs();
}
}
f = new File(f, component);
}
return f;
}
public static FileInputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
return new FileInputStream(openPath(false, root, path));
}
public static FileOutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
return new FileOutputStream(openPath(true, root, path));
}
public static String readInputStreamToString(InputStream in) throws IOException {
Reader r = new InputStreamReader(in);
StringBuilder sb = new StringBuilder();
char[] buf = new char[256];
int bytesRead;
while ((bytesRead = r.read(buf)) != -1) {
sb.append(buf, 0, bytesRead);
}
return sb.toString();
}
public static void writeStringToOutputStream(OutputStream out, String str) throws IOException {
out.write(str.getBytes("UTF-8"));
}
}
@@ -7,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;
@@ -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;
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" >
<stroke android:width="1dip" android:color="#ffffff"/>
</shape>
@@ -33,12 +33,10 @@
android:text="@string/searching_pc"/> android:text="@string/searching_pc"/>
</RelativeLayout> </RelativeLayout>
<GridView <FrameLayout
android:id="@+id/pcGridView" android:id="@+id/pcFragmentContainer"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:gravity="center" android:gravity="center"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
@@ -33,12 +33,10 @@
android:text="@string/searching_pc"/> android:text="@string/searching_pc"/>
</RelativeLayout> </RelativeLayout>
<GridView <FrameLayout
android:id="@+id/pcGridView" android:id="@+id/pcFragmentContainer"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:gravity="center" android:gravity="center"
android:layout_toLeftOf="@+id/manuallyAddPc" android:layout_toLeftOf="@+id/manuallyAddPc"
android:layout_toStartOf="@+id/manuallyAddPc" android:layout_toStartOf="@+id/manuallyAddPc"
@@ -8,21 +8,17 @@
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".AppView" > tools:context=".AppView" >
<GridView <FrameLayout
android:id="@+id/appGridView" android:id="@+id/appFragmentContainer"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:stretchMode="spacingWidth"
android:gravity="center" android:gravity="center"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:layout_below="@+id/appListText"> android:layout_below="@+id/appListText"/>
</GridView>
<TextView <TextView
android:id="@+id/appListText" android:id="@+id/appListText"
@@ -30,6 +26,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerHorizontal="true" android:layout_centerHorizontal="true"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:gravity="center"
android:paddingTop="0dp" android:paddingTop="0dp"
android:paddingBottom="10dp" android:paddingBottom="10dp"
android:textSize="28sp"/> android:textSize="28sp"/>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp">
<RelativeLayout
android:id="@+id/grid_image_layout"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/grid_image"
android:cropToPadding="false"
android:scaleType="fitXY"
android:layout_centerHorizontal="true"
android:layout_width="100dp"
android:layout_height="117dp">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="33dp"
android:layout_height="33dp">
</ImageView>
</RelativeLayout>
<TextView
android:id="@+id/grid_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
android:gravity="center"
android:singleLine="true"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:textSize="14sp" >
</TextView>
</RelativeLayout>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/fragmentView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/list_view_unselected"
android:fastScrollEnabled="true"
android:longClickable="false"
android:focusable="true"
android:focusableInTouchMode="true"
android:stackFromBottom="false" >
</ListView>
</LinearLayout>
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="15dp">
<RelativeLayout
android:id="@+id/grid_image_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/grid_image"
android:layout_centerHorizontal="true"
android:layout_width="100dp"
android:layout_height="67dp">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_marginTop="10dp"
android:layout_marginLeft="42dp"
android:layout_marginStart="42dp"
android:layout_marginRight="13dp"
android:layout_marginEnd="13dp"
android:layout_width="33dp"
android:layout_height="33dp">
</ImageView>
</RelativeLayout>
<TextView
android:id="@+id/grid_text"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
android:gravity="center"
android:textSize="14sp" >
</TextView>
</RelativeLayout>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:padding="10dp"
android:layout_height="wrap_content">
<TextView
android:id="@+id/grid_text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textIsSelectable="false"
android:textSize="16sp" >
</TextView>
</LinearLayout>
+17 -3
View File
@@ -56,6 +56,9 @@
<!-- General strings --> <!-- General strings -->
<string name="ip_hint">Indirizzo IP del PC</string> <string name="ip_hint">Indirizzo IP del PC</string>
<string name="searching_pc">Ricerca PC in corso…</string> <string name="searching_pc">Ricerca PC in corso…</string>
<string name="yes">Sì</string>
<string name="no">No</string>
<string name="lost_connection">Connessione con il PC persa</string>
<!-- AppList activity --> <!-- AppList activity -->
<string name="title_applist">Applicazioni su</string> <string name="title_applist">Applicazioni su</string>
@@ -70,6 +73,7 @@
<string name="applist_quit_app">Chiusura in corso…</string> <string name="applist_quit_app">Chiusura in corso…</string>
<string name="applist_quit_success">Sessione chiusa con successo</string> <string name="applist_quit_success">Sessione chiusa con successo</string>
<string name="applist_quit_fail">Chiusura sessione fallita</string> <string name="applist_quit_fail">Chiusura sessione fallita</string>
<string name="applist_quit_confirmation">Sei sicuro di voler chiudere l\'applicazione avviata? Tutti i dati non salvati saranno persi.</string>
<!-- Add computer manually activity --> <!-- Add computer manually activity -->
<string name="title_add_pc">Aggiungi PC Manualmente</string> <string name="title_add_pc">Aggiungi PC Manualmente</string>
@@ -87,20 +91,30 @@
<string name="summary_seekbar_bitrate">Abbassa il bitrate per ridurre lo stuttering; alza il bitrate per aumenteare la qualità dell\'immagine</string> <string name="summary_seekbar_bitrate">Abbassa il bitrate per ridurre lo stuttering; alza il bitrate per aumenteare la qualità dell\'immagine</string>
<string name="suffix_seekbar_bitrate">Mbps</string> <string name="suffix_seekbar_bitrate">Mbps</string>
<string name="title_checkbox_stretch_video">Forza video in full-screen</string> <string name="title_checkbox_stretch_video">Forza video in full-screen</string>
<string name="title_checkbox_disable_warnings">Disabilita i messaggi di warning</string> <string name="title_checkbox_disable_warnings">Disabilita messaggi di warning</string>
<string name="summary_checkbox_disable_warnings">Disabilita i messaggi di warning sullo schermo durante lo streaming</string> <string name="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>
<string name="category_ui_settings">Impostazioni Interfaccia</string>
<string name="title_language_list">Lingua</string>
<string name="summary_language_list">Lingua da usare in Limelight</string>
<string name="title_checkbox_list_mode">Usa lista invece della griglia</string>
<string name="summary_checkbox_list_mode">Visualizza applicazioni e computers in una lista invece di una griglia</string>
<string name="title_checkbox_small_icon_mode">Usa icone piccole</string>
<string name="summary_checkbox_small_icon_mode">Usa icone piccole nella vista a griglia per avere più oggetti sullo schermo</string>
<string name="category_host_settings">Impostazioni Host</string> <string name="category_host_settings">Impostazioni Host</string>
<string name="title_checkbox_enable_sops">Ottimizza le impostazioni dei giochi</string> <string name="title_checkbox_enable_sops">Ottimizza le impostazioni dei giochi</string>
<string name="summary_checkbox_enable_sops">Permetti a GFE di modificare le impostazioni dei giochi per uno streaming ottimale</string> <string name="summary_checkbox_enable_sops">Permetti a GFE di modificare le impostazioni dei giochi per uno streaming ottimale</string>
<string name="title_checkbox_host_audio">Riproduci audio sul PC</string> <string name="title_checkbox_host_audio">Riproduci audio sul PC</string>
<string name="summary_checkbox_host_audio">Riproduci l\'audio sul computer e su questo dispositivo. Richiede GFE 2.1.2+</string> <string name="summary_checkbox_host_audio">Riproduci l\'audio sul computer e su questo dispositivo</string>
<string name="category_advanced_settings">Impostazioni Avanzate</string> <string name="category_advanced_settings">Impostazioni Avanzate</string>
<string name="title_decoder_list">Cambia decoder</string> <string name="title_decoder_list">Cambia decoder</string>
<string name="summary_decoder_list">Il decoder software può aumentare la latenza video quando si usano impostazioni streaming basse</string> <string name="summary_decoder_list">Il decoder software può ridurre la latenza video quando si usano impostazioni streaming basse</string>
</resources> </resources>
+11
View File
@@ -13,6 +13,17 @@
<item>1080p60</item> <item>1080p60</item>
</string-array> </string-array>
<string-array name="language_names" translatable="false">
<item>Default</item>
<item>English</item>
<item>Italiano</item>
</string-array>
<string-array name="language_values" translatable="false">
<item>default</item>
<item>en</item>
<item>it</item>
</string-array>
<string-array name="decoder_names"> <string-array name="decoder_names">
<item>Auto-select Decoder</item> <item>Auto-select Decoder</item>
<item>Force Software Decoding</item> <item>Force Software Decoding</item>
+15 -1
View File
@@ -56,6 +56,9 @@
<!-- General strings --> <!-- General strings -->
<string name="ip_hint">IP address of GeForce PC</string> <string name="ip_hint">IP address of GeForce PC</string>
<string name="searching_pc">Searching for PCs…</string> <string name="searching_pc">Searching for PCs…</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="lost_connection">Lost connection to PC</string>
<!-- AppList activity --> <!-- AppList activity -->
<string name="title_applist">Apps on</string> <string name="title_applist">Apps on</string>
@@ -70,6 +73,7 @@
<string name="applist_quit_app">Quitting</string> <string name="applist_quit_app">Quitting</string>
<string name="applist_quit_success">Successfully quit</string> <string name="applist_quit_success">Successfully quit</string>
<string name="applist_quit_fail">Failed to quit</string> <string name="applist_quit_fail">Failed to quit</string>
<string name="applist_quit_confirmation">Are you sure you want to quit the running app? All unsaved data will be lost.</string>
<!-- Add computer manually activity --> <!-- Add computer manually activity -->
<string name="title_add_pc">Add PC Manually</string> <string name="title_add_pc">Add PC Manually</string>
@@ -91,14 +95,24 @@
<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>
<string name="category_ui_settings">UI Settings</string>
<string name="title_language_list">Language</string>
<string name="summary_language_list">Language to use for Limelight</string>
<string name="title_checkbox_list_mode">Use lists instead of grids</string>
<string name="summary_checkbox_list_mode">Display apps and PCs in lists instead of grids</string>
<string name="title_checkbox_small_icon_mode">Use small icons</string>
<string name="summary_checkbox_small_icon_mode">Use small icons in grid items to allow more items on screen</string>
<string name="category_host_settings">Host Settings</string> <string name="category_host_settings">Host Settings</string>
<string name="title_checkbox_enable_sops">Optimize game settings</string> <string name="title_checkbox_enable_sops">Optimize game settings</string>
<string name="summary_checkbox_enable_sops">Allow GFE to modify game settings for optimal streaming</string> <string name="summary_checkbox_enable_sops">Allow GFE to modify game settings for optimal streaming</string>
<string name="title_checkbox_host_audio">Play audio on PC</string> <string name="title_checkbox_host_audio">Play audio on PC</string>
<string name="summary_checkbox_host_audio">Play audio from the computer and this device. Requires GFE 2.1.2+</string> <string name="summary_checkbox_host_audio">Play audio from the computer and this device</string>
<string name="category_advanced_settings">Advanced Settings</string> <string name="category_advanced_settings">Advanced Settings</string>
<string name="title_decoder_list">Change decoder</string> <string name="title_decoder_list">Change decoder</string>
+27 -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"
@@ -46,6 +51,24 @@
android:summary="@string/summary_checkbox_host_audio" android:summary="@string/summary_checkbox_host_audio"
android:defaultValue="false" /> android:defaultValue="false" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/category_ui_settings">
<ListPreference
android:key="list_languages"
android:title="@string/title_language_list"
android:entries="@array/language_names"
android:entryValues="@array/language_values"
android:summary="@string/summary_language_list"
android:defaultValue="default" />
<com.limelight.preferences.SmallIconCheckboxPreference
android:key="checkbox_small_icon_mode"
android:title="@string/title_checkbox_small_icon_mode"
android:summary="@string/summary_checkbox_small_icon_mode" />
<CheckBoxPreference
android:key="checkbox_list_mode"
android:title="@string/title_checkbox_list_mode"
android:summary="@string/summary_checkbox_list_mode"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_advanced_settings"> <PreferenceCategory android:title="@string/category_advanced_settings">
<ListPreference <ListPreference
android:key="list_decoders" android:key="list_decoders"
+1 -2
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="com.limelight" >
<!-- Non-root application name --> <!-- Non-root application name -->
<application android:label="Limelight" /> <application android:label="Limelight" />
+1 -2
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="com.limelight.root" >
<!-- Root permissions --> <!-- Root permissions -->
<uses-permission android:name="android.permission.ACCESS_SUPERUSER" /> <uses-permission android:name="android.permission.ACCESS_SUPERUSER" />
+1 -1
View File
@@ -4,7 +4,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:0.13.2' classpath 'com.android.tools.build:gradle:1.0.1'
} }
} }
+2 -2
View File
@@ -1,6 +1,6 @@
#Wed Apr 10 15:27:10 PDT 2013 #Sun Dec 07 22:52:07 PST 2014
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+4 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4"> <module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" type="JAVA_MODULE" version="4">
<component name="FacetManager"> <component name="FacetManager">
<facet type="java-gradle" name="Java-Gradle"> <facet type="java-gradle" name="Java-Gradle">
<configuration> <configuration>
@@ -7,7 +7,9 @@
</configuration> </configuration>
</facet> </facet>
</component> </component>
<component name="NewModuleRootManager" inherit-compiler-output="true"> <component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/classes/main" />
<output-test url="file://$MODULE_DIR$/build/classes/test" />
<exclude-output /> <exclude-output />
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" /> <excludeFolder url="file://$MODULE_DIR$/.gradle" />