mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-10-22 16:17:27 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdbecfa6dd | ||
|
|
5e45aeb70b |
No files matched your search
+13
-11
@@ -9,12 +9,11 @@
|
|||||||
<facet type="android" name="Android">
|
<facet type="android" name="Android">
|
||||||
<configuration>
|
<configuration>
|
||||||
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
|
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
|
||||||
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
|
|
||||||
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
|
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
|
||||||
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
|
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
|
||||||
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugAndroidTest" />
|
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" />
|
||||||
<option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" />
|
<option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" />
|
||||||
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugAndroidTestSources" />
|
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugTestSources" />
|
||||||
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
||||||
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
||||||
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
|
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
|
||||||
@@ -25,7 +24,6 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="NewModuleRootManager" inherit-compiler-output="false">
|
<component name="NewModuleRootManager" inherit-compiler-output="false">
|
||||||
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
|
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
|
||||||
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/nonRoot/debug" />
|
|
||||||
<exclude-output />
|
<exclude-output />
|
||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
|
||||||
@@ -41,12 +39,12 @@
|
|||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/java" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/java" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/jni" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/jni" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/test/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/test/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/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/androidTest/nonRoot/debug" type="java-test-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/test/nonRoot/debug" type="java-test-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/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" />
|
||||||
@@ -103,13 +101,17 @@
|
|||||||
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
|
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
|
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="jdk" jdkName="Android API 22 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="support-v4-r7" level="project" />
|
||||||
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
|
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
|
||||||
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" />
|
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" />
|
||||||
<orderEntry type="library" exported="" name="tinyrtsp" level="project" />
|
<orderEntry type="library" exported="" name="tinyrtsp" level="project" />
|
||||||
<orderEntry type="library" exported="" name="limelight-common" level="project" />
|
<orderEntry type="library" exported="" name="limelight-common" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="ion-2.0.5" level="project" />
|
||||||
<orderEntry type="library" exported="" name="okhttp-2.2.0" level="project" />
|
<orderEntry type="library" exported="" name="okhttp-2.2.0" level="project" />
|
||||||
<orderEntry type="library" exported="" name="jcodec-0.1.9" level="project" />
|
<orderEntry type="library" exported="" name="jcodec-0.1.9" level="project" />
|
||||||
<orderEntry type="library" exported="" name="okio-1.2.0" level="project" />
|
<orderEntry type="library" exported="" name="okio-1.2.0" level="project" />
|
||||||
|
|||||||
+13
-9
@@ -4,15 +4,15 @@ import org.apache.tools.ant.taskdefs.condition.Os
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 22
|
compileSdkVersion 21
|
||||||
buildToolsVersion "21.1.2"
|
buildToolsVersion "21.1.2"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 22
|
targetSdkVersion 21
|
||||||
|
|
||||||
versionName "3.1.3"
|
versionName "3.1-beta1"
|
||||||
versionCode = 58
|
versionCode = 50
|
||||||
}
|
}
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
@@ -62,13 +62,17 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile group: 'org.jcodec', name: 'jcodec', version: '0.1.9'
|
compile group: 'org.jcodec', name: 'jcodec', version: '+'
|
||||||
|
|
||||||
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
|
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '+'
|
||||||
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
|
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '+'
|
||||||
|
|
||||||
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.2.0'
|
compile group: 'com.google.android', name: 'support-v4', version:'+'
|
||||||
compile group: 'com.squareup.okio', name:'okio', version:'1.2.0'
|
compile group: 'com.koushikdutta.ion', name: 'ion', version:'+'
|
||||||
|
compile group: 'com.google.code.gson', name: 'gson', version:'+'
|
||||||
|
|
||||||
|
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'+'
|
||||||
|
compile group: 'com.squareup.okio', name:'okio', version:'+'
|
||||||
|
|
||||||
compile files('libs/jmdns-fixed.jar')
|
compile files('libs/jmdns-fixed.jar')
|
||||||
compile files('libs/limelight-common.jar')
|
compile files('libs/limelight-common.jar')
|
||||||
|
|||||||
Binary file not shown.
@@ -1,6 +1,9 @@
|
|||||||
package com.limelight;
|
package com.limelight;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
@@ -8,7 +11,10 @@ import java.util.List;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
||||||
import com.limelight.computers.ComputerManagerListener;
|
import com.limelight.computers.ComputerManagerListener;
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
import com.limelight.grid.AppGridAdapter;
|
import com.limelight.grid.AppGridAdapter;
|
||||||
@@ -21,7 +27,6 @@ import com.limelight.ui.AdapterFragment;
|
|||||||
import com.limelight.ui.AdapterFragmentCallbacks;
|
import com.limelight.ui.AdapterFragmentCallbacks;
|
||||||
import com.limelight.utils.CacheHelper;
|
import com.limelight.utils.CacheHelper;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
import com.limelight.utils.ServerHelper;
|
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
@@ -43,6 +48,7 @@ import android.view.ContextMenu.ContextMenuInfo;
|
|||||||
import android.widget.AbsListView;
|
import android.widget.AbsListView;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.AdapterView.OnItemClickListener;
|
import android.widget.AdapterView.OnItemClickListener;
|
||||||
|
import android.widget.Spinner;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||||
@@ -55,8 +61,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
private ComputerManagerService.ApplistPoller poller;
|
private ComputerManagerService.ApplistPoller poller;
|
||||||
private SpinnerDialog blockingLoadSpinner;
|
private SpinnerDialog blockingLoadSpinner;
|
||||||
private String lastRawApplist;
|
private String lastRawApplist;
|
||||||
private int lastRunningAppId;
|
|
||||||
private boolean suspendGridUpdates;
|
private int consecutiveAppListFailures = 0;
|
||||||
|
private final static int CONSECUTIVE_FAILURE_LIMIT = 3;
|
||||||
|
|
||||||
private final static int START_OR_RESUME_ID = 1;
|
private final static int START_OR_RESUME_ID = 1;
|
||||||
private final static int QUIT_ID = 2;
|
private final static int QUIT_ID = 2;
|
||||||
@@ -67,7 +74,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
public final static String UUID_EXTRA = "UUID";
|
public final static String UUID_EXTRA = "UUID";
|
||||||
|
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
private 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);
|
||||||
@@ -114,6 +121,11 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private InetAddress getAddress() {
|
||||||
|
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||||
|
computer.localIp : computer.remoteIp;
|
||||||
|
}
|
||||||
|
|
||||||
private void startComputerUpdates() {
|
private void startComputerUpdates() {
|
||||||
if (managerBinder == null) {
|
if (managerBinder == null) {
|
||||||
return;
|
return;
|
||||||
@@ -122,17 +134,15 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
managerBinder.startPolling(new ComputerManagerListener() {
|
managerBinder.startPolling(new ComputerManagerListener() {
|
||||||
@Override
|
@Override
|
||||||
public void notifyComputerUpdated(ComputerDetails details) {
|
public void notifyComputerUpdated(ComputerDetails details) {
|
||||||
// Do nothing if updates are suspended
|
|
||||||
if (suspendGridUpdates) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't care about other computers
|
// Don't care about other computers
|
||||||
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
|
if (details != computer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (details.state == ComputerDetails.State.OFFLINE) {
|
if (details.state != ComputerDetails.State.ONLINE) {
|
||||||
|
consecutiveAppListFailures++;
|
||||||
|
|
||||||
|
if (consecutiveAppListFailures >= CONSECUTIVE_FAILURE_LIMIT) {
|
||||||
// The PC is unreachable now
|
// The PC is unreachable now
|
||||||
AppView.this.runOnUiThread(new Runnable() {
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
@@ -142,34 +152,27 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// App list is the same or empty
|
consecutiveAppListFailures = 0;
|
||||||
|
|
||||||
|
// App list is the same or empty; nothing to do
|
||||||
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
|
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
|
||||||
|
|
||||||
// Let's check if the running app ID changed
|
|
||||||
if (details.runningGameId != lastRunningAppId) {
|
|
||||||
// Update the currently running game using the app ID
|
|
||||||
lastRunningAppId = details.runningGameId;
|
|
||||||
updateUiWithServerinfo(details);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastRunningAppId = details.runningGameId;
|
|
||||||
lastRawApplist = details.rawAppList;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
lastRawApplist = details.rawAppList;
|
||||||
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
|
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
|
||||||
|
|
||||||
if (blockingLoadSpinner != null) {
|
if (blockingLoadSpinner != null) {
|
||||||
blockingLoadSpinner.dismiss();
|
blockingLoadSpinner.dismiss();
|
||||||
blockingLoadSpinner = null;
|
blockingLoadSpinner = null;
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {}
|
} catch (Exception e) {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -187,10 +190,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
if (managerBinder != null) {
|
if (managerBinder != null) {
|
||||||
managerBinder.stopPolling();
|
managerBinder.stopPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appGridAdapter != null) {
|
|
||||||
appGridAdapter.cancelQueuedOperations();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -273,6 +272,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
int runningAppId = -1;
|
int runningAppId = -1;
|
||||||
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||||
AppObject app = (AppObject) appGridAdapter.getItem(i);
|
AppObject app = (AppObject) appGridAdapter.getItem(i);
|
||||||
|
if (app.app == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (app.app.getIsRunning()) {
|
if (app.app.getIsRunning()) {
|
||||||
runningAppId = app.app.getAppId();
|
runningAppId = app.app.getAppId();
|
||||||
break;
|
break;
|
||||||
@@ -287,6 +290,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
|
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
|
||||||
|
if (selectedApp == null || selectedApp.app == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
int runningAppId = getRunningAppId();
|
int runningAppId = getRunningAppId();
|
||||||
if (runningAppId != -1) {
|
if (runningAppId != -1) {
|
||||||
if (runningAppId == selectedApp.app.getAppId()) {
|
if (runningAppId == selectedApp.app.getAppId()) {
|
||||||
@@ -304,6 +311,33 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
public void onContextMenuClosed(Menu menu) {
|
public void onContextMenuClosed(Menu menu) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void displayQuitConfirmationDialog(final Runnable onYes, final Runnable onNo) {
|
||||||
|
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
switch (which){
|
||||||
|
case DialogInterface.BUTTON_POSITIVE:
|
||||||
|
if (onYes != null) {
|
||||||
|
onYes.run();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DialogInterface.BUTTON_NEGATIVE:
|
||||||
|
if (onNo != null) {
|
||||||
|
onNo.run();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
|
builder.setMessage(getResources().getString(R.string.applist_quit_confirmation))
|
||||||
|
.setPositiveButton(getResources().getString(R.string.yes), dialogClickListener)
|
||||||
|
.setNegativeButton(getResources().getString(R.string.no), dialogClickListener)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onContextItemSelected(MenuItem item) {
|
public boolean onContextItemSelected(MenuItem item) {
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||||
@@ -311,37 +345,25 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case START_WTIH_QUIT:
|
case START_WTIH_QUIT:
|
||||||
// Display a confirmation dialog first
|
// Display a confirmation dialog first
|
||||||
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
displayQuitConfirmationDialog(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
doStart(app.app);
|
||||||
}
|
}
|
||||||
}, null);
|
}, null);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case START_OR_RESUME_ID:
|
case START_OR_RESUME_ID:
|
||||||
// Resume is the same as start for us
|
// Resume is the same as start for us
|
||||||
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
doStart(app.app);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case QUIT_ID:
|
case QUIT_ID:
|
||||||
// Display a confirmation dialog first
|
// Display a confirmation dialog first
|
||||||
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
displayQuitConfirmationDialog(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
suspendGridUpdates = true;
|
doQuit(app.app);
|
||||||
ServerHelper.doQuit(AppView.this,
|
|
||||||
ServerHelper.getCurrentAddressFromComputer(computer),
|
|
||||||
app.app, managerBinder, new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Trigger a poll immediately
|
|
||||||
suspendGridUpdates = false;
|
|
||||||
if (poller != null) {
|
|
||||||
poller.pollNow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, null);
|
}, null);
|
||||||
return true;
|
return true;
|
||||||
@@ -354,61 +376,26 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateUiWithServerinfo(final ComputerDetails details) {
|
|
||||||
AppView.this.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
boolean updated = false;
|
|
||||||
|
|
||||||
// Look through our current app list to tag the running app
|
|
||||||
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
|
||||||
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
|
||||||
|
|
||||||
// There can only be one or zero apps running.
|
|
||||||
if (existingApp.app.getIsRunning() &&
|
|
||||||
existingApp.app.getAppId() == details.runningGameId) {
|
|
||||||
// This app was running and still is, so we're done now
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (existingApp.app.getAppId() == details.runningGameId) {
|
|
||||||
// This app wasn't running but now is
|
|
||||||
existingApp.app.setIsRunning(true);
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
else if (existingApp.app.getIsRunning()) {
|
|
||||||
// This app was running but now isn't
|
|
||||||
existingApp.app.setIsRunning(false);
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// This app wasn't running and still isn't
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updated) {
|
|
||||||
appGridAdapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateUiWithAppList(final List<NvApp> appList) {
|
private void updateUiWithAppList(final List<NvApp> appList) {
|
||||||
AppView.this.runOnUiThread(new Runnable() {
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
boolean updated = false;
|
boolean updated = false;
|
||||||
|
|
||||||
// First handle app updates and additions
|
|
||||||
for (NvApp app : appList) {
|
for (NvApp app : appList) {
|
||||||
boolean foundExistingApp = false;
|
boolean foundExistingApp = false;
|
||||||
|
|
||||||
// Try to update an existing app in the list first
|
// Try to update an existing app in the list first
|
||||||
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||||
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||||
|
if (existingApp.app == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (existingApp.app.getAppId() == app.getAppId()) {
|
if (existingApp.app.getAppId() == app.getAppId()) {
|
||||||
// Found the app; update its properties
|
// Found the app; update its properties
|
||||||
if (existingApp.app.getIsRunning() != app.getIsRunning()) {
|
if (existingApp.app.getIsRunning() != app.getIsRunning()) {
|
||||||
existingApp.app.setIsRunning(app.getIsRunning());
|
existingApp.app.setIsRunningBoolean(app.getIsRunning());
|
||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
if (!existingApp.app.getAppName().equals(app.getAppName())) {
|
if (!existingApp.app.getAppName().equals(app.getAppName())) {
|
||||||
@@ -428,34 +415,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next handle app removals
|
|
||||||
int i = 0;
|
|
||||||
while (i < appGridAdapter.getCount()) {
|
|
||||||
boolean foundExistingApp = false;
|
|
||||||
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
|
||||||
|
|
||||||
// Check if this app is in the latest list
|
|
||||||
for (NvApp app : appList) {
|
|
||||||
if (existingApp.app.getAppId() == app.getAppId()) {
|
|
||||||
foundExistingApp = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This app was removed in the latest app list
|
|
||||||
if (!foundExistingApp) {
|
|
||||||
appGridAdapter.removeApp(existingApp);
|
|
||||||
updated = true;
|
|
||||||
|
|
||||||
// Check this same index again because the item at i+1 is now at i after
|
|
||||||
// the removal
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move on to the next item
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updated) {
|
if (updated) {
|
||||||
appGridAdapter.notifyDataSetChanged();
|
appGridAdapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
@@ -463,6 +422,58 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void doStart(NvApp app) {
|
||||||
|
Intent intent = new Intent(this, Game.class);
|
||||||
|
intent.putExtra(Game.EXTRA_HOST,
|
||||||
|
computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||||
|
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
|
||||||
|
intent.putExtra(Game.EXTRA_APP, app.getAppName());
|
||||||
|
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
||||||
|
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
||||||
|
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doQuit(final NvApp app) {
|
||||||
|
Toast.makeText(AppView.this, getResources().getString(R.string.applist_quit_app)+" "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
NvHTTP httpConn;
|
||||||
|
String message;
|
||||||
|
try {
|
||||||
|
httpConn = new NvHTTP(getAddress(),
|
||||||
|
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(AppView.this));
|
||||||
|
if (httpConn.quitApp()) {
|
||||||
|
message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName();
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
message = getResources().getString(R.string.error_unknown_host);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
message = getResources().getString(R.string.error_404);
|
||||||
|
} catch (Exception e) {
|
||||||
|
message = e.getMessage();
|
||||||
|
} finally {
|
||||||
|
// Trigger a poll immediately
|
||||||
|
if (poller != null) {
|
||||||
|
poller.pollNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String toastMessage = message;
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAdapterFragmentLayoutId() {
|
public int getAdapterFragmentLayoutId() {
|
||||||
return PreferenceConfiguration.readPreferences(this).listMode ?
|
return PreferenceConfiguration.readPreferences(this).listMode ?
|
||||||
@@ -478,26 +489,25 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||||
long id) {
|
long id) {
|
||||||
AppObject app = (AppObject) appGridAdapter.getItem(pos);
|
AppObject app = (AppObject) appGridAdapter.getItem(pos);
|
||||||
|
if (app == null || app.app == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Only open the context menu if something is running, otherwise start it
|
// Only open the context menu if something is running, otherwise start it
|
||||||
if (getRunningAppId() != -1) {
|
if (getRunningAppId() != -1) {
|
||||||
openContextMenu(arg1);
|
openContextMenu(arg1);
|
||||||
} else {
|
} else {
|
||||||
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
doStart(app.app);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
registerForContextMenu(listView);
|
registerForContextMenu(listView);
|
||||||
listView.requestFocus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AppObject {
|
public class AppObject {
|
||||||
public final NvApp app;
|
public 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import com.limelight.nvstream.NvConnection;
|
|||||||
import com.limelight.nvstream.NvConnectionListener;
|
import com.limelight.nvstream.NvConnectionListener;
|
||||||
import com.limelight.nvstream.StreamConfiguration;
|
import com.limelight.nvstream.StreamConfiguration;
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
import com.limelight.nvstream.http.NvApp;
|
|
||||||
import com.limelight.nvstream.input.KeyboardPacket;
|
import com.limelight.nvstream.input.KeyboardPacket;
|
||||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
@@ -26,7 +25,6 @@ import android.app.Activity;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.graphics.Point;
|
import android.graphics.Point;
|
||||||
import android.hardware.input.InputManager;
|
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.net.wifi.WifiManager;
|
import android.net.wifi.WifiManager;
|
||||||
@@ -61,7 +59,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private int lastButtonState = 0;
|
private int lastButtonState = 0;
|
||||||
|
|
||||||
// Only 2 touches are supported
|
// Only 2 touches are supported
|
||||||
private final TouchContext[] touchContextMap = new TouchContext[2];
|
private TouchContext[] touchContextMap = new TouchContext[2];
|
||||||
private long threeFingerDownTime = 0;
|
private long threeFingerDownTime = 0;
|
||||||
|
|
||||||
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
|
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
|
||||||
@@ -70,7 +68,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private KeyboardTranslator keybTranslator;
|
private KeyboardTranslator keybTranslator;
|
||||||
|
|
||||||
private PreferenceConfiguration prefConfig;
|
private PreferenceConfiguration prefConfig;
|
||||||
private final Point screenSize = new Point(0, 0);
|
private Point screenSize = new Point(0, 0);
|
||||||
|
|
||||||
private NvConnection conn;
|
private NvConnection conn;
|
||||||
private SpinnerDialog spinner;
|
private SpinnerDialog spinner;
|
||||||
@@ -90,8 +88,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private int drFlags = 0;
|
private int drFlags = 0;
|
||||||
|
|
||||||
public static final String EXTRA_HOST = "Host";
|
public static final String EXTRA_HOST = "Host";
|
||||||
public static final String EXTRA_APP_NAME = "AppName";
|
public static final String EXTRA_APP = "App";
|
||||||
public static final String EXTRA_APP_ID = "AppId";
|
|
||||||
public static final String EXTRA_UNIQUEID = "UniqueId";
|
public static final String EXTRA_UNIQUEID = "UniqueId";
|
||||||
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
||||||
|
|
||||||
@@ -173,23 +170,17 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
wifiLock.acquire();
|
wifiLock.acquire();
|
||||||
|
|
||||||
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
|
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
|
||||||
String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME);
|
String app = Game.this.getIntent().getStringExtra(EXTRA_APP);
|
||||||
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
|
|
||||||
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
||||||
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
|
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
|
||||||
|
|
||||||
if (appId == StreamConfiguration.INVALID_APP_ID) {
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
decoderRenderer = new ConfigurableDecoderRenderer();
|
decoderRenderer = new ConfigurableDecoderRenderer();
|
||||||
decoderRenderer.initializeWithFlags(drFlags);
|
decoderRenderer.initializeWithFlags(drFlags);
|
||||||
|
|
||||||
StreamConfiguration config = new StreamConfiguration.Builder()
|
StreamConfiguration config = new StreamConfiguration.Builder()
|
||||||
.setResolution(prefConfig.width, prefConfig.height)
|
.setResolution(prefConfig.width, prefConfig.height)
|
||||||
.setRefreshRate(prefConfig.fps)
|
.setRefreshRate(prefConfig.fps)
|
||||||
.setApp(new NvApp(appName, appId))
|
.setApp(app)
|
||||||
.setBitrate(prefConfig.bitrate * 1000)
|
.setBitrate(prefConfig.bitrate * 1000)
|
||||||
.setEnableSops(prefConfig.enableSops)
|
.setEnableSops(prefConfig.enableSops)
|
||||||
.enableAdaptiveResolution((decoderRenderer.getCapabilities() &
|
.enableAdaptiveResolution((decoderRenderer.getCapabilities() &
|
||||||
@@ -202,10 +193,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
// Initialize the connection
|
// Initialize the connection
|
||||||
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
|
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
|
||||||
keybTranslator = new KeyboardTranslator(conn);
|
keybTranslator = new KeyboardTranslator(conn);
|
||||||
controllerHandler = new ControllerHandler(conn, this, prefConfig.multiController, prefConfig.deadzonePercentage);
|
controllerHandler = new ControllerHandler(conn, this, 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()) {
|
||||||
@@ -234,12 +222,22 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
{
|
{
|
||||||
// Get the visible width of the activity
|
// Get the visible width of the activity
|
||||||
double visibleWidth = getWindow().getDecorView().getWidth();
|
double visibleWidth = getWindow().getDecorView().getWidth();
|
||||||
|
double visibleHeight = getWindow().getDecorView().getHeight();
|
||||||
|
|
||||||
ViewGroup.LayoutParams lp = sv.getLayoutParams();
|
ViewGroup.LayoutParams lp = sv.getLayoutParams();
|
||||||
|
|
||||||
|
double vidRatio = vidHeight / vidWidth;
|
||||||
|
double visibleRatio = visibleHeight / visibleWidth;
|
||||||
|
|
||||||
// Calculate the new size of the SurfaceView
|
// Calculate the new size of the SurfaceView
|
||||||
|
if (vidRatio < visibleRatio) {
|
||||||
lp.width = (int) visibleWidth;
|
lp.width = (int) visibleWidth;
|
||||||
lp.height = (int) ((vidHeight / vidWidth) * visibleWidth);
|
lp.height = (int) ((vidHeight / vidWidth) * visibleWidth);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lp.width = (int) ((vidWidth / vidHeight) * visibleHeight);
|
||||||
|
lp.height = (int) visibleHeight;
|
||||||
|
}
|
||||||
|
|
||||||
// Apply the size change
|
// Apply the size change
|
||||||
sv.setLayoutParams(lp);
|
sv.setLayoutParams(lp);
|
||||||
@@ -254,7 +252,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
private final Runnable hideSystemUi = new Runnable() {
|
private 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
|
||||||
@@ -290,9 +288,6 @@ 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();
|
||||||
|
|
||||||
@@ -323,7 +318,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
wifiLock.release();
|
wifiLock.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Runnable toggleGrab = new Runnable() {
|
private Runnable toggleGrab = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import com.limelight.computers.ComputerManagerListener;
|
|||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
import com.limelight.grid.PcGridAdapter;
|
import com.limelight.grid.PcGridAdapter;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.nvstream.http.NvApp;
|
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
import com.limelight.nvstream.http.PairingManager;
|
import com.limelight.nvstream.http.PairingManager;
|
||||||
import com.limelight.nvstream.http.PairingManager.PairState;
|
import com.limelight.nvstream.http.PairingManager.PairState;
|
||||||
@@ -23,10 +22,10 @@ import com.limelight.preferences.StreamSettings;
|
|||||||
import com.limelight.ui.AdapterFragment;
|
import com.limelight.ui.AdapterFragment;
|
||||||
import com.limelight.ui.AdapterFragmentCallbacks;
|
import com.limelight.ui.AdapterFragmentCallbacks;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
import com.limelight.utils.ServerHelper;
|
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.FragmentTransaction;
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@@ -54,7 +53,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
private PcGridAdapter pcGridAdapter;
|
private PcGridAdapter pcGridAdapter;
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
private boolean freezeUpdates, runningPolling;
|
private boolean freezeUpdates, runningPolling;
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
private 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);
|
||||||
@@ -95,8 +94,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
private final static int UNPAIR_ID = 3;
|
private final static int UNPAIR_ID = 3;
|
||||||
private final static int WOL_ID = 4;
|
private final static int WOL_ID = 4;
|
||||||
private final static int DELETE_ID = 5;
|
private final static int DELETE_ID = 5;
|
||||||
private final static int RESUME_ID = 6;
|
|
||||||
private final static int QUIT_ID = 7;
|
|
||||||
|
|
||||||
private void initializeViews() {
|
private void initializeViews() {
|
||||||
setContentView(R.layout.activity_pc_view);
|
setContentView(R.layout.activity_pc_view);
|
||||||
@@ -241,7 +238,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
||||||
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
if (computer == null || computer.details == null ||
|
||||||
|
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -256,16 +254,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (computer.details.runningGameId != 0) {
|
menu.add(Menu.NONE, APP_LIST_ID, 1, getResources().getString(R.string.pcview_menu_app_list));
|
||||||
menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
|
menu.add(Menu.NONE, UNPAIR_ID, 2, getResources().getString(R.string.pcview_menu_unpair_pc));
|
||||||
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.add(Menu.NONE, APP_LIST_ID, 3, getResources().getString(R.string.pcview_menu_app_list));
|
|
||||||
|
|
||||||
// FIXME: We used to be able to unpair here but it's been broken since GFE 2.1.x, so I've replaced
|
|
||||||
// it with delete which actually work
|
|
||||||
menu.add(Menu.NONE, DELETE_ID, 4, getResources().getString(R.string.pcview_menu_delete_pc));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,8 +476,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
@Override
|
@Override
|
||||||
public boolean onContextItemSelected(MenuItem item) {
|
public boolean onContextItemSelected(MenuItem item) {
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||||
final ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId())
|
||||||
|
{
|
||||||
case PAIR_ID:
|
case PAIR_ID:
|
||||||
doPair(computer.details);
|
doPair(computer.details);
|
||||||
return true;
|
return true;
|
||||||
@@ -513,32 +504,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
doAppList(computer.details);
|
doAppList(computer.details);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case RESUME_ID:
|
|
||||||
if (managerBinder == null) {
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId), computer.details, managerBinder);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case QUIT_ID:
|
|
||||||
if (managerBinder == null) {
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display a confirmation dialog first
|
|
||||||
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
ServerHelper.doQuit(PcView.this,
|
|
||||||
ServerHelper.getCurrentAddressFromComputer(computer.details),
|
|
||||||
new NvApp("app", 0), managerBinder, null);
|
|
||||||
}
|
|
||||||
}, null);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return super.onContextItemSelected(item);
|
return super.onContextItemSelected(item);
|
||||||
}
|
}
|
||||||
@@ -551,12 +516,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
if (details.equals(computer.details)) {
|
if (details.equals(computer.details)) {
|
||||||
pcGridAdapter.removeComputer(computer);
|
pcGridAdapter.removeComputer(computer);
|
||||||
pcGridAdapter.notifyDataSetChanged();
|
pcGridAdapter.notifyDataSetChanged();
|
||||||
|
|
||||||
if (pcGridAdapter.getCount() == 0) {
|
|
||||||
// Show the "Discovery in progress" view
|
|
||||||
noPcFoundLayout.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -569,7 +528,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
||||||
|
|
||||||
// Check if this is the same computer
|
// Check if this is the same computer
|
||||||
if (details.uuid.equals(computer.details.uuid)) {
|
if (details.equals(computer.details)) {
|
||||||
existingEntry = computer;
|
existingEntry = computer;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -626,9 +585,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
public ComputerDetails details;
|
public ComputerDetails details;
|
||||||
|
|
||||||
public ComputerObject(ComputerDetails details) {
|
public ComputerObject(ComputerDetails details) {
|
||||||
if (details == null) {
|
|
||||||
throw new IllegalArgumentException("details must not be null");
|
|
||||||
}
|
|
||||||
this.details = details;
|
this.details = details;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
|
|||||||
|
|
||||||
public class AndroidAudioRenderer implements AudioRenderer {
|
public class AndroidAudioRenderer implements AudioRenderer {
|
||||||
|
|
||||||
private static final int FRAME_SIZE = 960;
|
public 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 final File certFile;
|
private File certFile;
|
||||||
private final File keyFile;
|
private File keyFile;
|
||||||
|
|
||||||
private X509Certificate cert;
|
private X509Certificate cert;
|
||||||
private RSAPrivateKey key;
|
private RSAPrivateKey key;
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
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;
|
||||||
@@ -15,10 +13,26 @@ import com.limelight.nvstream.input.ControllerPacket;
|
|||||||
import com.limelight.ui.GameGestures;
|
import com.limelight.ui.GameGestures;
|
||||||
import com.limelight.utils.Vector2d;
|
import com.limelight.utils.Vector2d;
|
||||||
|
|
||||||
public class ControllerHandler implements InputManager.InputDeviceListener {
|
public class ControllerHandler {
|
||||||
|
private short inputMap = 0x0000;
|
||||||
|
private byte leftTrigger = 0x00;
|
||||||
|
private byte rightTrigger = 0x00;
|
||||||
|
private short rightStickX = 0x0000;
|
||||||
|
private short rightStickY = 0x0000;
|
||||||
|
private short leftStickX = 0x0000;
|
||||||
|
private short leftStickY = 0x0000;
|
||||||
|
private int emulatingButtonFlags = 0;
|
||||||
|
|
||||||
|
// Used for OUYA bumper state tracking since they force all buttons
|
||||||
|
// up when the OUYA button goes down. We watch the last time we get
|
||||||
|
// a bumper up and compare that to our maximum delay when we receive
|
||||||
|
// a Start button press to see if we should activate one of our
|
||||||
|
// emulated button combos.
|
||||||
|
private long lastLbUpTime = 0;
|
||||||
|
private long lastRbUpTime = 0;
|
||||||
private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
|
private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
|
||||||
|
|
||||||
|
private long startDownTime = 0;
|
||||||
private static final int START_DOWN_TIME_KEYB_MS = 750;
|
private static final int START_DOWN_TIME_KEYB_MS = 750;
|
||||||
|
|
||||||
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
|
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
|
||||||
@@ -29,23 +43,19 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
private static final int EMULATED_SPECIAL_UP_DELAY_MS = 100;
|
private static final int EMULATED_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 final Vector2d inputVector = new Vector2d();
|
private Vector2d inputVector = new Vector2d();
|
||||||
|
|
||||||
private final HashMap<String, ControllerContext> contexts = new HashMap<String, ControllerContext>();
|
private HashMap<String, ControllerMapping> mappings = new HashMap<String, ControllerMapping>();
|
||||||
|
|
||||||
private final NvConnection conn;
|
private NvConnection conn;
|
||||||
private final double stickDeadzone;
|
private double stickDeadzone;
|
||||||
private final ControllerContext defaultContext = new ControllerContext();
|
private final ControllerMapping defaultMapping = new ControllerMapping();
|
||||||
private final GameGestures gestures;
|
private GameGestures gestures;
|
||||||
private boolean hasGameController;
|
private boolean hasGameController;
|
||||||
|
|
||||||
private final boolean multiControllerEnabled;
|
public ControllerHandler(NvConnection conn, GameGestures gestures, int deadzonePercentage) {
|
||||||
private short currentControllers;
|
|
||||||
|
|
||||||
public ControllerHandler(NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
|
|
||||||
this.conn = conn;
|
this.conn = conn;
|
||||||
this.gestures = gestures;
|
this.gestures = gestures;
|
||||||
this.multiControllerEnabled = multiControllerEnabled;
|
|
||||||
|
|
||||||
// HACK: For now we're hardcoding a 10% deadzone. Some deadzone
|
// HACK: For now we're hardcoding a 10% deadzone. Some deadzone
|
||||||
// is required for controller batching support to work.
|
// is required for controller batching support to work.
|
||||||
@@ -72,16 +82,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
|
|
||||||
this.stickDeadzone = (double)deadzonePercentage / 100.0;
|
this.stickDeadzone = (double)deadzonePercentage / 100.0;
|
||||||
|
|
||||||
// Initialize the default context for events with no device
|
// Initialize the default mapping for events with no device
|
||||||
defaultContext.leftStickXAxis = MotionEvent.AXIS_X;
|
defaultMapping.leftStickXAxis = MotionEvent.AXIS_X;
|
||||||
defaultContext.leftStickYAxis = MotionEvent.AXIS_Y;
|
defaultMapping.leftStickYAxis = MotionEvent.AXIS_Y;
|
||||||
defaultContext.leftStickDeadzoneRadius = (float) stickDeadzone;
|
defaultMapping.leftStickDeadzoneRadius = (float) stickDeadzone;
|
||||||
defaultContext.rightStickXAxis = MotionEvent.AXIS_Z;
|
defaultMapping.rightStickXAxis = MotionEvent.AXIS_Z;
|
||||||
defaultContext.rightStickYAxis = MotionEvent.AXIS_RZ;
|
defaultMapping.rightStickYAxis = MotionEvent.AXIS_RZ;
|
||||||
defaultContext.rightStickDeadzoneRadius = (float) stickDeadzone;
|
defaultMapping.rightStickDeadzoneRadius = (float) stickDeadzone;
|
||||||
defaultContext.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
defaultMapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||||
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
defaultMapping.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) {
|
||||||
@@ -97,63 +106,19 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
return range;
|
return range;
|
||||||
}
|
}
|
||||||
|
|
||||||
private short assignNewControllerNumber() {
|
private ControllerMapping createMappingForDevice(InputDevice dev) {
|
||||||
for (short i = 0; i < 4; i++) {
|
ControllerMapping mapping = new ControllerMapping();
|
||||||
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 context for device: "+devName);
|
LimeLog.info("Creating controller mapping for device: "+devName);
|
||||||
|
|
||||||
context.name = devName;
|
mapping.leftStickXAxis = MotionEvent.AXIS_X;
|
||||||
context.id = dev.getId();
|
mapping.leftStickYAxis = MotionEvent.AXIS_Y;
|
||||||
|
if (getMotionRangeForJoystickAxis(dev, mapping.leftStickXAxis) != null &&
|
||||||
context.leftStickXAxis = MotionEvent.AXIS_X;
|
getMotionRangeForJoystickAxis(dev, mapping.leftStickYAxis) != null) {
|
||||||
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;
|
||||||
context.hasJoystickAxes = true;
|
mapping.hasJoystickAxes = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
|
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
|
||||||
@@ -163,14 +128,14 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
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)
|
||||||
context.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER;
|
mapping.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER;
|
||||||
context.rightTriggerAxis = MotionEvent.AXIS_RTRIGGER;
|
mapping.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)
|
||||||
context.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
mapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||||
context.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
mapping.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -179,34 +144,34 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
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
|
||||||
context.rightStickXAxis = MotionEvent.AXIS_RX;
|
mapping.rightStickXAxis = MotionEvent.AXIS_RX;
|
||||||
context.rightStickYAxis = MotionEvent.AXIS_RY;
|
mapping.rightStickYAxis = MotionEvent.AXIS_RY;
|
||||||
|
|
||||||
// Xbox controllers use Z and RZ for triggers
|
// Xbox controllers use Z and RZ for triggers
|
||||||
context.leftTriggerAxis = MotionEvent.AXIS_Z;
|
mapping.leftTriggerAxis = MotionEvent.AXIS_Z;
|
||||||
context.rightTriggerAxis = MotionEvent.AXIS_RZ;
|
mapping.rightTriggerAxis = MotionEvent.AXIS_RZ;
|
||||||
context.triggersIdleNegative = true;
|
mapping.triggersIdleNegative = true;
|
||||||
context.isXboxController = true;
|
mapping.isXboxController = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// DS4 controller uses RX and RY for triggers
|
// DS4 controller uses RX and RY for triggers
|
||||||
context.leftTriggerAxis = MotionEvent.AXIS_RX;
|
mapping.leftTriggerAxis = MotionEvent.AXIS_RX;
|
||||||
context.rightTriggerAxis = MotionEvent.AXIS_RY;
|
mapping.rightTriggerAxis = MotionEvent.AXIS_RY;
|
||||||
context.triggersIdleNegative = true;
|
mapping.triggersIdleNegative = true;
|
||||||
|
|
||||||
context.isDualShock4 = true;
|
mapping.isDualShock4 = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.rightStickXAxis == -1 && context.rightStickYAxis == -1) {
|
if (mapping.rightStickXAxis == -1 && mapping.rightStickYAxis == -1) {
|
||||||
InputDevice.MotionRange zRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z);
|
InputDevice.MotionRange 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) {
|
||||||
context.rightStickXAxis = MotionEvent.AXIS_Z;
|
mapping.rightStickXAxis = MotionEvent.AXIS_Z;
|
||||||
context.rightStickYAxis = MotionEvent.AXIS_RZ;
|
mapping.rightStickYAxis = MotionEvent.AXIS_RZ;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
||||||
@@ -214,8 +179,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
|
|
||||||
// Try RX and RY now
|
// Try RX and RY now
|
||||||
if (rxRange != null && ryRange != null) {
|
if (rxRange != null && ryRange != null) {
|
||||||
context.rightStickXAxis = MotionEvent.AXIS_RX;
|
mapping.rightStickXAxis = MotionEvent.AXIS_RX;
|
||||||
context.rightStickYAxis = MotionEvent.AXIS_RY;
|
mapping.rightStickYAxis = MotionEvent.AXIS_RY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,30 +189,30 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
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) {
|
||||||
context.hatXAxis = MotionEvent.AXIS_HAT_X;
|
mapping.hatXAxis = MotionEvent.AXIS_HAT_X;
|
||||||
context.hatYAxis = MotionEvent.AXIS_HAT_Y;
|
mapping.hatYAxis = MotionEvent.AXIS_HAT_Y;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
|
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
||||||
context.leftStickDeadzoneRadius = (float) stickDeadzone;
|
mapping.leftStickDeadzoneRadius = (float) stickDeadzone;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
|
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
||||||
context.rightStickDeadzoneRadius = (float) stickDeadzone;
|
mapping.rightStickDeadzoneRadius = (float) stickDeadzone;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
|
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
|
||||||
InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, context.leftTriggerAxis);
|
InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, mapping.leftTriggerAxis);
|
||||||
InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, context.rightTriggerAxis);
|
InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, mapping.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
|
||||||
context.triggerDeadzone = Math.max(Math.abs(ltRange.getFlat()), Math.abs(rtRange.getFlat()));
|
mapping.triggerDeadzone = Math.max(Math.abs(ltRange.getFlat()), Math.abs(rtRange.getFlat()));
|
||||||
|
|
||||||
// For triggers without (valid) deadzones, we'll use 13% (around XInput's default)
|
// For triggers without (valid) deadzones, we'll use 13% (around XInput's default)
|
||||||
if (context.triggerDeadzone < 0.13f ||
|
if (mapping.triggerDeadzone < 0.13f ||
|
||||||
context.triggerDeadzone > 0.30f)
|
mapping.triggerDeadzone > 0.30f)
|
||||||
{
|
{
|
||||||
context.triggerDeadzone = 0.13f;
|
mapping.triggerDeadzone = 0.13f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,86 +226,71 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
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]) {
|
||||||
context.backIsStart = true;
|
mapping.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
|
||||||
context.triggerDeadzone = 0.30f;
|
mapping.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 (!context.hasJoystickAxes) {
|
if (!mapping.hasJoystickAxes) {
|
||||||
context.isRemote = true;
|
mapping.isRemote = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// SHIELD controllers will use small stick deadzones
|
// NYKO Playpad has a fake hat that mimics the left stick for some reason
|
||||||
else if (devName.contains("SHIELD")) {
|
else if (devName.contains("NYKO PLAYPAD")) {
|
||||||
context.leftStickDeadzoneRadius = 0.07f;
|
mapping.hatXAxis = -1;
|
||||||
context.rightStickDeadzoneRadius = 0.07f;
|
mapping.hatYAxis = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius);
|
LimeLog.info("Analog stick deadzone: "+mapping.leftStickDeadzoneRadius+" "+mapping.rightStickDeadzoneRadius);
|
||||||
LimeLog.info("Trigger deadzone: "+context.triggerDeadzone);
|
LimeLog.info("Trigger deadzone: "+mapping.triggerDeadzone);
|
||||||
|
|
||||||
if (devName != null && devName.equals("gpio-keys")) {
|
return mapping;
|
||||||
// This is the back button on Shield portable consoles
|
|
||||||
context.controllerNumber = 0;
|
|
||||||
}
|
|
||||||
else if (multiControllerEnabled && context.hasJoystickAxes) {
|
|
||||||
context.controllerNumber = assignNewControllerNumber();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
context.controllerNumber = 0;
|
|
||||||
}
|
|
||||||
LimeLog.info("Assigned as controller "+context.controllerNumber);
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ControllerContext getContextForDevice(InputDevice dev) {
|
private ControllerMapping getMappingForDevice(InputDevice dev) {
|
||||||
// Unknown devices use the default context
|
// Unknown devices use the default mapping
|
||||||
if (dev == null) {
|
if (dev == null) {
|
||||||
return defaultContext;
|
return defaultMapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
String descriptor = dev.getDescriptor();
|
String descriptor = dev.getDescriptor();
|
||||||
|
|
||||||
// Return the existing context if it exists
|
// Return the existing mapping if it exists
|
||||||
ControllerContext context = contexts.get(descriptor);
|
ControllerMapping mapping = mappings.get(descriptor);
|
||||||
if (context != null) {
|
if (mapping != null) {
|
||||||
return context;
|
return mapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise create a new context
|
// Otherwise create a new mapping
|
||||||
context = createContextForDevice(dev);
|
mapping = createMappingForDevice(dev);
|
||||||
contexts.put(descriptor, context);
|
mappings.put(descriptor, mapping);
|
||||||
|
|
||||||
return context;
|
return mapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendControllerInputPacket(ControllerContext context) {
|
private void sendControllerInputPacket() {
|
||||||
conn.sendControllerInput(context.controllerNumber, context.inputMap,
|
conn.sendControllerInput(inputMap, leftTrigger, rightTrigger,
|
||||||
context.leftTrigger, context.rightTrigger,
|
leftStickX, leftStickY, rightStickX, rightStickY);
|
||||||
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(ControllerContext context, KeyEvent event) {
|
private int handleRemapping(ControllerMapping mapping, KeyEvent event) {
|
||||||
// For remotes, don't capture the back button
|
// For remotes, don't capture the back button
|
||||||
if (context.isRemote) {
|
if (mapping.isRemote) {
|
||||||
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.isDualShock4) {
|
if (mapping.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;
|
||||||
@@ -379,7 +329,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
|
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
|
||||||
switch (event.getKeyCode()) {
|
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:
|
||||||
@@ -390,9 +340,9 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (context.hatXAxis == -1 &&
|
else if (mapping.hatXAxis == -1 &&
|
||||||
context.hatYAxis == -1 &&
|
mapping.hatYAxis == -1 &&
|
||||||
context.isXboxController &&
|
mapping.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
|
||||||
@@ -425,20 +375,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
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
|
||||||
context.backIsStart = false;
|
mapping.backIsStart = false;
|
||||||
}
|
}
|
||||||
else if (keyCode == KeyEvent.KEYCODE_BUTTON_SELECT) {
|
else if (mapping.backIsStart && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
@@ -460,101 +402,101 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
// evaluates the deadzone.
|
// evaluates the deadzone.
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleAxisSet(ControllerContext context, float lsX, float lsY, float rsX,
|
private void handleAxisSet(ControllerMapping mapping, float lsX, float lsY, float rsX,
|
||||||
float rsY, float lt, float rt, float hatX, float hatY) {
|
float rsY, float lt, float rt, float hatX, float hatY) {
|
||||||
|
|
||||||
if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
|
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
||||||
Vector2d leftStickVector = populateCachedVector(lsX, lsY);
|
Vector2d leftStickVector = populateCachedVector(lsX, lsY);
|
||||||
|
|
||||||
handleDeadZone(leftStickVector, context.leftStickDeadzoneRadius);
|
handleDeadZone(leftStickVector, mapping.leftStickDeadzoneRadius);
|
||||||
|
|
||||||
context.leftStickX = (short) (leftStickVector.getX() * 0x7FFE);
|
leftStickX = (short) (leftStickVector.getX() * 0x7FFE);
|
||||||
context.leftStickY = (short) (-leftStickVector.getY() * 0x7FFE);
|
leftStickY = (short) (-leftStickVector.getY() * 0x7FFE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
|
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
||||||
Vector2d rightStickVector = populateCachedVector(rsX, rsY);
|
Vector2d rightStickVector = populateCachedVector(rsX, rsY);
|
||||||
|
|
||||||
handleDeadZone(rightStickVector, context.rightStickDeadzoneRadius);
|
handleDeadZone(rightStickVector, mapping.rightStickDeadzoneRadius);
|
||||||
|
|
||||||
context.rightStickX = (short) (rightStickVector.getX() * 0x7FFE);
|
rightStickX = (short) (rightStickVector.getX() * 0x7FFE);
|
||||||
context.rightStickY = (short) (-rightStickVector.getY() * 0x7FFE);
|
rightStickY = (short) (-rightStickVector.getY() * 0x7FFE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
|
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
|
||||||
if (context.triggersIdleNegative) {
|
if (mapping.triggersIdleNegative) {
|
||||||
lt = (lt + 1) / 2;
|
lt = (lt + 1) / 2;
|
||||||
rt = (rt + 1) / 2;
|
rt = (rt + 1) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lt <= context.triggerDeadzone) {
|
if (lt <= mapping.triggerDeadzone) {
|
||||||
lt = 0;
|
lt = 0;
|
||||||
}
|
}
|
||||||
if (rt <= context.triggerDeadzone) {
|
if (rt <= mapping.triggerDeadzone) {
|
||||||
rt = 0;
|
rt = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.leftTrigger = (byte)(lt * 0xFF);
|
leftTrigger = (byte)(lt * 0xFF);
|
||||||
context.rightTrigger = (byte)(rt * 0xFF);
|
rightTrigger = (byte)(rt * 0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
|
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
|
||||||
context.inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
|
inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
|
||||||
if (hatX < -0.5) {
|
if (hatX < -0.5) {
|
||||||
context.inputMap |= ControllerPacket.LEFT_FLAG;
|
inputMap |= ControllerPacket.LEFT_FLAG;
|
||||||
}
|
}
|
||||||
else if (hatX > 0.5) {
|
else if (hatX > 0.5) {
|
||||||
context.inputMap |= ControllerPacket.RIGHT_FLAG;
|
inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
|
inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
|
||||||
if (hatY < -0.5) {
|
if (hatY < -0.5) {
|
||||||
context.inputMap |= ControllerPacket.UP_FLAG;
|
inputMap |= ControllerPacket.UP_FLAG;
|
||||||
}
|
}
|
||||||
else if (hatY > 0.5) {
|
else if (hatY > 0.5) {
|
||||||
context.inputMap |= ControllerPacket.DOWN_FLAG;
|
inputMap |= ControllerPacket.DOWN_FLAG;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendControllerInputPacket(context);
|
sendControllerInputPacket();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean handleMotionEvent(MotionEvent event) {
|
public boolean handleMotionEvent(MotionEvent event) {
|
||||||
ControllerContext context = getContextForDevice(event.getDevice());
|
ControllerMapping mapping = getMappingForDevice(event.getDevice());
|
||||||
float lsX = 0, lsY = 0, rsX = 0, rsY = 0, rt = 0, lt = 0, hatX = 0, hatY = 0;
|
float lsX = 0, lsY = 0, rsX = 0, rsY = 0, rt = 0, lt = 0, hatX = 0, hatY = 0;
|
||||||
|
|
||||||
// We purposefully ignore the historical values in the motion event as it makes
|
// We purposefully ignore the historical values in the motion event as it makes
|
||||||
// the controller feel sluggish for some users.
|
// the controller feel sluggish for some users.
|
||||||
|
|
||||||
if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
|
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
||||||
lsX = event.getAxisValue(context.leftStickXAxis);
|
lsX = event.getAxisValue(mapping.leftStickXAxis);
|
||||||
lsY = event.getAxisValue(context.leftStickYAxis);
|
lsY = event.getAxisValue(mapping.leftStickYAxis);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
|
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
||||||
rsX = event.getAxisValue(context.rightStickXAxis);
|
rsX = event.getAxisValue(mapping.rightStickXAxis);
|
||||||
rsY = event.getAxisValue(context.rightStickYAxis);
|
rsY = event.getAxisValue(mapping.rightStickYAxis);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
|
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
|
||||||
lt = event.getAxisValue(context.leftTriggerAxis);
|
lt = event.getAxisValue(mapping.leftTriggerAxis);
|
||||||
rt = event.getAxisValue(context.rightTriggerAxis);
|
rt = event.getAxisValue(mapping.rightTriggerAxis);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
|
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
|
||||||
hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
|
hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
|
||||||
hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
|
hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAxisSet(context, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY);
|
handleAxisSet(mapping, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean handleButtonUp(KeyEvent event) {
|
public boolean handleButtonUp(KeyEvent event) {
|
||||||
ControllerContext context = getContextForDevice(event.getDevice());
|
ControllerMapping mapping = getMappingForDevice(event.getDevice());
|
||||||
|
|
||||||
int keyCode = handleRemapping(context, event);
|
int keyCode = handleRemapping(mapping, event);
|
||||||
if (keyCode == 0) {
|
if (keyCode == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -573,80 +515,78 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
|
|
||||||
switch (keyCode) {
|
switch (keyCode) {
|
||||||
case KeyEvent.KEYCODE_BUTTON_MODE:
|
case KeyEvent.KEYCODE_BUTTON_MODE:
|
||||||
context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_START:
|
case KeyEvent.KEYCODE_BUTTON_START:
|
||||||
case KeyEvent.KEYCODE_MENU:
|
case KeyEvent.KEYCODE_MENU:
|
||||||
if (SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) {
|
if (SystemClock.uptimeMillis() - 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
|
gestures.showKeyboard();
|
||||||
// until we have a custom keyboard or some other fix
|
|
||||||
//gestures.showKeyboard();
|
|
||||||
}
|
}
|
||||||
context.inputMap &= ~ControllerPacket.PLAY_FLAG;
|
inputMap &= ~ControllerPacket.PLAY_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BACK:
|
case KeyEvent.KEYCODE_BACK:
|
||||||
case KeyEvent.KEYCODE_BUTTON_SELECT:
|
case KeyEvent.KEYCODE_BUTTON_SELECT:
|
||||||
context.inputMap &= ~ControllerPacket.BACK_FLAG;
|
inputMap &= ~ControllerPacket.BACK_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||||
context.inputMap &= ~ControllerPacket.LEFT_FLAG;
|
inputMap &= ~ControllerPacket.LEFT_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||||
context.inputMap &= ~ControllerPacket.RIGHT_FLAG;
|
inputMap &= ~ControllerPacket.RIGHT_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_DPAD_UP:
|
case KeyEvent.KEYCODE_DPAD_UP:
|
||||||
context.inputMap &= ~ControllerPacket.UP_FLAG;
|
inputMap &= ~ControllerPacket.UP_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||||
context.inputMap &= ~ControllerPacket.DOWN_FLAG;
|
inputMap &= ~ControllerPacket.DOWN_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_B:
|
case KeyEvent.KEYCODE_BUTTON_B:
|
||||||
context.inputMap &= ~ControllerPacket.B_FLAG;
|
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:
|
||||||
context.inputMap &= ~ControllerPacket.A_FLAG;
|
inputMap &= ~ControllerPacket.A_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_X:
|
case KeyEvent.KEYCODE_BUTTON_X:
|
||||||
context.inputMap &= ~ControllerPacket.X_FLAG;
|
inputMap &= ~ControllerPacket.X_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_Y:
|
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||||
context.inputMap &= ~ControllerPacket.Y_FLAG;
|
inputMap &= ~ControllerPacket.Y_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_L1:
|
case KeyEvent.KEYCODE_BUTTON_L1:
|
||||||
context.inputMap &= ~ControllerPacket.LB_FLAG;
|
inputMap &= ~ControllerPacket.LB_FLAG;
|
||||||
context.lastLbUpTime = SystemClock.uptimeMillis();
|
lastLbUpTime = SystemClock.uptimeMillis();
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_R1:
|
case KeyEvent.KEYCODE_BUTTON_R1:
|
||||||
context.inputMap &= ~ControllerPacket.RB_FLAG;
|
inputMap &= ~ControllerPacket.RB_FLAG;
|
||||||
context.lastRbUpTime = SystemClock.uptimeMillis();
|
lastRbUpTime = SystemClock.uptimeMillis();
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_THUMBL:
|
case KeyEvent.KEYCODE_BUTTON_THUMBL:
|
||||||
context.inputMap &= ~ControllerPacket.LS_CLK_FLAG;
|
inputMap &= ~ControllerPacket.LS_CLK_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_THUMBR:
|
case KeyEvent.KEYCODE_BUTTON_THUMBR:
|
||||||
context.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
|
inputMap &= ~ControllerPacket.RS_CLK_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_L2:
|
case KeyEvent.KEYCODE_BUTTON_L2:
|
||||||
context.leftTrigger = 0;
|
leftTrigger = 0;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_R2:
|
case KeyEvent.KEYCODE_BUTTON_R2:
|
||||||
context.rightTrigger = 0;
|
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 ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
|
if ((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 ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
|
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
|
||||||
(context.inputMap & ControllerPacket.LB_FLAG) == 0)
|
(inputMap & ControllerPacket.LB_FLAG) == 0)
|
||||||
{
|
{
|
||||||
context.inputMap &= ~ControllerPacket.BACK_FLAG;
|
inputMap &= ~ControllerPacket.BACK_FLAG;
|
||||||
|
|
||||||
context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT;
|
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
|
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
|
||||||
@@ -655,16 +595,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if we're emulating the special button
|
// Check if we're emulating the special button
|
||||||
if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0)
|
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0)
|
||||||
{
|
{
|
||||||
// If either start or select and RB is up, the special button comes up too
|
// If either start or select and RB is up, the special button comes up too
|
||||||
if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
|
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
|
||||||
((context.inputMap & ControllerPacket.BACK_FLAG) == 0 &&
|
((inputMap & ControllerPacket.BACK_FLAG) == 0 &&
|
||||||
(context.inputMap & ControllerPacket.RB_FLAG) == 0))
|
(inputMap & ControllerPacket.RB_FLAG) == 0))
|
||||||
{
|
{
|
||||||
context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
|
|
||||||
context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL;
|
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
|
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
|
||||||
@@ -672,115 +612,112 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendControllerInputPacket(context);
|
sendControllerInputPacket();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean handleButtonDown(KeyEvent event) {
|
public boolean handleButtonDown(KeyEvent event) {
|
||||||
ControllerContext context = getContextForDevice(event.getDevice());
|
ControllerMapping mapping = getMappingForDevice(event.getDevice());
|
||||||
|
|
||||||
int keyCode = handleRemapping(context, event);
|
int keyCode = handleRemapping(mapping, event);
|
||||||
if (keyCode == 0) {
|
if (keyCode == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (keyCode) {
|
switch (keyCode) {
|
||||||
case KeyEvent.KEYCODE_BUTTON_MODE:
|
case KeyEvent.KEYCODE_BUTTON_MODE:
|
||||||
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_START:
|
case KeyEvent.KEYCODE_BUTTON_START:
|
||||||
case KeyEvent.KEYCODE_MENU:
|
case KeyEvent.KEYCODE_MENU:
|
||||||
if (event.getRepeatCount() == 0) {
|
if (event.getRepeatCount() == 0) {
|
||||||
context.startDownTime = SystemClock.uptimeMillis();
|
startDownTime = SystemClock.uptimeMillis();
|
||||||
}
|
}
|
||||||
context.inputMap |= ControllerPacket.PLAY_FLAG;
|
inputMap |= ControllerPacket.PLAY_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BACK:
|
case KeyEvent.KEYCODE_BACK:
|
||||||
case KeyEvent.KEYCODE_BUTTON_SELECT:
|
case KeyEvent.KEYCODE_BUTTON_SELECT:
|
||||||
context.inputMap |= ControllerPacket.BACK_FLAG;
|
inputMap |= ControllerPacket.BACK_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||||
context.inputMap |= ControllerPacket.LEFT_FLAG;
|
inputMap |= ControllerPacket.LEFT_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||||
context.inputMap |= ControllerPacket.RIGHT_FLAG;
|
inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_DPAD_UP:
|
case KeyEvent.KEYCODE_DPAD_UP:
|
||||||
context.inputMap |= ControllerPacket.UP_FLAG;
|
inputMap |= ControllerPacket.UP_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||||
context.inputMap |= ControllerPacket.DOWN_FLAG;
|
inputMap |= ControllerPacket.DOWN_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_B:
|
case KeyEvent.KEYCODE_BUTTON_B:
|
||||||
context.inputMap |= ControllerPacket.B_FLAG;
|
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:
|
||||||
context.inputMap |= ControllerPacket.A_FLAG;
|
inputMap |= ControllerPacket.A_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_X:
|
case KeyEvent.KEYCODE_BUTTON_X:
|
||||||
context.inputMap |= ControllerPacket.X_FLAG;
|
inputMap |= ControllerPacket.X_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_Y:
|
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||||
context.inputMap |= ControllerPacket.Y_FLAG;
|
inputMap |= ControllerPacket.Y_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_L1:
|
case KeyEvent.KEYCODE_BUTTON_L1:
|
||||||
context.inputMap |= ControllerPacket.LB_FLAG;
|
inputMap |= ControllerPacket.LB_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_R1:
|
case KeyEvent.KEYCODE_BUTTON_R1:
|
||||||
context.inputMap |= ControllerPacket.RB_FLAG;
|
inputMap |= ControllerPacket.RB_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_THUMBL:
|
case KeyEvent.KEYCODE_BUTTON_THUMBL:
|
||||||
context.inputMap |= ControllerPacket.LS_CLK_FLAG;
|
inputMap |= ControllerPacket.LS_CLK_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_THUMBR:
|
case KeyEvent.KEYCODE_BUTTON_THUMBR:
|
||||||
context.inputMap |= ControllerPacket.RS_CLK_FLAG;
|
inputMap |= ControllerPacket.RS_CLK_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_L2:
|
case KeyEvent.KEYCODE_BUTTON_L2:
|
||||||
context.leftTrigger = (byte)0xFF;
|
leftTrigger = (byte)0xFF;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_R2:
|
case KeyEvent.KEYCODE_BUTTON_R2:
|
||||||
context.rightTrigger = (byte)0xFF;
|
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 ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
|
if ((inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
|
||||||
((context.inputMap & ControllerPacket.LB_FLAG) != 0 ||
|
((inputMap & ControllerPacket.LB_FLAG) != 0 ||
|
||||||
SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
|
SystemClock.uptimeMillis() - lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
|
||||||
{
|
{
|
||||||
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
|
inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
|
||||||
context.inputMap |= ControllerPacket.BACK_FLAG;
|
inputMap |= ControllerPacket.BACK_FLAG;
|
||||||
|
|
||||||
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
|
emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We detect select+start or start+RB as the special button combo
|
// We detect select+start or start+RB as the special button combo
|
||||||
if (((context.inputMap & ControllerPacket.RB_FLAG) != 0 ||
|
if (((inputMap & ControllerPacket.RB_FLAG) != 0 ||
|
||||||
(SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
|
(SystemClock.uptimeMillis() - lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
|
||||||
(context.inputMap & ControllerPacket.BACK_FLAG) != 0) &&
|
(inputMap & ControllerPacket.BACK_FLAG) != 0) &&
|
||||||
(context.inputMap & ControllerPacket.PLAY_FLAG) != 0)
|
(inputMap & ControllerPacket.PLAY_FLAG) != 0)
|
||||||
{
|
{
|
||||||
context.inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
|
inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
|
||||||
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
|
|
||||||
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send a new input packet if this is the first instance of a button down event
|
// 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 || context.emulatingButtonFlags != 0) {
|
if (event.getRepeatCount() == 0 || emulatingButtonFlags != 0) {
|
||||||
sendControllerInputPacket(context);
|
sendControllerInputPacket();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ControllerContext {
|
class ControllerMapping {
|
||||||
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;
|
||||||
@@ -800,29 +737,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
|||||||
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
|
||||||
*/
|
*/
|
||||||
private static final short KEY_PREFIX = (short) 0x80;
|
public static final short KEY_PREFIX = (short) 0x80;
|
||||||
|
|
||||||
public static final int VK_0 = 48;
|
public static final int VK_0 = 48;
|
||||||
public static final int VK_9 = 57;
|
public static final int VK_9 = 57;
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ public class TouchContext {
|
|||||||
private long originalTouchTime = 0;
|
private long originalTouchTime = 0;
|
||||||
private boolean cancelled;
|
private boolean cancelled;
|
||||||
|
|
||||||
private final NvConnection conn;
|
private NvConnection conn;
|
||||||
private final int actionIndex;
|
private int actionIndex;
|
||||||
private final double xFactor;
|
private double xFactor, yFactor;
|
||||||
private final double yFactor;
|
|
||||||
|
|
||||||
private static final int TAP_MOVEMENT_THRESHOLD = 10;
|
private static final int TAP_MOVEMENT_THRESHOLD = 10;
|
||||||
private static final int TAP_TIME_THRESHOLD = 250;
|
private static final int TAP_TIME_THRESHOLD = 250;
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ public class EvdevEvent {
|
|||||||
/* Keys */
|
/* Keys */
|
||||||
public static final short KEY_Q = 16;
|
public static final short KEY_Q = 16;
|
||||||
|
|
||||||
public final short type;
|
public short type;
|
||||||
public final short code;
|
public short code;
|
||||||
public final int value;
|
public int value;
|
||||||
|
|
||||||
public EvdevEvent(short type, short code, int value) {
|
public EvdevEvent(short type, short code, int value) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import com.limelight.LimeLog;
|
|||||||
|
|
||||||
public class EvdevHandler {
|
public class EvdevHandler {
|
||||||
|
|
||||||
private final String absolutePath;
|
private String absolutePath;
|
||||||
private final EvdevListener listener;
|
private EvdevListener listener;
|
||||||
private boolean shutdown = false;
|
private boolean shutdown = false;
|
||||||
private int fd = -1;
|
private int fd = -1;
|
||||||
|
|
||||||
private final Thread handlerThread = new Thread() {
|
private Thread handlerThread = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
// All the finally blocks here make this code look like a mess
|
// All the finally blocks here make this code look like a mess
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public class EvdevReader {
|
|||||||
// 4.4 and later to do live SELinux policy changes.
|
// 4.4 and later to do live SELinux policy changes.
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
EvdevShell shell = EvdevShell.getInstance();
|
EvdevShell shell = EvdevShell.getInstance();
|
||||||
shell.runCommand("supolicy --live \"allow untrusted_app input_device dir { open getattr read search }\" " +
|
shell.runCommand("supolicy --live \"allow untrusted_app input_device dir { getattr read search }\" " +
|
||||||
"\"allow untrusted_app input_device chr_file { open read write ioctl }\"");
|
"\"allow untrusted_app input_device chr_file { open read write ioctl }\"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import android.view.KeyEvent;
|
|||||||
|
|
||||||
public class EvdevTranslator {
|
public class EvdevTranslator {
|
||||||
|
|
||||||
private static final short[] EVDEV_KEY_CODES = {
|
public static final short EVDEV_KEY_CODES[] = {
|
||||||
0, //KeyEvent.VK_RESERVED
|
0, //KeyEvent.VK_RESERVED
|
||||||
KeyEvent.KEYCODE_ESCAPE,
|
KeyEvent.KEYCODE_ESCAPE,
|
||||||
KeyEvent.KEYCODE_1,
|
KeyEvent.KEYCODE_1,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
private int totalFrames;
|
private int totalFrames;
|
||||||
private long totalTimeMs;
|
private long totalTimeMs;
|
||||||
|
|
||||||
private final int cpuCount = Runtime.getRuntime().availableProcessors();
|
private int cpuCount = Runtime.getRuntime().availableProcessors();
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private int findOptimalPerformanceLevel() {
|
private int findOptimalPerformanceLevel() {
|
||||||
@@ -178,6 +178,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
long nextFrameTime = System.currentTimeMillis();
|
long nextFrameTime = System.currentTimeMillis();
|
||||||
|
DecodeUnit du;
|
||||||
while (!isInterrupted())
|
while (!isInterrupted())
|
||||||
{
|
{
|
||||||
long diff = nextFrameTime - System.currentTimeMillis();
|
long diff = nextFrameTime - System.currentTimeMillis();
|
||||||
@@ -231,8 +232,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
|
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
|
||||||
decoderBuffer.clear();
|
decoderBuffer.clear();
|
||||||
|
|
||||||
for (ByteBufferDescriptor bbd = decodeUnit.getBufferHead();
|
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
|
||||||
bbd != null; bbd = bbd.nextDescriptor) {
|
|
||||||
decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
|
decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,8 +242,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
|
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
|
||||||
|
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
for (ByteBufferDescriptor bbd = decodeUnit.getBufferHead();
|
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
|
||||||
bbd != null; bbd = bbd.nextDescriptor) {
|
|
||||||
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
|
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
|
||||||
offset += bbd.length;
|
offset += bbd.length;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.limelight.binding.video;
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
import com.limelight.nvstream.av.DecodeUnit;
|
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||||
|
|
||||||
@@ -56,11 +55,6 @@ public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
return decoderRenderer.getCapabilities();
|
return decoderRenderer.getCapabilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void directSubmitDecodeUnit(DecodeUnit du) {
|
|
||||||
decoderRenderer.directSubmitDecodeUnit(du);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAverageDecoderLatency() {
|
public int getAverageDecoderLatency() {
|
||||||
if (decoderRenderer != null) {
|
if (decoderRenderer != null) {
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ package com.limelight.binding.video;
|
|||||||
|
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
|
||||||
public abstract class EnhancedDecoderRenderer extends VideoDecoderRenderer {
|
public abstract class EnhancedDecoderRenderer implements VideoDecoderRenderer {
|
||||||
public abstract String getDecoderName();
|
public abstract String getDecoderName();
|
||||||
}
|
}
|
||||||
@@ -28,9 +28,9 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
private ByteBuffer[] videoDecoderInputBuffers;
|
private ByteBuffer[] videoDecoderInputBuffers;
|
||||||
private MediaCodec videoDecoder;
|
private MediaCodec videoDecoder;
|
||||||
private Thread rendererThread;
|
private Thread rendererThread;
|
||||||
private final boolean needsSpsBitstreamFixup, isExynos4;
|
private boolean needsSpsBitstreamFixup, isExynos4;
|
||||||
private VideoDepacketizer depacketizer;
|
private VideoDepacketizer depacketizer;
|
||||||
private final boolean adaptivePlayback, directSubmit;
|
private boolean adaptivePlayback;
|
||||||
private int initialWidth, initialHeight;
|
private int initialWidth, initialHeight;
|
||||||
|
|
||||||
private boolean needsBaselineSpsHack;
|
private boolean needsBaselineSpsHack;
|
||||||
@@ -46,6 +46,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
private int numPpsIn;
|
private int numPpsIn;
|
||||||
private int numIframeIn;
|
private int numIframeIn;
|
||||||
|
|
||||||
|
private static final boolean ENABLE_ASYNC_RENDERER = false;
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||||
public MediaCodecDecoderRenderer() {
|
public MediaCodecDecoderRenderer() {
|
||||||
//dumpDecoders();
|
//dumpDecoders();
|
||||||
@@ -56,15 +58,12 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
if (decoder == null) {
|
if (decoder == null) {
|
||||||
// This case is handled later in setup()
|
// This case is handled later in setup()
|
||||||
needsSpsBitstreamFixup = isExynos4 =
|
|
||||||
adaptivePlayback = directSubmit = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
decoderName = decoder.getName();
|
decoderName = decoder.getName();
|
||||||
|
|
||||||
// Set decoder-specific attributes
|
// Set decoder-specific attributes
|
||||||
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(decoderName, decoder);
|
|
||||||
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
|
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
|
||||||
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
|
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
|
||||||
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder);
|
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder);
|
||||||
@@ -78,11 +77,9 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
if (isExynos4) {
|
if (isExynos4) {
|
||||||
LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
|
LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
|
||||||
}
|
}
|
||||||
if (directSubmit) {
|
|
||||||
LimeLog.info("Decoder "+decoderName+" will use direct submit");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
@Override
|
@Override
|
||||||
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||||
this.initialWidth = width;
|
this.initialWidth = width;
|
||||||
@@ -110,6 +107,52 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
|
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On Lollipop, we use asynchronous mode to avoid having a busy looping renderer thread
|
||||||
|
if (ENABLE_ASYNC_RENDERER && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
videoDecoder.setCallback(new MediaCodec.Callback() {
|
||||||
|
@Override
|
||||||
|
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
|
||||||
|
LimeLog.info("Output format changed");
|
||||||
|
LimeLog.info("New output Format: " + format);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOutputBufferAvailable(MediaCodec codec, int index,
|
||||||
|
BufferInfo info) {
|
||||||
|
try {
|
||||||
|
// FIXME: It looks like we can't frameskip here
|
||||||
|
codec.releaseOutputBuffer(index, true);
|
||||||
|
} catch (Exception e) {
|
||||||
|
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInputBufferAvailable(MediaCodec codec, int index) {
|
||||||
|
try {
|
||||||
|
submitDecodeUnit(depacketizer.takeNextDecodeUnit(), codec.getInputBuffer(index), index);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// What do we do here?
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (Exception e) {
|
||||||
|
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(MediaCodec codec, CodecException e) {
|
||||||
|
if (e.isTransient()) {
|
||||||
|
LimeLog.warning(e.getDiagnosticInfo());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.severe(e.getDiagnosticInfo());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
|
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
|
||||||
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
||||||
|
|
||||||
@@ -119,7 +162,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
private void handleDecoderException(Exception e, ByteBuffer buf, int codecFlags) {
|
private void handleDecoderException(MediaCodecDecoderRenderer dr, Exception e, ByteBuffer buf, int codecFlags) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
if (e instanceof CodecException) {
|
if (e instanceof CodecException) {
|
||||||
CodecException codecExc = (CodecException) e;
|
CodecException codecExc = (CodecException) e;
|
||||||
@@ -135,92 +178,14 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (buf != null || codecFlags != 0) {
|
if (buf != null || codecFlags != 0) {
|
||||||
throw new RendererException(this, e, buf, codecFlags);
|
throw new RendererException(dr, e, buf, codecFlags);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new RendererException(this, e);
|
throw new RendererException(dr, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startDirectSubmitRendererThread()
|
private void startRendererThread()
|
||||||
{
|
|
||||||
rendererThread = new Thread() {
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
BufferInfo info = new BufferInfo();
|
|
||||||
while (!isInterrupted()) {
|
|
||||||
try {
|
|
||||||
// Try to output a frame
|
|
||||||
int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
|
|
||||||
if (outIndex >= 0) {
|
|
||||||
long presentationTimeUs = info.presentationTimeUs;
|
|
||||||
int lastIndex = outIndex;
|
|
||||||
|
|
||||||
// Get the last output buffer in the queue
|
|
||||||
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
|
|
||||||
videoDecoder.releaseOutputBuffer(lastIndex, false);
|
|
||||||
|
|
||||||
lastIndex = outIndex;
|
|
||||||
presentationTimeUs = info.presentationTimeUs;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render the last buffer
|
|
||||||
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
|
||||||
|
|
||||||
// Add delta time to the totals (excluding probable outliers)
|
|
||||||
long delta = System.currentTimeMillis() - (presentationTimeUs / 1000);
|
|
||||||
if (delta >= 0 && delta < 1000) {
|
|
||||||
decoderTimeMs += delta;
|
|
||||||
totalTimeMs += delta;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (outIndex) {
|
|
||||||
case MediaCodec.INFO_TRY_AGAIN_LATER:
|
|
||||||
break;
|
|
||||||
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
|
|
||||||
LimeLog.info("Output buffers changed");
|
|
||||||
break;
|
|
||||||
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
|
||||||
LimeLog.info("Output format changed");
|
|
||||||
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
handleDecoderException(e, null, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
rendererThread.setName("Video - Renderer (MediaCodec)");
|
|
||||||
rendererThread.setPriority(Thread.NORM_PRIORITY + 2);
|
|
||||||
rendererThread.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private int dequeueInputBuffer(boolean wait, boolean infiniteWait) {
|
|
||||||
int index;
|
|
||||||
long startTime, queueTime;
|
|
||||||
|
|
||||||
startTime = System.currentTimeMillis();
|
|
||||||
|
|
||||||
index = videoDecoder.dequeueInputBuffer(wait ? (infiniteWait ? -1 : 3000) : 0);
|
|
||||||
if (index < 0) {
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
queueTime = System.currentTimeMillis();
|
|
||||||
|
|
||||||
if (queueTime - startTime >= 20) {
|
|
||||||
LimeLog.warning("Queue input buffer ran long: "+(queueTime - startTime)+" ms");
|
|
||||||
}
|
|
||||||
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startLegacyRendererThread()
|
|
||||||
{
|
{
|
||||||
rendererThread = new Thread() {
|
rendererThread = new Thread() {
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
@@ -229,7 +194,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
BufferInfo info = new BufferInfo();
|
BufferInfo info = new BufferInfo();
|
||||||
DecodeUnit du = null;
|
DecodeUnit du = null;
|
||||||
int inputIndex = -1;
|
int inputIndex = -1;
|
||||||
long lastDuDequeueTime = 0;
|
|
||||||
while (!isInterrupted())
|
while (!isInterrupted())
|
||||||
{
|
{
|
||||||
// In order to get as much data to the decoder as early as possible,
|
// In order to get as much data to the decoder as early as possible,
|
||||||
@@ -237,12 +201,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
if (inputIndex == -1 && du == null) {
|
if (inputIndex == -1 && du == null) {
|
||||||
try {
|
try {
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
inputIndex = dequeueInputBuffer(false, false);
|
inputIndex = videoDecoder.dequeueInputBuffer(0);
|
||||||
du = depacketizer.pollNextDecodeUnit();
|
du = depacketizer.pollNextDecodeUnit();
|
||||||
if (du != null) {
|
|
||||||
lastDuDequeueTime = System.currentTimeMillis();
|
|
||||||
notifyDuReceived(du);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop if we can't get a DU or input buffer
|
// Stop if we can't get a DU or input buffer
|
||||||
if (du == null || inputIndex == -1) {
|
if (du == null || inputIndex == -1) {
|
||||||
@@ -256,7 +216,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
inputIndex = -1;
|
inputIndex = -1;
|
||||||
handleDecoderException(e, null, 0);
|
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,30 +231,21 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
// If we've got a DU waiting to be given to the decoder,
|
// If we've got a DU waiting to be given to the decoder,
|
||||||
// wait a full 3 ms for an input buffer. Otherwise
|
// wait a full 3 ms for an input buffer. Otherwise
|
||||||
// just see if we can get one immediately.
|
// just see if we can get one immediately.
|
||||||
inputIndex = dequeueInputBuffer(du != null, false);
|
inputIndex = videoDecoder.dequeueInputBuffer(du != null ? 3000 : 0);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
inputIndex = -1;
|
inputIndex = -1;
|
||||||
handleDecoderException(e, null, 0);
|
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab a decode unit if we don't have one already
|
// Grab a decode unit if we don't have one already
|
||||||
if (du == null) {
|
if (du == null) {
|
||||||
du = depacketizer.pollNextDecodeUnit();
|
du = depacketizer.pollNextDecodeUnit();
|
||||||
if (du != null) {
|
|
||||||
lastDuDequeueTime = System.currentTimeMillis();
|
|
||||||
notifyDuReceived(du);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we've got both a decode unit and an input buffer, we'll
|
// If we've got both a decode unit and an input buffer, we'll
|
||||||
// submit now. Otherwise, we wait until we have one.
|
// submit now. Otherwise, we wait until we have one.
|
||||||
if (du != null && inputIndex >= 0) {
|
if (du != null && inputIndex >= 0) {
|
||||||
long submissionTime = System.currentTimeMillis();
|
|
||||||
if (submissionTime - lastDuDequeueTime >= 20) {
|
|
||||||
LimeLog.warning("Receiving an input buffer took too long: "+(submissionTime - lastDuDequeueTime)+" ms");
|
|
||||||
}
|
|
||||||
|
|
||||||
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
|
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
|
||||||
|
|
||||||
// DU and input buffer have both been consumed
|
// DU and input buffer have both been consumed
|
||||||
@@ -347,7 +298,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
handleDecoderException(e, null, 0);
|
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -365,15 +316,11 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
// Start the decoder
|
// Start the decoder
|
||||||
videoDecoder.start();
|
videoDecoder.start();
|
||||||
|
|
||||||
|
// On devices pre-Lollipop, we'll use a rendering thread
|
||||||
|
if (!ENABLE_ASYNC_RENDERER || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
|
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
|
||||||
|
startRendererThread();
|
||||||
if (directSubmit) {
|
|
||||||
startDirectSubmitRendererThread();
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
startLegacyRendererThread();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,7 +357,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
timestampUs, codecFlags);
|
timestampUs, codecFlags);
|
||||||
break;
|
break;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
handleDecoderException(e, null, codecFlags);
|
handleDecoderException(this, e, null, codecFlags);
|
||||||
lastException = e;
|
lastException = e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -422,7 +369,14 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
|
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
|
||||||
long timestampUs = System.currentTimeMillis() * 1000;
|
long currentTime = System.currentTimeMillis();
|
||||||
|
long delta = currentTime-decodeUnit.getReceiveTimestamp();
|
||||||
|
if (delta >= 0 && delta < 1000) {
|
||||||
|
totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp();
|
||||||
|
totalFrames++;
|
||||||
|
}
|
||||||
|
|
||||||
|
long timestampUs = currentTime * 1000;
|
||||||
if (timestampUs <= lastTimestampUs) {
|
if (timestampUs <= lastTimestampUs) {
|
||||||
// We can't submit multiple buffers with the same timestamp
|
// We can't submit multiple buffers with the same timestamp
|
||||||
// so bump it up by one before queuing
|
// so bump it up by one before queuing
|
||||||
@@ -446,7 +400,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
boolean needsSpsReplay = false;
|
boolean needsSpsReplay = false;
|
||||||
|
|
||||||
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
|
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
|
||||||
ByteBufferDescriptor header = decodeUnit.getBufferHead();
|
ByteBufferDescriptor header = decodeUnit.getBufferList().get(0);
|
||||||
if (header.data[header.offset+4] == 0x67) {
|
if (header.data[header.offset+4] == 0x67) {
|
||||||
numSpsIn++;
|
numSpsIn++;
|
||||||
|
|
||||||
@@ -531,8 +485,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Copy data from our buffer list into the input buffer
|
// Copy data from our buffer list into the input buffer
|
||||||
for (ByteBufferDescriptor desc = decodeUnit.getBufferHead();
|
for (ByteBufferDescriptor desc : decodeUnit.getBufferList())
|
||||||
desc != null; desc = desc.nextDescriptor) {
|
{
|
||||||
buf.put(desc.data, desc.offset, desc.length);
|
buf.put(desc.data, desc.offset, desc.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,7 +502,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void replaySps() {
|
private void replaySps() {
|
||||||
int inputIndex = dequeueInputBuffer(true, true);
|
int inputIndex = videoDecoder.dequeueInputBuffer(-1);
|
||||||
ByteBuffer inputBuffer = videoDecoderInputBuffers[inputIndex];
|
ByteBuffer inputBuffer = videoDecoderInputBuffers[inputIndex];
|
||||||
|
|
||||||
inputBuffer.clear();
|
inputBuffer.clear();
|
||||||
@@ -576,15 +530,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCapabilities() {
|
public int getCapabilities() {
|
||||||
int caps = 0;
|
return adaptivePlayback ?
|
||||||
|
|
||||||
caps |= adaptivePlayback ?
|
|
||||||
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0;
|
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0;
|
||||||
|
|
||||||
caps |= directSubmit ?
|
|
||||||
VideoDecoderRenderer.CAPABILITY_DIRECT_SUBMIT : 0;
|
|
||||||
|
|
||||||
return caps;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -608,40 +555,11 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
return decoderName;
|
return decoderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyDuReceived(DecodeUnit du) {
|
|
||||||
long currentTime = System.currentTimeMillis();
|
|
||||||
long delta = currentTime-du.getReceiveTimestamp();
|
|
||||||
if (delta >= 0 && delta < 1000) {
|
|
||||||
totalTimeMs += currentTime-du.getReceiveTimestamp();
|
|
||||||
totalFrames++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void directSubmitDecodeUnit(DecodeUnit du) {
|
|
||||||
int inputIndex;
|
|
||||||
|
|
||||||
notifyDuReceived(du);
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
try {
|
|
||||||
inputIndex = dequeueInputBuffer(true, true);
|
|
||||||
break;
|
|
||||||
} catch (Exception e) {
|
|
||||||
handleDecoderException(e, null, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputIndex >= 0) {
|
|
||||||
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RendererException extends RuntimeException {
|
public class RendererException extends RuntimeException {
|
||||||
private static final long serialVersionUID = 8985937536997012406L;
|
private static final long serialVersionUID = 8985937536997012406L;
|
||||||
|
|
||||||
private final Exception originalException;
|
private Exception originalException;
|
||||||
private final MediaCodecDecoderRenderer renderer;
|
private MediaCodecDecoderRenderer renderer;
|
||||||
private ByteBuffer currentBuffer;
|
private ByteBuffer currentBuffer;
|
||||||
private int currentCodecFlags;
|
private int currentCodecFlags;
|
||||||
|
|
||||||
|
|||||||
@@ -20,26 +20,12 @@ import com.limelight.LimeLog;
|
|||||||
|
|
||||||
public class MediaCodecHelper {
|
public class MediaCodecHelper {
|
||||||
|
|
||||||
private static final List<String> preferredDecoders;
|
public static final List<String> preferredDecoders;
|
||||||
|
|
||||||
private static final List<String> blacklistedDecoderPrefixes;
|
public static final List<String> blacklistedDecoderPrefixes;
|
||||||
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
||||||
private static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
public static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
||||||
private static final List<String> baselineProfileHackPrefixes;
|
public static final List<String> baselineProfileHackPrefixes;
|
||||||
private static final List<String> directSubmitPrefixes;
|
|
||||||
|
|
||||||
static {
|
|
||||||
directSubmitPrefixes = new LinkedList<String>();
|
|
||||||
|
|
||||||
// These decoders have low enough input buffer latency that they
|
|
||||||
// can be directly invoked from the receive thread
|
|
||||||
directSubmitPrefixes.add("omx.qcom");
|
|
||||||
directSubmitPrefixes.add("omx.sec");
|
|
||||||
directSubmitPrefixes.add("omx.exynos");
|
|
||||||
directSubmitPrefixes.add("omx.intel");
|
|
||||||
directSubmitPrefixes.add("omx.brcm");
|
|
||||||
directSubmitPrefixes.add("omx.TI");
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
preferredDecoders = new LinkedList<String>();
|
preferredDecoders = new LinkedList<String>();
|
||||||
@@ -112,10 +98,6 @@ public class MediaCodecHelper {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean decoderCanDirectSubmit(String decoderName, MediaCodecInfo decoderInfo) {
|
|
||||||
return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
|
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
|
||||||
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
|
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
|
||||||
}
|
}
|
||||||
@@ -164,7 +146,7 @@ public class MediaCodecHelper {
|
|||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaCodecInfo findPreferredDecoder() {
|
public static MediaCodecInfo findPreferredDecoder() {
|
||||||
// This is a different algorithm than the other findXXXDecoder functions,
|
// This is a different algorithm than the other findXXXDecoder functions,
|
||||||
// because we want to evaluate the decoders in our list's order
|
// because we want to evaluate the decoders in our list's order
|
||||||
// rather than MediaCodecList's order
|
// rather than MediaCodecList's order
|
||||||
@@ -235,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")
|
||||||
private static MediaCodecInfo findKnownSafeDecoder() throws Exception {
|
public static MediaCodecInfo findKnownSafeDecoder() throws Exception {
|
||||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||||
// Skip encoders
|
// Skip encoders
|
||||||
if (codecInfo.isEncoder()) {
|
if (codecInfo.isEncoder()) {
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
package com.limelight.computers;
|
package com.limelight.computers;
|
||||||
|
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.net.Socket;
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -31,21 +30,18 @@ import android.os.IBinder;
|
|||||||
import org.xmlpull.v1.XmlPullParserException;
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
public class ComputerManagerService extends Service {
|
public class ComputerManagerService extends Service {
|
||||||
private static final int SERVERINFO_POLLING_PERIOD_MS = 3000;
|
private static final int POLLING_PERIOD_MS = 3000;
|
||||||
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
|
|
||||||
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||||
private static final int FAST_POLL_TIMEOUT = 500;
|
|
||||||
private static final int OFFLINE_POLL_TRIES = 3;
|
|
||||||
|
|
||||||
private final ComputerManagerBinder binder = new ComputerManagerBinder();
|
private ComputerManagerBinder binder = new ComputerManagerBinder();
|
||||||
|
|
||||||
private ComputerDatabaseManager dbManager;
|
private ComputerDatabaseManager dbManager;
|
||||||
private final AtomicInteger dbRefCount = new AtomicInteger(0);
|
private AtomicInteger dbRefCount = new AtomicInteger(0);
|
||||||
|
|
||||||
private IdentityManager idManager;
|
private IdentityManager idManager;
|
||||||
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
|
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
|
||||||
private ComputerManagerListener listener = null;
|
private ComputerManagerListener listener = null;
|
||||||
private final AtomicInteger activePolls = new AtomicInteger(0);
|
private AtomicInteger activePolls = new AtomicInteger(0);
|
||||||
private boolean pollingActive = false;
|
private boolean pollingActive = false;
|
||||||
|
|
||||||
private DiscoveryService.DiscoveryBinder discoveryBinder;
|
private DiscoveryService.DiscoveryBinder discoveryBinder;
|
||||||
@@ -69,7 +65,10 @@ 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, boolean newPc, int offlineCount) throws InterruptedException {
|
private boolean runPoll(ComputerDetails details)
|
||||||
|
{
|
||||||
|
boolean newPc = details.name.isEmpty();
|
||||||
|
|
||||||
if (!getLocalDatabaseReference()) {
|
if (!getLocalDatabaseReference()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -77,22 +76,12 @@ public class ComputerManagerService extends Service {
|
|||||||
activePolls.incrementAndGet();
|
activePolls.incrementAndGet();
|
||||||
|
|
||||||
// Poll the machine
|
// Poll the machine
|
||||||
try {
|
if (!doPollMachine(details)) {
|
||||||
if (!pollComputer(details)) {
|
|
||||||
if (!newPc && offlineCount < OFFLINE_POLL_TRIES) {
|
|
||||||
// Return without calling the listener
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
details.state = ComputerDetails.State.OFFLINE;
|
details.state = ComputerDetails.State.OFFLINE;
|
||||||
details.reachability = ComputerDetails.Reachability.OFFLINE;
|
details.reachability = ComputerDetails.Reachability.OFFLINE;
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
|
||||||
releaseLocalDatabaseReference();
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
activePolls.decrementAndGet();
|
activePolls.decrementAndGet();
|
||||||
}
|
|
||||||
|
|
||||||
// If it's online, update our persistent state
|
// If it's online, update our persistent state
|
||||||
if (details.state == ComputerDetails.State.ONLINE) {
|
if (details.state == ComputerDetails.State.ONLINE) {
|
||||||
@@ -102,7 +91,7 @@ public class ComputerManagerService extends Service {
|
|||||||
if (dbManager.getComputerByName(details.name) == null) {
|
if (dbManager.getComputerByName(details.name) == null) {
|
||||||
// It's gone
|
// It's gone
|
||||||
releaseLocalDatabaseReference();
|
releaseLocalDatabaseReference();
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,21 +111,13 @@ public class ComputerManagerService extends Service {
|
|||||||
Thread t = new Thread() {
|
Thread t = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
||||||
int offlineCount = 0;
|
|
||||||
while (!isInterrupted() && pollingActive) {
|
while (!isInterrupted() && pollingActive) {
|
||||||
try {
|
|
||||||
// Check if this poll has modified the details
|
// Check if this poll has modified the details
|
||||||
if (!runPoll(details, false, offlineCount)) {
|
runPoll(details);
|
||||||
LimeLog.warning(details.name + " is offline (try " + offlineCount + ")");
|
|
||||||
offlineCount++;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
offlineCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait until the next polling interval
|
// Wait until the next polling interval
|
||||||
Thread.sleep(SERVERINFO_POLLING_PERIOD_MS / ((offlineCount > 0) ? 2 : 1));
|
try {
|
||||||
|
Thread.sleep(POLLING_PERIOD_MS);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -196,6 +177,10 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -254,7 +239,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
|
||||||
addComputerBlocking(computer.getAddress());
|
addComputer(computer.getAddress());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -270,11 +255,28 @@ public class ComputerManagerService extends Service {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addComputer(InetAddress addr) {
|
||||||
|
// Setup a placeholder
|
||||||
|
ComputerDetails fakeDetails = new ComputerDetails();
|
||||||
|
fakeDetails.localIp = addr;
|
||||||
|
fakeDetails.remoteIp = addr;
|
||||||
|
fakeDetails.name = "";
|
||||||
|
|
||||||
|
addTuple(fakeDetails);
|
||||||
|
}
|
||||||
|
|
||||||
private void addTuple(ComputerDetails details) {
|
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.uuid.equals(details.uuid)) {
|
if (tuple.computer == details ||
|
||||||
|
// If there's no name on one of these computers, compare with the local IP
|
||||||
|
((details.name.isEmpty() || tuple.computer.name.isEmpty()) &&
|
||||||
|
tuple.computer.localIp.equals(details.localIp)) ||
|
||||||
|
// If there is a name on both computers, compare with name
|
||||||
|
((!details.name.isEmpty() && !tuple.computer.name.isEmpty()) &&
|
||||||
|
tuple.computer.name.equals(details.name))) {
|
||||||
|
|
||||||
// Update details anyway in case this machine has been re-added by IP
|
// Update details anyway in case this machine has been re-added by IP
|
||||||
// after not being reachable by our existing information
|
// after not being reachable by our existing information
|
||||||
tuple.computer.localIp = details.localIp;
|
tuple.computer.localIp = details.localIp;
|
||||||
@@ -305,18 +307,13 @@ 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
|
||||||
try {
|
runPoll(fakeDetails);
|
||||||
runPoll(fakeDetails, true, 0);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the machine is reachable, it was successful
|
// If the machine is reachable, it was successful
|
||||||
if (fakeDetails.state == ComputerDetails.State.ONLINE) {
|
if (fakeDetails.state == ComputerDetails.State.ONLINE) {
|
||||||
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
|
|
||||||
|
|
||||||
// Start a polling thread for this machine
|
// Start a polling thread for this machine
|
||||||
addTuple(fakeDetails);
|
addTuple(fakeDetails);
|
||||||
return true;
|
return true;
|
||||||
@@ -387,91 +384,14 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just try to establish a TCP connection to speculatively detect a running
|
private boolean pollComputer(ComputerDetails details, boolean localFirst) {
|
||||||
// GFE server
|
|
||||||
private boolean fastPollIp(InetAddress addr) {
|
|
||||||
Socket s = new Socket();
|
|
||||||
try {
|
|
||||||
s.connect(new InetSocketAddress(addr, NvHTTP.PORT), FAST_POLL_TIMEOUT);
|
|
||||||
s.close();
|
|
||||||
return true;
|
|
||||||
} catch (IOException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startFastPollThread(final InetAddress addr, final boolean[] info) {
|
|
||||||
Thread t = new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
boolean pollRes = fastPollIp(addr);
|
|
||||||
|
|
||||||
synchronized (info) {
|
|
||||||
info[0] = true; // Done
|
|
||||||
info[1] = pollRes; // Polling result
|
|
||||||
|
|
||||||
info.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
t.setName("Fast Poll - "+addr.getHostAddress());
|
|
||||||
t.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private ComputerDetails.Reachability fastPollPc(final InetAddress local, final InetAddress remote) throws InterruptedException {
|
|
||||||
final boolean[] remoteInfo = new boolean[2];
|
|
||||||
final boolean[] localInfo = new boolean[2];
|
|
||||||
|
|
||||||
startFastPollThread(local, localInfo);
|
|
||||||
startFastPollThread(remote, remoteInfo);
|
|
||||||
|
|
||||||
// Check local first
|
|
||||||
synchronized (localInfo) {
|
|
||||||
while (!localInfo[0]) {
|
|
||||||
localInfo.wait(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localInfo[1]) {
|
|
||||||
return ComputerDetails.Reachability.LOCAL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now remote
|
|
||||||
synchronized (remoteInfo) {
|
|
||||||
while (!remoteInfo[0]) {
|
|
||||||
remoteInfo.wait(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remoteInfo[1]) {
|
|
||||||
return ComputerDetails.Reachability.REMOTE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ComputerDetails.Reachability.OFFLINE;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
|
|
||||||
ComputerDetails polledDetails;
|
ComputerDetails polledDetails;
|
||||||
ComputerDetails.Reachability reachability;
|
|
||||||
|
|
||||||
// If the local address is routable across the Internet,
|
// If the local address is routable across the Internet,
|
||||||
// always consider this PC remote to be conservative
|
// always consider this PC remote to be conservative
|
||||||
if (details.localIp.equals(details.remoteIp)) {
|
if (details.localIp.equals(details.remoteIp)) {
|
||||||
reachability = ComputerDetails.Reachability.REMOTE;
|
localFirst = false;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
// Do a TCP-level connection to the HTTP server to see if it's listening
|
|
||||||
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localIp+", "+details.remoteIp+")");
|
|
||||||
reachability = fastPollPc(details.localIp, details.remoteIp);
|
|
||||||
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no connection could be established to either IP address, there's nothing we can do
|
|
||||||
if (reachability == ComputerDetails.Reachability.OFFLINE) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
|
|
||||||
|
|
||||||
if (localFirst) {
|
if (localFirst) {
|
||||||
polledDetails = tryPollIp(details, details.localIp);
|
polledDetails = tryPollIp(details, details.localIp);
|
||||||
@@ -505,19 +425,24 @@ public class ComputerManagerService extends Service {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the old MAC address
|
|
||||||
String savedMacAddress = details.macAddress;
|
|
||||||
|
|
||||||
// If we got here, it's reachable
|
// If we got here, it's reachable
|
||||||
details.update(polledDetails);
|
details.update(polledDetails);
|
||||||
|
return true;
|
||||||
// If the new MAC address is empty, restore the old one (workaround for GFE bug)
|
|
||||||
if (details.macAddress.equals("00:00:00:00:00:00") && savedMacAddress != null) {
|
|
||||||
LimeLog.info("MAC address was empty; using existing value: "+savedMacAddress);
|
|
||||||
details.macAddress = savedMacAddress;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
private boolean doPollMachine(ComputerDetails details) {
|
||||||
|
if (details.reachability == ComputerDetails.Reachability.UNKNOWN ||
|
||||||
|
details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
// Always try local first to avoid potential UDP issues when
|
||||||
|
// attempting to stream via the router's external IP address
|
||||||
|
// behind its NAT
|
||||||
|
return pollComputer(details, true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If we're already reached a machine via a particular IP address,
|
||||||
|
// always try that one first
|
||||||
|
return pollComputer(details, details.reachability == ComputerDetails.Reachability.LOCAL);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -566,8 +491,8 @@ public class ComputerManagerService extends Service {
|
|||||||
|
|
||||||
public class ApplistPoller {
|
public class ApplistPoller {
|
||||||
private Thread thread;
|
private Thread thread;
|
||||||
private final ComputerDetails computer;
|
private ComputerDetails computer;
|
||||||
private final Object pollEvent = new Object();
|
private Object pollEvent = new Object();
|
||||||
|
|
||||||
public ApplistPoller(ComputerDetails computer) {
|
public ApplistPoller(ComputerDetails computer) {
|
||||||
this.computer = computer;
|
this.computer = computer;
|
||||||
@@ -582,7 +507,7 @@ public class ComputerManagerService extends Service {
|
|||||||
private boolean waitPollingDelay() {
|
private boolean waitPollingDelay() {
|
||||||
try {
|
try {
|
||||||
synchronized (pollEvent) {
|
synchronized (pollEvent) {
|
||||||
pollEvent.wait(APPLIST_POLLING_PERIOD_MS);
|
pollEvent.wait(POLLING_PERIOD_MS);
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
return false;
|
return false;
|
||||||
@@ -627,19 +552,9 @@ public class ComputerManagerService extends Service {
|
|||||||
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
|
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
|
||||||
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
|
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
|
||||||
// Open the cache file
|
// Open the cache file
|
||||||
OutputStream cacheOut = null;
|
FileOutputStream cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
|
||||||
try {
|
|
||||||
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
|
|
||||||
CacheHelper.writeStringToOutputStream(cacheOut, appList);
|
CacheHelper.writeStringToOutputStream(cacheOut, appList);
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
if (cacheOut != null) {
|
|
||||||
cacheOut.close();
|
cacheOut.close();
|
||||||
}
|
|
||||||
} catch (IOException e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the computer
|
// Update the computer
|
||||||
computer.rawAppList = appList;
|
computer.rawAppList = appList;
|
||||||
@@ -678,7 +593,7 @@ public class ComputerManagerService extends Service {
|
|||||||
|
|
||||||
class PollingTuple {
|
class PollingTuple {
|
||||||
public Thread thread;
|
public Thread thread;
|
||||||
public final ComputerDetails computer;
|
public 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 final DiscoveryBinder binder = new DiscoveryBinder();
|
private DiscoveryBinder binder = new DiscoveryBinder();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IBinder onBind(Intent intent) {
|
public IBinder onBind(Intent intent) {
|
||||||
|
|||||||
@@ -1,69 +1,118 @@
|
|||||||
package com.limelight.grid;
|
package com.limelight.grid;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.view.View;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.koushikdutta.async.future.FutureCallback;
|
||||||
|
import com.koushikdutta.ion.ImageViewBitmapInfo;
|
||||||
|
import com.koushikdutta.ion.Ion;
|
||||||
import com.limelight.AppView;
|
import com.limelight.AppView;
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.grid.assets.CachedAppAssetLoader;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.grid.assets.DiskAssetLoader;
|
|
||||||
import com.limelight.grid.assets.MemoryAssetLoader;
|
|
||||||
import com.limelight.grid.assets.NetworkAssetLoader;
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||||
|
import com.limelight.utils.CacheHelper;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.Socket;
|
||||||
import java.security.KeyManagementException;
|
import java.security.KeyManagementException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.SecureRandom;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
import javax.net.ssl.HostnameVerifier;
|
||||||
|
import javax.net.ssl.KeyManager;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLSession;
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
import javax.net.ssl.X509KeyManager;
|
||||||
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||||
private static final int ART_WIDTH_PX = 300;
|
|
||||||
private static final int SMALL_WIDTH_DP = 100;
|
|
||||||
private static final int LARGE_WIDTH_DP = 150;
|
|
||||||
|
|
||||||
private final CachedAppAssetLoader loader;
|
private ComputerDetails computer;
|
||||||
|
private String uniqueId;
|
||||||
|
private LimelightCryptoProvider cryptoProvider;
|
||||||
|
private SSLContext sslContext;
|
||||||
|
private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>();
|
||||||
|
|
||||||
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws KeyManagementException, NoSuchAlgorithmException {
|
public AppGridAdapter(Context context, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException {
|
||||||
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : 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);
|
||||||
|
|
||||||
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
|
this.computer = computer;
|
||||||
int dp;
|
this.uniqueId = uniqueId;
|
||||||
|
|
||||||
if (small) {
|
cryptoProvider = PlatformBinding.getCryptoProvider(context);
|
||||||
dp = SMALL_WIDTH_DP;
|
|
||||||
}
|
sslContext = SSLContext.getInstance("SSL");
|
||||||
else {
|
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom());
|
||||||
dp = LARGE_WIDTH_DP;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160));
|
TrustManager[] trustAllCerts = new TrustManager[] {
|
||||||
if (scalingDivisor < 1.0) {
|
new X509TrustManager() {
|
||||||
// We don't want to make them bigger before draw-time
|
public X509Certificate[] getAcceptedIssuers() {
|
||||||
scalingDivisor = 1.0;
|
return new X509Certificate[0];
|
||||||
}
|
}
|
||||||
LimeLog.info("Art scaling divisor: " + scalingDivisor);
|
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
|
||||||
|
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
|
||||||
|
}};
|
||||||
|
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
KeyManager[] ourKeyman = new KeyManager[] {
|
||||||
options.inSampleSize = (int) scalingDivisor;
|
new X509KeyManager() {
|
||||||
|
public String chooseClientAlias(String[] keyTypes,
|
||||||
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
Principal[] issuers, Socket socket) {
|
||||||
new NetworkAssetLoader(context, uniqueId),
|
return "Limelight-RSA";
|
||||||
new MemoryAssetLoader(),
|
|
||||||
new DiskAssetLoader(context.getCacheDir()),
|
|
||||||
BitmapFactory.decodeResource(activity.getResources(),
|
|
||||||
R.drawable.image_loading, options));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cancelQueuedOperations() {
|
public String chooseServerAlias(String keyType, Principal[] issuers,
|
||||||
loader.cancelForegroundLoads();
|
Socket socket) {
|
||||||
loader.cancelBackgroundLoads();
|
return null;
|
||||||
loader.freeCacheMemory();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public X509Certificate[] getCertificateChain(String alias) {
|
||||||
|
return new X509Certificate[] {cryptoProvider.getClientCertificate()};
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getClientAliases(String keyType, Principal[] issuers) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PrivateKey getPrivateKey(String alias) {
|
||||||
|
return cryptoProvider.getClientPrivateKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getServerAliases(String keyType, Principal[] issuers) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ignore differences between given hostname and certificate hostname
|
||||||
|
HostnameVerifier hv = new HostnameVerifier() {
|
||||||
|
public boolean verify(String hostname, SSLSession session) { return true; }
|
||||||
|
};
|
||||||
|
|
||||||
private void sortList() {
|
private void sortList() {
|
||||||
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
||||||
@Override
|
@Override
|
||||||
@@ -73,22 +122,62 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addApp(AppView.AppObject app) {
|
private InetAddress getCurrentAddress() {
|
||||||
// Queue a request to fetch this bitmap into cache
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
loader.queueCacheLoad(app.app);
|
return computer.localIp;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return computer.remoteIp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add the app to our sorted list
|
public void addApp(AppView.AppObject app) {
|
||||||
itemList.add(app);
|
itemList.add(app);
|
||||||
sortList();
|
sortList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeApp(AppView.AppObject app) {
|
public void abortPendingRequests() {
|
||||||
itemList.remove(app);
|
HashMap<ImageView, Future> tempMap;
|
||||||
|
|
||||||
|
synchronized (pendingRequests) {
|
||||||
|
// Copy the pending requests under a lock
|
||||||
|
tempMap = new HashMap<ImageView, Future>(pendingRequests);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean populateImageView(ImageView imgView, AppView.AppObject obj) {
|
for (Future f : tempMap.values()) {
|
||||||
// Let the cached asset loader handle it
|
if (!f.isCancelled() && !f.isDone()) {
|
||||||
loader.populateImageView(obj.app, imgView);
|
f.cancel(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (pendingRequests) {
|
||||||
|
// Remove cancelled requests
|
||||||
|
for (ImageView v : tempMap.keySet()) {
|
||||||
|
pendingRequests.remove(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Handle pruning of bitmap cache
|
||||||
|
private void populateBitmapCache(UUID uuid, int appId, Bitmap bitmap) {
|
||||||
|
try {
|
||||||
|
// PNG ignores quality setting
|
||||||
|
FileOutputStream out = CacheHelper.openCacheFileForOutput(context.getCacheDir(), "boxart", uuid.toString(), appId+".png");
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
|
||||||
|
out.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) {
|
||||||
|
// Hide the image view while we're loading the image from disk cache
|
||||||
|
imgView.setVisibility(View.INVISIBLE);
|
||||||
|
|
||||||
|
// Check the on-disk cache
|
||||||
|
new ImageCacheRequest(imgView, obj.app.getAppId()).execute();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,4 +201,84 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
// No overlay
|
// No overlay
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ImageCacheRequest extends AsyncTask<Void, Void, Bitmap> {
|
||||||
|
private ImageView view;
|
||||||
|
private int appId;
|
||||||
|
|
||||||
|
public ImageCacheRequest(ImageView view, int appId) {
|
||||||
|
this.view = view;
|
||||||
|
this.appId = appId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Bitmap doInBackground(Void... v) {
|
||||||
|
InputStream in = null;
|
||||||
|
try {
|
||||||
|
in = CacheHelper.openCacheFileForInput(context.getCacheDir(), "boxart", computer.uuid.toString(), appId + ".png");
|
||||||
|
return BitmapFactory.decodeStream(in);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
if (in != null) {
|
||||||
|
try {
|
||||||
|
in.close();
|
||||||
|
} catch (IOException e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Bitmap result) {
|
||||||
|
if (result != null) {
|
||||||
|
// Disk cache was read successfully
|
||||||
|
LimeLog.info("Image disk cache hit for ("+computer.uuid+", "+appId+")");
|
||||||
|
view.setImageBitmap(result);
|
||||||
|
view.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.info("Image disk cache miss for ("+computer.uuid+", "+appId+")");
|
||||||
|
LimeLog.info("Requesting: "+"https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
|
||||||
|
appId + "&AssetType=2&AssetIdx=0");
|
||||||
|
|
||||||
|
// Load the placeholder image
|
||||||
|
view.setImageResource(defaultImageRes);
|
||||||
|
view.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
// Set SSL contexts correctly to allow us to authenticate
|
||||||
|
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts);
|
||||||
|
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext);
|
||||||
|
|
||||||
|
// Kick off the deferred image load
|
||||||
|
synchronized (pendingRequests) {
|
||||||
|
Future<Bitmap> f = Ion.with(context)
|
||||||
|
.load("https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
|
||||||
|
appId + "&AssetType=2&AssetIdx=0")
|
||||||
|
.asBitmap()
|
||||||
|
.setCallback(new FutureCallback<Bitmap>() {
|
||||||
|
@Override
|
||||||
|
public void onCompleted(Exception e, Bitmap result) {
|
||||||
|
synchronized (pendingRequests) {
|
||||||
|
pendingRequests.remove(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
// Make the view visible now
|
||||||
|
view.setImageBitmap(result);
|
||||||
|
view.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
// Populate the disk cache if we got an image back
|
||||||
|
populateBitmapCache(computer.uuid, appId, result);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Leave the loading icon as is (probably should change this eventually...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pendingRequests.put(view, f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -13,11 +13,11 @@ import com.limelight.R;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
||||||
protected final Context context;
|
protected Context context;
|
||||||
protected final int defaultImageRes;
|
protected int defaultImageRes;
|
||||||
protected final int layoutId;
|
protected int layoutId;
|
||||||
protected final ArrayList<T> itemList = new ArrayList<T>();
|
protected ArrayList<T> itemList = new ArrayList<T>();
|
||||||
protected final LayoutInflater inflater;
|
protected LayoutInflater inflater;
|
||||||
|
|
||||||
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
|
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
|||||||
@@ -1,327 +0,0 @@
|
|||||||
package com.limelight.grid.assets;
|
|
||||||
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
import com.limelight.nvstream.http.NvApp;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
|
||||||
import java.util.concurrent.ThreadPoolExecutor;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class CachedAppAssetLoader {
|
|
||||||
private static final int MAX_CONCURRENT_DISK_LOADS = 3;
|
|
||||||
private static final int MAX_CONCURRENT_NETWORK_LOADS = 3;
|
|
||||||
private static final int MAX_CONCURRENT_CACHE_LOADS = 1;
|
|
||||||
|
|
||||||
private static final int MAX_PENDING_CACHE_LOADS = 100;
|
|
||||||
private static final int MAX_PENDING_NETWORK_LOADS = 40;
|
|
||||||
private static final int MAX_PENDING_DISK_LOADS = 40;
|
|
||||||
|
|
||||||
private final ThreadPoolExecutor cacheExecutor = new ThreadPoolExecutor(
|
|
||||||
MAX_CONCURRENT_CACHE_LOADS, MAX_CONCURRENT_CACHE_LOADS,
|
|
||||||
Long.MAX_VALUE, TimeUnit.DAYS,
|
|
||||||
new LinkedBlockingQueue<Runnable>(MAX_PENDING_CACHE_LOADS),
|
|
||||||
new ThreadPoolExecutor.DiscardOldestPolicy());
|
|
||||||
|
|
||||||
private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(
|
|
||||||
MAX_CONCURRENT_DISK_LOADS, MAX_CONCURRENT_DISK_LOADS,
|
|
||||||
Long.MAX_VALUE, TimeUnit.DAYS,
|
|
||||||
new LinkedBlockingQueue<Runnable>(MAX_PENDING_DISK_LOADS),
|
|
||||||
new ThreadPoolExecutor.DiscardOldestPolicy());
|
|
||||||
|
|
||||||
private final ThreadPoolExecutor networkExecutor = new ThreadPoolExecutor(
|
|
||||||
MAX_CONCURRENT_NETWORK_LOADS, MAX_CONCURRENT_NETWORK_LOADS,
|
|
||||||
Long.MAX_VALUE, TimeUnit.DAYS,
|
|
||||||
new LinkedBlockingQueue<Runnable>(MAX_PENDING_NETWORK_LOADS),
|
|
||||||
new ThreadPoolExecutor.DiscardOldestPolicy());
|
|
||||||
|
|
||||||
private final ComputerDetails computer;
|
|
||||||
private final double scalingDivider;
|
|
||||||
private final NetworkAssetLoader networkLoader;
|
|
||||||
private final MemoryAssetLoader memoryLoader;
|
|
||||||
private final DiskAssetLoader diskLoader;
|
|
||||||
private final Bitmap placeholderBitmap;
|
|
||||||
|
|
||||||
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
|
||||||
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
|
||||||
DiskAssetLoader diskLoader, Bitmap placeholderBitmap) {
|
|
||||||
this.computer = computer;
|
|
||||||
this.scalingDivider = scalingDivider;
|
|
||||||
this.networkLoader = networkLoader;
|
|
||||||
this.memoryLoader = memoryLoader;
|
|
||||||
this.diskLoader = diskLoader;
|
|
||||||
this.placeholderBitmap = placeholderBitmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cancelBackgroundLoads() {
|
|
||||||
Runnable r;
|
|
||||||
while ((r = cacheExecutor.getQueue().poll()) != null) {
|
|
||||||
cacheExecutor.remove(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cancelForegroundLoads() {
|
|
||||||
Runnable r;
|
|
||||||
|
|
||||||
while ((r = foregroundExecutor.getQueue().poll()) != null) {
|
|
||||||
foregroundExecutor.remove(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
while ((r = networkExecutor.getQueue().poll()) != null) {
|
|
||||||
networkExecutor.remove(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void freeCacheMemory() {
|
|
||||||
memoryLoader.clearCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
|
|
||||||
// Try 3 times
|
|
||||||
for (int i = 0; i < 3; i++) {
|
|
||||||
// Check again whether we've been cancelled or the image view is gone
|
|
||||||
if (task != null && (task.isCancelled() || task.imageViewRef.get() == null)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
InputStream in = networkLoader.getBitmapStream(tuple);
|
|
||||||
if (in != null) {
|
|
||||||
// Write the stream straight to disk
|
|
||||||
diskLoader.populateCacheWithStream(tuple, in);
|
|
||||||
|
|
||||||
// If there's a task associated with this load, we should return the bitmap
|
|
||||||
if (task != null) {
|
|
||||||
return diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Otherwise it's a background load and we return nothing
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait 1 second with a bit of fuzz
|
|
||||||
try {
|
|
||||||
Thread.sleep((int) (1000 + (Math.random() * 500)));
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
|
|
||||||
private final WeakReference<ImageView> imageViewRef;
|
|
||||||
private final boolean diskOnly;
|
|
||||||
|
|
||||||
private LoaderTuple tuple;
|
|
||||||
|
|
||||||
public LoaderTask(ImageView imageView, boolean diskOnly) {
|
|
||||||
this.imageViewRef = new WeakReference<ImageView>(imageView);
|
|
||||||
this.diskOnly = diskOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Bitmap doInBackground(LoaderTuple... params) {
|
|
||||||
tuple = params[0];
|
|
||||||
|
|
||||||
// Check whether it has been cancelled or the image view is gone
|
|
||||||
if (isCancelled() || imageViewRef.get() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
|
||||||
if (bmp == null) {
|
|
||||||
if (!diskOnly) {
|
|
||||||
// Try to load the asset from the network
|
|
||||||
bmp = doNetworkAssetLoad(tuple, this);
|
|
||||||
} else {
|
|
||||||
// Report progress to display the placeholder and spin
|
|
||||||
// off the network-capable task
|
|
||||||
publishProgress();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache the bitmap
|
|
||||||
if (bmp != null) {
|
|
||||||
memoryLoader.populateCache(tuple, bmp);
|
|
||||||
}
|
|
||||||
|
|
||||||
return bmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onProgressUpdate(Void... nothing) {
|
|
||||||
// Do nothing if cancelled
|
|
||||||
if (isCancelled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the current loader task for this view isn't us, do nothing
|
|
||||||
final ImageView imageView = imageViewRef.get();
|
|
||||||
if (getLoaderTask(imageView) == this) {
|
|
||||||
// Set off another loader task on the network executor
|
|
||||||
LoaderTask task = new LoaderTask(imageView, false);
|
|
||||||
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
|
|
||||||
imageView.setAlpha(1.0f);
|
|
||||||
imageView.setImageDrawable(asyncDrawable);
|
|
||||||
task.executeOnExecutor(networkExecutor, tuple);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Bitmap bitmap) {
|
|
||||||
// Do nothing if cancelled
|
|
||||||
if (isCancelled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final ImageView imageView = imageViewRef.get();
|
|
||||||
if (getLoaderTask(imageView) == this) {
|
|
||||||
// Set the bitmap
|
|
||||||
if (bitmap != null) {
|
|
||||||
imageView.setImageBitmap(bitmap);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show the view
|
|
||||||
imageView.setAlpha(1.0f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class AsyncDrawable extends BitmapDrawable {
|
|
||||||
private final WeakReference<LoaderTask> loaderTaskReference;
|
|
||||||
|
|
||||||
public AsyncDrawable(Resources res, Bitmap bitmap,
|
|
||||||
LoaderTask loaderTask) {
|
|
||||||
super(res, bitmap);
|
|
||||||
loaderTaskReference = new WeakReference<LoaderTask>(loaderTask);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LoaderTask getLoaderTask() {
|
|
||||||
return loaderTaskReference.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LoaderTask getLoaderTask(ImageView imageView) {
|
|
||||||
if (imageView == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Drawable drawable = imageView.getDrawable();
|
|
||||||
|
|
||||||
// If our drawable is in play, get the loader task
|
|
||||||
if (drawable instanceof AsyncDrawable) {
|
|
||||||
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
|
|
||||||
return asyncDrawable.getLoaderTask();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean cancelPendingLoad(LoaderTuple tuple, ImageView imageView) {
|
|
||||||
final LoaderTask loaderTask = getLoaderTask(imageView);
|
|
||||||
|
|
||||||
// Check if any task was pending for this image view
|
|
||||||
if (loaderTask != null && !loaderTask.isCancelled()) {
|
|
||||||
final LoaderTuple taskTuple = loaderTask.tuple;
|
|
||||||
|
|
||||||
// Cancel the task if it's not already loading the same data
|
|
||||||
if (taskTuple == null || !taskTuple.equals(tuple)) {
|
|
||||||
loaderTask.cancel(true);
|
|
||||||
} else {
|
|
||||||
// It's already loading what we want
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow the load to proceed
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void queueCacheLoad(NvApp app) {
|
|
||||||
final LoaderTuple tuple = new LoaderTuple(computer, app);
|
|
||||||
|
|
||||||
if (memoryLoader.loadBitmapFromCache(tuple) != null) {
|
|
||||||
// It's in memory which means it must also be on disk
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Queue a fetch in the cache executor
|
|
||||||
cacheExecutor.execute(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Check if the image is cached on disk
|
|
||||||
if (diskLoader.checkCacheExists(tuple)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to load the asset from the network and cache result on disk
|
|
||||||
doNetworkAssetLoad(tuple, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void populateImageView(NvApp app, ImageView view) {
|
|
||||||
LoaderTuple tuple = new LoaderTuple(computer, app);
|
|
||||||
|
|
||||||
// If there's already a task in progress for this view,
|
|
||||||
// cancel it. If the task is already loading the same image,
|
|
||||||
// we return and let that load finish.
|
|
||||||
if (!cancelPendingLoad(tuple, view)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// First, try the memory cache in the current context
|
|
||||||
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
|
|
||||||
if (bmp != null) {
|
|
||||||
// Show the bitmap immediately
|
|
||||||
view.setAlpha(1.0f);
|
|
||||||
view.setImageBitmap(bmp);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's not in memory, create an async task to load it. This task will be attached
|
|
||||||
// via AsyncDrawable to this view.
|
|
||||||
final LoaderTask task = new LoaderTask(view, true);
|
|
||||||
final AsyncDrawable asyncDrawable = new AsyncDrawable(view.getResources(), placeholderBitmap, task);
|
|
||||||
view.setAlpha(0.0f);
|
|
||||||
view.setImageDrawable(asyncDrawable);
|
|
||||||
|
|
||||||
// Run the task on our foreground executor
|
|
||||||
task.executeOnExecutor(foregroundExecutor, tuple);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LoaderTuple {
|
|
||||||
public final ComputerDetails computer;
|
|
||||||
public final NvApp app;
|
|
||||||
|
|
||||||
public LoaderTuple(ComputerDetails computer, NvApp app) {
|
|
||||||
this.computer = computer;
|
|
||||||
this.app = app;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (!(o instanceof LoaderTuple)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
LoaderTuple other = (LoaderTuple) o;
|
|
||||||
return computer.uuid.equals(other.computer.uuid) && app.getAppId() == other.app.getAppId();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "("+computer.uuid+", "+app.getAppId()+")";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package com.limelight.grid.assets;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.utils.CacheHelper;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
public class DiskAssetLoader {
|
|
||||||
private final File cacheDir;
|
|
||||||
|
|
||||||
public DiskAssetLoader(File cacheDir) {
|
|
||||||
this.cacheDir = cacheDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
|
|
||||||
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
|
||||||
InputStream in = null;
|
|
||||||
Bitmap bmp = null;
|
|
||||||
try {
|
|
||||||
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
||||||
options.inSampleSize = sampleSize;
|
|
||||||
bmp = BitmapFactory.decodeStream(in, null, options);
|
|
||||||
} catch (FileNotFoundException ignored) {
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} finally {
|
|
||||||
if (in != null) {
|
|
||||||
try {
|
|
||||||
in.close();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bmp != null) {
|
|
||||||
LimeLog.info("Disk cache hit for tuple: "+tuple);
|
|
||||||
}
|
|
||||||
|
|
||||||
return bmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
|
|
||||||
OutputStream out = null;
|
|
||||||
try {
|
|
||||||
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
|
||||||
CacheHelper.writeInputStreamToOutputStream(input, out);
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} finally {
|
|
||||||
if (out != null) {
|
|
||||||
try {
|
|
||||||
out.close();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.limelight.grid.assets;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.util.LruCache;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
|
|
||||||
public class MemoryAssetLoader {
|
|
||||||
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
|
|
||||||
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 16) {
|
|
||||||
@Override
|
|
||||||
protected int sizeOf(String key, Bitmap bitmap) {
|
|
||||||
// Sizeof returns kilobytes
|
|
||||||
return bitmap.getByteCount() / 1024;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
|
|
||||||
return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
|
||||||
Bitmap bmp = memoryCache.get(constructKey(tuple));
|
|
||||||
if (bmp != null) {
|
|
||||||
LimeLog.info("Memory cache hit for tuple: "+tuple);
|
|
||||||
}
|
|
||||||
return bmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
|
|
||||||
memoryCache.put(constructKey(tuple), bitmap);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clearCache() {
|
|
||||||
memoryCache.evictAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package com.limelight.grid.assets;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.binding.PlatformBinding;
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
|
|
||||||
public class NetworkAssetLoader {
|
|
||||||
private final Context context;
|
|
||||||
private final String uniqueId;
|
|
||||||
|
|
||||||
public NetworkAssetLoader(Context context, String uniqueId) {
|
|
||||||
this.context = context;
|
|
||||||
this.uniqueId = uniqueId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
|
||||||
NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
|
|
||||||
|
|
||||||
InputStream in = null;
|
|
||||||
try {
|
|
||||||
in = http.getBoxArt(tuple.app);
|
|
||||||
} catch (IOException e) {}
|
|
||||||
|
|
||||||
if (in != null) {
|
|
||||||
LimeLog.info("Network asset load complete: " + tuple);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LimeLog.info("Network asset load failed: " + tuple);
|
|
||||||
}
|
|
||||||
|
|
||||||
return in;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static InetAddress getCurrentAddress(ComputerDetails computer) {
|
|
||||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
|
||||||
return computer.localIp;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return computer.remoteIp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,6 @@ import com.limelight.utils.UiHelper;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
@@ -22,16 +21,15 @@ 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 final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
|
private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
|
||||||
private Thread addThread;
|
private Thread addThread;
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
private 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();
|
||||||
@@ -163,12 +161,6 @@ public class AddComputerManually extends Activity {
|
|||||||
|
|
||||||
computersToAdd.add(hostText.getText().toString().trim());
|
computersToAdd.add(hostText.getText().toString().trim());
|
||||||
}
|
}
|
||||||
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
|
|
||||||
// This is how the Fire TV dismisses the keyboard
|
|
||||||
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
||||||
imm.hideSoftInputFromWindow(hostText.getWindowToken(), 0);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ 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 {
|
||||||
static final String RES_FPS_PREF_STRING = "list_resolution_fps";
|
static final String RESX_PREF_STRING = "resolution_x";
|
||||||
|
static final String RESY_PREF_STRING = "resolution_y";
|
||||||
|
private static final String FPS_PREF_STRING = "fps";
|
||||||
private static final String DECODER_PREF_STRING = "list_decoders";
|
private static final String DECODER_PREF_STRING = "list_decoders";
|
||||||
static final String BITRATE_PREF_STRING = "seekbar_bitrate";
|
static final String BITRATE_PREF_STRING = "seekbar_bitrate";
|
||||||
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
|
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
|
||||||
@@ -17,16 +18,17 @@ public class PreferenceConfiguration {
|
|||||||
private static final String LANGUAGE_PREF_STRING = "list_languages";
|
private static final String LANGUAGE_PREF_STRING = "list_languages";
|
||||||
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
|
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
|
||||||
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
|
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
|
||||||
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
|
|
||||||
|
|
||||||
private static final int BITRATE_DEFAULT_720_30 = 5;
|
private static final int BITRATE_DEFAULT_720_30 = 5;
|
||||||
private static final int BITRATE_DEFAULT_720_60 = 10;
|
private static final int BITRATE_DEFAULT_720_60 = 10;
|
||||||
private static final int BITRATE_DEFAULT_1080_30 = 10;
|
private static final int BITRATE_DEFAULT_1080_30 = 10;
|
||||||
private static final int BITRATE_DEFAULT_1080_60 = 20;
|
private static final int BITRATE_DEFAULT_1080_60 = 20;
|
||||||
|
private static final int BITRATE_DEFAULT_MAX = 35;
|
||||||
|
|
||||||
private static final String DEFAULT_RES_FPS = "720p60";
|
private static final int DEFAULT_RESX = 1280;
|
||||||
|
private static final int DEFAULT_RESY = 720;
|
||||||
|
private static final String DEFAULT_FPS = "60";
|
||||||
private static final String DEFAULT_DECODER = "auto";
|
private static final String DEFAULT_DECODER = "auto";
|
||||||
private static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
|
|
||||||
private static final boolean DEFAULT_STRETCH = false;
|
private static final boolean DEFAULT_STRETCH = false;
|
||||||
private static final boolean DEFAULT_SOPS = true;
|
private static final boolean DEFAULT_SOPS = true;
|
||||||
private static final boolean DEFAULT_DISABLE_TOASTS = false;
|
private static final boolean DEFAULT_DISABLE_TOASTS = false;
|
||||||
@@ -34,7 +36,6 @@ public class PreferenceConfiguration {
|
|||||||
private static final int DEFAULT_DEADZONE = 15;
|
private static final int DEFAULT_DEADZONE = 15;
|
||||||
public static final String DEFAULT_LANGUAGE = "default";
|
public static final String DEFAULT_LANGUAGE = "default";
|
||||||
private static final boolean DEFAULT_LIST_MODE = false;
|
private static final boolean DEFAULT_LIST_MODE = false;
|
||||||
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
|
|
||||||
|
|
||||||
public static final int FORCE_HARDWARE_DECODER = -1;
|
public static final int FORCE_HARDWARE_DECODER = -1;
|
||||||
public static final int AUTOSELECT_DECODER = 0;
|
public static final int AUTOSELECT_DECODER = 0;
|
||||||
@@ -46,57 +47,42 @@ public class PreferenceConfiguration {
|
|||||||
public int deadzonePercentage;
|
public int deadzonePercentage;
|
||||||
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
|
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
|
||||||
public String language;
|
public String language;
|
||||||
public boolean listMode, smallIconMode, multiController;
|
public boolean listMode, smallIconMode;
|
||||||
|
|
||||||
public static int getDefaultBitrate(String resFpsString) {
|
|
||||||
if (resFpsString.equals("720p30")) {
|
|
||||||
return BITRATE_DEFAULT_720_30;
|
|
||||||
}
|
|
||||||
else if (resFpsString.equals("720p60")) {
|
|
||||||
return BITRATE_DEFAULT_720_60;
|
|
||||||
}
|
|
||||||
else if (resFpsString.equals("1080p30")) {
|
|
||||||
return BITRATE_DEFAULT_1080_30;
|
|
||||||
}
|
|
||||||
else if (resFpsString.equals("1080p60")) {
|
|
||||||
return BITRATE_DEFAULT_1080_60;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Should never get here
|
|
||||||
return DEFAULT_BITRATE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean getDefaultSmallMode(Context context) {
|
public static boolean getDefaultSmallMode(Context context) {
|
||||||
PackageManager manager = context.getPackageManager();
|
|
||||||
if (manager != null && manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
|
|
||||||
// TVs shouldn't use small mode by default
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use small mode on anything smaller than a 7" tablet
|
// Use small mode on anything smaller than a 7" tablet
|
||||||
return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
|
return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int getFpsInt(SharedPreferences prefs) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(prefs.getString(FPS_PREF_STRING, DEFAULT_FPS));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return Integer.parseInt(DEFAULT_FPS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static int getDefaultBitrate(Context context) {
|
public static int getDefaultBitrate(Context context) {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
|
||||||
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
|
long width = prefs.getInt(RESX_PREF_STRING, DEFAULT_RESX);
|
||||||
if (str.equals("720p30")) {
|
long height = prefs.getInt(RESY_PREF_STRING, DEFAULT_RESY);
|
||||||
|
|
||||||
|
long product = width * height * getFpsInt(prefs);
|
||||||
|
if (product <= 1280 * 720 * 30) {
|
||||||
return BITRATE_DEFAULT_720_30;
|
return BITRATE_DEFAULT_720_30;
|
||||||
}
|
}
|
||||||
else if (str.equals("720p60")) {
|
else if (product <= 1280 * 720 * 60) {
|
||||||
return BITRATE_DEFAULT_720_60;
|
return BITRATE_DEFAULT_720_60;
|
||||||
}
|
}
|
||||||
else if (str.equals("1080p30")) {
|
else if (product <= 1920 * 1080 * 30) {
|
||||||
return BITRATE_DEFAULT_1080_30;
|
return BITRATE_DEFAULT_1080_30;
|
||||||
}
|
}
|
||||||
else if (str.equals("1080p60")) {
|
else if (product <= 1920 * 1080 * 60) {
|
||||||
return BITRATE_DEFAULT_1080_60;
|
return BITRATE_DEFAULT_1080_60;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Should never get here
|
return BITRATE_DEFAULT_MAX;
|
||||||
return DEFAULT_BITRATE;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,33 +110,9 @@ public class PreferenceConfiguration {
|
|||||||
PreferenceConfiguration config = new PreferenceConfiguration();
|
PreferenceConfiguration config = new PreferenceConfiguration();
|
||||||
|
|
||||||
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, getDefaultBitrate(context));
|
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, getDefaultBitrate(context));
|
||||||
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
|
config.width = prefs.getInt(RESX_PREF_STRING, DEFAULT_RESX);
|
||||||
if (str.equals("720p30")) {
|
config.height = prefs.getInt(RESY_PREF_STRING, DEFAULT_RESY);
|
||||||
config.width = 1280;
|
config.fps = getFpsInt(prefs);
|
||||||
config.height = 720;
|
|
||||||
config.fps = 30;
|
|
||||||
}
|
|
||||||
else if (str.equals("720p60")) {
|
|
||||||
config.width = 1280;
|
|
||||||
config.height = 720;
|
|
||||||
config.fps = 60;
|
|
||||||
}
|
|
||||||
else if (str.equals("1080p30")) {
|
|
||||||
config.width = 1920;
|
|
||||||
config.height = 1080;
|
|
||||||
config.fps = 30;
|
|
||||||
}
|
|
||||||
else if (str.equals("1080p60")) {
|
|
||||||
config.width = 1920;
|
|
||||||
config.height = 1080;
|
|
||||||
config.fps = 60;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Should never get here
|
|
||||||
config.width = 1280;
|
|
||||||
config.height = 720;
|
|
||||||
config.fps = 60;
|
|
||||||
}
|
|
||||||
|
|
||||||
config.decoder = getDecoderValue(context);
|
config.decoder = getDecoderValue(context);
|
||||||
|
|
||||||
@@ -165,7 +127,6 @@ public class PreferenceConfiguration {
|
|||||||
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
|
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
|
||||||
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
|
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
|
||||||
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
|
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
|
||||||
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
|
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
package com.limelight.preferences;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.preference.DialogPreference;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.CompoundButton;
|
||||||
|
import android.widget.RadioButton;
|
||||||
|
import android.widget.SeekBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.limelight.R;
|
||||||
|
|
||||||
|
public class ResolutionPreference extends DialogPreference {
|
||||||
|
private RadioButton a4_3;
|
||||||
|
private RadioButton a16_9;
|
||||||
|
private RadioButton a16_10;
|
||||||
|
private SeekBar resolutionSeekBar;
|
||||||
|
private TextView resolutionText;
|
||||||
|
|
||||||
|
private int resolutionX;
|
||||||
|
private int resolutionY;
|
||||||
|
|
||||||
|
public ResolutionPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
|
||||||
|
setDialogLayoutResource(R.layout.dialog_resolution);
|
||||||
|
setPositiveButtonText(android.R.string.ok);
|
||||||
|
setNegativeButtonText(android.R.string.cancel);
|
||||||
|
|
||||||
|
setDialogIcon(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getPreferenceState() {
|
||||||
|
int checkboxState;
|
||||||
|
int sliderState;
|
||||||
|
|
||||||
|
if (a4_3.isChecked()) {
|
||||||
|
checkboxState = 1;
|
||||||
|
}
|
||||||
|
else if (a16_10.isChecked()) {
|
||||||
|
checkboxState = 2;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
checkboxState = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
sliderState = resolutionSeekBar.getProgress();
|
||||||
|
|
||||||
|
return checkboxState << 24 | sliderState;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPreferenceState(int state) {
|
||||||
|
int checkboxState;
|
||||||
|
int sliderState;
|
||||||
|
|
||||||
|
checkboxState = state >> 24;
|
||||||
|
sliderState = state & 0x00FFFFFF;
|
||||||
|
|
||||||
|
a4_3.setChecked(false);
|
||||||
|
a16_10.setChecked(false);
|
||||||
|
a16_9.setChecked(false);
|
||||||
|
|
||||||
|
if (checkboxState == 1) {
|
||||||
|
a4_3.setChecked(true);
|
||||||
|
}
|
||||||
|
else if (checkboxState == 2) {
|
||||||
|
a16_10.setChecked(true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
a16_9.setChecked(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolutionSeekBar.setProgress(sliderState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateResolution() {
|
||||||
|
double aspectFactor;
|
||||||
|
int step = resolutionSeekBar.getProgress() + 1;
|
||||||
|
|
||||||
|
if (a4_3.isChecked()) {
|
||||||
|
aspectFactor = 4.0/3.0;
|
||||||
|
|
||||||
|
switch (step) {
|
||||||
|
case 1:
|
||||||
|
// 640x480
|
||||||
|
resolutionY = 480;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
// 800x600
|
||||||
|
resolutionY = 600;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// 1024x768
|
||||||
|
resolutionY = 768;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
// 1600x1200
|
||||||
|
resolutionY = 1200;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (a16_9.isChecked()) {
|
||||||
|
aspectFactor = 16.0/9.0;
|
||||||
|
|
||||||
|
switch (step) {
|
||||||
|
case 1:
|
||||||
|
// 1280x720
|
||||||
|
resolutionY = 720;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
// 1920x1080
|
||||||
|
resolutionY = 1080;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// 2560x1440
|
||||||
|
resolutionY = 1440;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
// 3840x2160
|
||||||
|
resolutionY = 2160;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else /* if (a16_10.isChecked() */ {
|
||||||
|
aspectFactor = 16.0/10.0;
|
||||||
|
|
||||||
|
switch (step) {
|
||||||
|
case 1:
|
||||||
|
// 1280x800
|
||||||
|
resolutionY = 800;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
// 1920x1200
|
||||||
|
resolutionY = 1200;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// 2560x1600
|
||||||
|
resolutionY = 1600;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
// 3840x2400
|
||||||
|
resolutionY = 2400;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolutionX = (int)(resolutionY * aspectFactor);
|
||||||
|
|
||||||
|
resolutionText.setText(resolutionX+"x"+resolutionY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDialogClosed(boolean positiveResult) {
|
||||||
|
if (positiveResult) {
|
||||||
|
SharedPreferences.Editor e = getPreferenceManager().getSharedPreferences().edit();
|
||||||
|
e.putInt(PreferenceConfiguration.RESX_PREF_STRING, resolutionX);
|
||||||
|
e.putInt(PreferenceConfiguration.RESY_PREF_STRING, resolutionY);
|
||||||
|
persistInt(getPreferenceState());
|
||||||
|
e.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected View onCreateDialogView() {
|
||||||
|
View v = super.onCreateDialogView();
|
||||||
|
|
||||||
|
a4_3 = (RadioButton) v.findViewById(R.id.aspect_4_3);
|
||||||
|
a16_9 = (RadioButton) v.findViewById(R.id.aspect_16_9);
|
||||||
|
a16_10 = (RadioButton) v.findViewById(R.id.aspect_16_10);
|
||||||
|
resolutionText = (TextView) v.findViewById(R.id.resolutionText);
|
||||||
|
resolutionSeekBar = (SeekBar) v.findViewById(R.id.resolutionSeekbar);
|
||||||
|
|
||||||
|
CompoundButton.OnCheckedChangeListener ccListener = new CompoundButton.OnCheckedChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||||
|
updateResolution();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
a4_3.setOnCheckedChangeListener(ccListener);
|
||||||
|
a16_9.setOnCheckedChangeListener(ccListener);
|
||||||
|
a16_10.setOnCheckedChangeListener(ccListener);
|
||||||
|
|
||||||
|
resolutionSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||||
|
updateResolution();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check the correct checkbox
|
||||||
|
setPreferenceState(getPersistedInt(0));
|
||||||
|
|
||||||
|
// Set initial resolution value
|
||||||
|
updateResolution();
|
||||||
|
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,14 +20,10 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
|
|
||||||
private SeekBar seekBar;
|
private SeekBar seekBar;
|
||||||
private TextView valueText;
|
private TextView valueText;
|
||||||
private final Context context;
|
private Context context;
|
||||||
|
|
||||||
private final String dialogMessage;
|
private String dialogMessage, suffix;
|
||||||
private final String suffix;
|
private int defaultValue, maxValue, minValue, currentValue;
|
||||||
private final int defaultValue;
|
|
||||||
private final int maxValue;
|
|
||||||
private final int minValue;
|
|
||||||
private int currentValue;
|
|
||||||
|
|
||||||
public SeekBarPreference(Context context, AttributeSet attrs) {
|
public SeekBarPreference(Context context, AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
@@ -131,6 +127,13 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setMax(int max) {
|
||||||
|
this.maxValue = max;
|
||||||
|
}
|
||||||
|
public int getMax() {
|
||||||
|
return this.maxValue;
|
||||||
|
}
|
||||||
|
|
||||||
public void setProgress(int progress) {
|
public void setProgress(int progress) {
|
||||||
this.currentValue = progress;
|
this.currentValue = progress;
|
||||||
if (seekBar != null) {
|
if (seekBar != null) {
|
||||||
|
|||||||
@@ -51,26 +51,6 @@ public class StreamSettings extends Activity {
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
addPreferencesFromResource(R.xml.preferences);
|
addPreferencesFromResource(R.xml.preferences);
|
||||||
|
|
||||||
// Add a listener to the FPS and resolution preference
|
|
||||||
// so the bitrate can be auto-adjusted
|
|
||||||
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING);
|
|
||||||
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
|
||||||
String valueStr = (String) newValue;
|
|
||||||
|
|
||||||
// Write the new bitrate value
|
|
||||||
prefs.edit()
|
|
||||||
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
|
|
||||||
PreferenceConfiguration.getDefaultBitrate(valueStr))
|
|
||||||
.apply();
|
|
||||||
|
|
||||||
// Allow the original preference change to take place
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
package com.limelight.utils;
|
package com.limelight.utils;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
@@ -11,6 +9,8 @@ import java.io.InputStream;
|
|||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.util.Scanner;
|
||||||
|
|
||||||
public class CacheHelper {
|
public class CacheHelper {
|
||||||
private static File openPath(boolean createPath, File root, String... path) {
|
private static File openPath(boolean createPath, File root, String... path) {
|
||||||
@@ -30,25 +30,12 @@ public class CacheHelper {
|
|||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean cacheFileExists(File root, String... path) {
|
public static FileInputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
|
||||||
return openPath(false, root, path).exists();
|
return new FileInputStream(openPath(false, root, path));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
|
public static FileOutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
|
||||||
return new BufferedInputStream(new FileInputStream(openPath(false, root, path)));
|
return new FileOutputStream(openPath(true, root, path));
|
||||||
}
|
|
||||||
|
|
||||||
public static OutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
|
|
||||||
return new BufferedOutputStream(new FileOutputStream(openPath(true, root, path)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void writeInputStreamToOutputStream(InputStream in, OutputStream out) throws IOException {
|
|
||||||
byte[] buf = new byte[4096];
|
|
||||||
int bytesRead;
|
|
||||||
|
|
||||||
while ((bytesRead = in.read(buf)) != -1) {
|
|
||||||
out.write(buf, 0, bytesRead);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String readInputStreamToString(InputStream in) throws IOException {
|
public static String readInputStreamToString(InputStream in) throws IOException {
|
||||||
|
|||||||
@@ -7,16 +7,15 @@ import android.app.AlertDialog;
|
|||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
|
|
||||||
public class Dialog implements Runnable {
|
public class Dialog implements Runnable {
|
||||||
private final String title;
|
private String title, message;
|
||||||
private final String message;
|
private Activity activity;
|
||||||
private final Activity activity;
|
private boolean endAfterDismiss;
|
||||||
private final boolean endAfterDismiss;
|
|
||||||
|
|
||||||
private AlertDialog alert;
|
private AlertDialog alert;
|
||||||
|
|
||||||
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
|
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
|
||||||
|
|
||||||
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
public Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||||
{
|
{
|
||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
package com.limelight.utils;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.limelight.Game;
|
|
||||||
import com.limelight.R;
|
|
||||||
import com.limelight.binding.PlatformBinding;
|
|
||||||
import com.limelight.computers.ComputerManagerService;
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
import com.limelight.nvstream.http.GfeHttpResponseException;
|
|
||||||
import com.limelight.nvstream.http.NvApp;
|
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
|
|
||||||
public class ServerHelper {
|
|
||||||
public static InetAddress getCurrentAddressFromComputer(ComputerDetails computer) {
|
|
||||||
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
|
||||||
computer.localIp : computer.remoteIp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
|
||||||
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
|
||||||
Intent intent = new Intent(parent, Game.class);
|
|
||||||
intent.putExtra(Game.EXTRA_HOST,
|
|
||||||
computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
|
||||||
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
|
|
||||||
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
|
||||||
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
|
||||||
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
|
||||||
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
|
||||||
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
|
||||||
parent.startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void doQuit(final Activity parent,
|
|
||||||
final InetAddress address,
|
|
||||||
final NvApp app,
|
|
||||||
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
|
||||||
final Runnable onComplete) {
|
|
||||||
Toast.makeText(parent, parent.getResources().getString(R.string.applist_quit_app) + " " + app.getAppName() + "...", Toast.LENGTH_SHORT).show();
|
|
||||||
new Thread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
NvHTTP httpConn;
|
|
||||||
String message;
|
|
||||||
try {
|
|
||||||
httpConn = new NvHTTP(address,
|
|
||||||
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(parent));
|
|
||||||
if (httpConn.quitApp()) {
|
|
||||||
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
|
|
||||||
} else {
|
|
||||||
message = parent.getResources().getString(R.string.applist_quit_fail) + " " + app.getAppName();
|
|
||||||
}
|
|
||||||
} catch (GfeHttpResponseException e) {
|
|
||||||
if (e.getErrorCode() == 599) {
|
|
||||||
message = "This session wasn't started by this device," +
|
|
||||||
" so it cannot be quit. End streaming on the original " +
|
|
||||||
"device or the PC itself. (Error code: "+e.getErrorCode()+")";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
message = e.getMessage();
|
|
||||||
}
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
message = parent.getResources().getString(R.string.error_unknown_host);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
message = parent.getResources().getString(R.string.error_404);
|
|
||||||
} catch (Exception e) {
|
|
||||||
message = e.getMessage();
|
|
||||||
} finally {
|
|
||||||
if (onComplete != null) {
|
|
||||||
onComplete.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final String toastMessage = message;
|
|
||||||
parent.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Toast.makeText(parent, toastMessage, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,15 +9,14 @@ import android.content.DialogInterface;
|
|||||||
import android.content.DialogInterface.OnCancelListener;
|
import android.content.DialogInterface.OnCancelListener;
|
||||||
|
|
||||||
public class SpinnerDialog implements Runnable,OnCancelListener {
|
public class SpinnerDialog implements Runnable,OnCancelListener {
|
||||||
private final String title;
|
private String title, message;
|
||||||
private final String message;
|
private Activity activity;
|
||||||
private final Activity activity;
|
|
||||||
private ProgressDialog progress;
|
private ProgressDialog progress;
|
||||||
private final boolean finish;
|
private boolean finish;
|
||||||
|
|
||||||
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
|
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
|
||||||
|
|
||||||
private SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
public SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
||||||
{
|
{
|
||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
package com.limelight.utils;
|
package com.limelight.utils;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.app.UiModeManager;
|
import android.app.UiModeManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.limelight.R;
|
|
||||||
|
|
||||||
public class UiHelper {
|
public class UiHelper {
|
||||||
|
|
||||||
// Values from https://developer.android.com/training/tv/start/layouts.html
|
// Values from https://developer.android.com/training/tv/start/layouts.html
|
||||||
@@ -32,31 +28,4 @@ public class UiHelper {
|
|||||||
horizontalPaddingPixels, verticalPaddingPixels);
|
horizontalPaddingPixels, verticalPaddingPixels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) {
|
|
||||||
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
switch (which){
|
|
||||||
case DialogInterface.BUTTON_POSITIVE:
|
|
||||||
if (onYes != null) {
|
|
||||||
onYes.run();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case DialogInterface.BUTTON_NEGATIVE:
|
|
||||||
if (onNo != null) {
|
|
||||||
onNo.run();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(parent);
|
|
||||||
builder.setMessage(parent.getResources().getString(R.string.applist_quit_confirmation))
|
|
||||||
.setPositiveButton(parent.getResources().getString(R.string.yes), dialogClickListener)
|
|
||||||
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@@ -10,4 +10,4 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="center" />
|
android:layout_gravity="center" />
|
||||||
|
|
||||||
</merge>
|
</FrameLayout>
|
||||||
@@ -9,7 +9,5 @@
|
|||||||
android:numColumns="auto_fit"
|
android:numColumns="auto_fit"
|
||||||
android:columnWidth="160dp"
|
android:columnWidth="160dp"
|
||||||
android:stretchMode="spacingWidth"
|
android:stretchMode="spacingWidth"
|
||||||
android:focusable="true"
|
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
android:gravity="center"/>
|
android:gravity="center"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -9,7 +9,5 @@
|
|||||||
android:numColumns="auto_fit"
|
android:numColumns="auto_fit"
|
||||||
android:columnWidth="105dp"
|
android:columnWidth="105dp"
|
||||||
android:stretchMode="spacingWidth"
|
android:stretchMode="spacingWidth"
|
||||||
android:focusable="true"
|
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
android:gravity="center"/>
|
android:gravity="center"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/dialog_resolution"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:id="@+id/aspectGroup">
|
||||||
|
<RadioButton
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="5dp"
|
||||||
|
android:id="@+id/aspect_4_3"
|
||||||
|
android:text="@string/aspect_4_3"/>
|
||||||
|
<RadioButton
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="5dp"
|
||||||
|
android:id="@+id/aspect_16_9"
|
||||||
|
android:text="@string/aspect_16_9"/>
|
||||||
|
<RadioButton
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="5dp"
|
||||||
|
android:id="@+id/aspect_16_10"
|
||||||
|
android:text="@string/aspect_16_10"/>
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<SeekBar
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@+id/aspectGroup"
|
||||||
|
android:max="3"
|
||||||
|
android:id="@+id/resolutionSeekbar"/>
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:id="@+id/resolutionText"
|
||||||
|
android:layout_below="@+id/resolutionSeekbar"/>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
@@ -6,12 +6,10 @@
|
|||||||
<ListView
|
<ListView
|
||||||
android:id="@+id/fragmentView"
|
android:id="@+id/fragmentView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/list_view_unselected"
|
android:background="@drawable/list_view_unselected"
|
||||||
android:fastScrollEnabled="true"
|
android:fastScrollEnabled="true"
|
||||||
android:longClickable="false"
|
android:longClickable="false"
|
||||||
android:focusable="true"
|
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
android:stackFromBottom="false" >
|
android:stackFromBottom="false" >
|
||||||
|
|
||||||
</ListView>
|
</ListView>
|
||||||
|
|||||||
@@ -8,7 +8,5 @@
|
|||||||
android:layout_height="fill_parent"
|
android:layout_height="fill_parent"
|
||||||
android:numColumns="auto_fit"
|
android:numColumns="auto_fit"
|
||||||
android:columnWidth="160dp"
|
android:columnWidth="160dp"
|
||||||
android:focusable="true"
|
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
android:gravity="center"/>
|
android:gravity="center"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -8,7 +8,5 @@
|
|||||||
android:layout_height="fill_parent"
|
android:layout_height="fill_parent"
|
||||||
android:numColumns="auto_fit"
|
android:numColumns="auto_fit"
|
||||||
android:columnWidth="105dp"
|
android:columnWidth="105dp"
|
||||||
android:focusable="true"
|
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
android:gravity="center"/>
|
android:gravity="center"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -95,8 +95,6 @@
|
|||||||
<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>
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string-array name="resolution_names">
|
<string-array name="fps_names">
|
||||||
<item>720p 30 FPS</item>
|
<item>30 FPS</item>
|
||||||
<item>720p 60 FPS</item>
|
<item>60 FPS</item>
|
||||||
<item>1080p 30 FPS</item>
|
|
||||||
<item>1080p 60 FPS</item>
|
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="resolution_values" translatable="false">
|
<string-array name="fps_values" translatable="false">
|
||||||
<item>720p30</item>
|
<item>30</item>
|
||||||
<item>720p60</item>
|
<item>60</item>
|
||||||
<item>1080p30</item>
|
|
||||||
<item>1080p60</item>
|
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="language_names" translatable="false">
|
<string-array name="language_names" translatable="false">
|
||||||
|
|||||||
@@ -44,9 +44,9 @@
|
|||||||
<string name="conn_establishing_title">Establishing Connection</string>
|
<string name="conn_establishing_title">Establishing Connection</string>
|
||||||
<string name="conn_establishing_msg">Starting connection</string>
|
<string name="conn_establishing_msg">Starting connection</string>
|
||||||
<string name="conn_metered">Warning: Your active network connection is metered!</string>
|
<string name="conn_metered">Warning: Your active network connection is metered!</string>
|
||||||
<string name="conn_client_latency">Average frame decoding latency:</string>
|
<string name="conn_client_latency">Average client-side frame latency:</string>
|
||||||
<string name="conn_client_latency_hw">hardware decoder latency:</string>
|
<string name="conn_client_latency_hw">hardware decoder latency:</string>
|
||||||
<string name="conn_hardware_latency">Average hardware decoding latency:</string>
|
<string name="conn_hardware_latency">Average hardware decoder latency:</string>
|
||||||
<string name="conn_starting">Starting</string>
|
<string name="conn_starting">Starting</string>
|
||||||
<string name="conn_error_title">Connection Error</string>
|
<string name="conn_error_title">Connection Error</string>
|
||||||
<string name="conn_error_msg">Failed to start</string>
|
<string name="conn_error_msg">Failed to start</string>
|
||||||
@@ -85,8 +85,10 @@
|
|||||||
|
|
||||||
<!-- Preferences -->
|
<!-- Preferences -->
|
||||||
<string name="category_basic_settings">Basic Settings</string>
|
<string name="category_basic_settings">Basic Settings</string>
|
||||||
<string name="title_resolution_list">Select resolution and FPS target</string>
|
<string name="title_resolution">Select stream resolution</string>
|
||||||
<string name="summary_resolution_list">Setting values too high for your device may cause lag or crashing</string>
|
<string name="summary_resolution">Some resolutions may not be supported on your device</string>
|
||||||
|
<string name="title_fps_list">Select target FPS</string>
|
||||||
|
<string name="summary_fps_list">Some FPS values may not be supported with high resolutions on your device</string>
|
||||||
<string name="title_seekbar_bitrate">Select target video bitrate</string>
|
<string name="title_seekbar_bitrate">Select target video bitrate</string>
|
||||||
<string name="summary_seekbar_bitrate">Lower bitrate to reduce stuttering. Raise bitrate to increase image quality.</string>
|
<string name="summary_seekbar_bitrate">Lower bitrate to reduce stuttering. Raise bitrate to increase image quality.</string>
|
||||||
<string name="suffix_seekbar_bitrate">Mbps</string>
|
<string name="suffix_seekbar_bitrate">Mbps</string>
|
||||||
@@ -95,8 +97,6 @@
|
|||||||
<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>
|
||||||
|
|
||||||
@@ -117,4 +117,9 @@
|
|||||||
<string name="category_advanced_settings">Advanced Settings</string>
|
<string name="category_advanced_settings">Advanced Settings</string>
|
||||||
<string name="title_decoder_list">Change decoder</string>
|
<string name="title_decoder_list">Change decoder</string>
|
||||||
<string name="summary_decoder_list">Software decoding may improve video latency at lower streaming settings</string>
|
<string name="summary_decoder_list">Software decoding may improve video latency at lower streaming settings</string>
|
||||||
|
|
||||||
|
<!-- Resolution Preference -->
|
||||||
|
<string name="aspect_4_3">4:3</string>
|
||||||
|
<string name="aspect_16_9">16:9</string>
|
||||||
|
<string name="aspect_16_10">16:10</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -2,13 +2,17 @@
|
|||||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/category_basic_settings">
|
<PreferenceCategory android:title="@string/category_basic_settings">
|
||||||
|
<com.limelight.preferences.ResolutionPreference
|
||||||
|
android:key="resolution"
|
||||||
|
android:title="@string/title_resolution"
|
||||||
|
android:summary="@string/summary_resolution" />
|
||||||
<ListPreference
|
<ListPreference
|
||||||
android:key="list_resolution_fps"
|
android:key="list_resolution_fps"
|
||||||
android:title="@string/title_resolution_list"
|
android:title="@string/title_fps_list"
|
||||||
android:summary="@string/summary_resolution_list"
|
android:summary="@string/summary_fps_list"
|
||||||
android:entries="@array/resolution_names"
|
android:entries="@array/fps_names"
|
||||||
android:entryValues="@array/resolution_values"
|
android:entryValues="@array/fps_values"
|
||||||
android:defaultValue="720p60" />
|
android:defaultValue="60" />
|
||||||
<com.limelight.preferences.SeekBarPreference
|
<com.limelight.preferences.SeekBarPreference
|
||||||
android:key="seekbar_bitrate"
|
android:key="seekbar_bitrate"
|
||||||
android:dialogMessage="@string/summary_seekbar_bitrate"
|
android:dialogMessage="@string/summary_seekbar_bitrate"
|
||||||
@@ -26,19 +30,14 @@
|
|||||||
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"/>
|
||||||
<CheckBoxPreference
|
</PreferenceCategory-->
|
||||||
android:key="checkbox_multi_controller"
|
|
||||||
android:title="@string/title_checkbox_multi_controller"
|
|
||||||
android:summary="@string/summary_checkbox_multi_controller"
|
|
||||||
android:defaultValue="true" />
|
|
||||||
</PreferenceCategory>
|
|
||||||
<PreferenceCategory android:title="@string/category_host_settings">
|
<PreferenceCategory android:title="@string/category_host_settings">
|
||||||
<CheckBoxPreference
|
<CheckBoxPreference
|
||||||
android:key="checkbox_enable_sops"
|
android:key="checkbox_enable_sops"
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:1.1.0'
|
classpath 'com.android.tools.build:gradle:1.0.0'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
Reference in new issue
Block a user