mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-10-26 05:50:23 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdbecfa6dd | ||
|
|
5e45aeb70b |
No files matched your search
+12
-10
@@ -9,12 +9,11 @@
|
||||
<facet type="android" name="Android">
|
||||
<configuration>
|
||||
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
|
||||
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
|
||||
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
|
||||
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
|
||||
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugAndroidTest" />
|
||||
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" />
|
||||
<option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" />
|
||||
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugAndroidTestSources" />
|
||||
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugTestSources" />
|
||||
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
||||
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
||||
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
|
||||
@@ -25,7 +24,6 @@
|
||||
</component>
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="false">
|
||||
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
|
||||
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/nonRoot/debug" />
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
|
||||
@@ -41,12 +39,12 @@
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/jni" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/nonRoot/debug" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/androidTest/nonRoot/debug" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/test/nonRoot/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/test/nonRoot/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/nonRoot/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/nonRoot/debug" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/test/nonRoot/debug" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
|
||||
@@ -106,10 +104,14 @@
|
||||
<orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
|
||||
<orderEntry type="library" exported="" name="androidasync-2.0.5" level="project" />
|
||||
<orderEntry type="library" exported="" name="gson-2.3.1" level="project" />
|
||||
<orderEntry type="library" exported="" name="support-v4-r7" level="project" />
|
||||
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
|
||||
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" />
|
||||
<orderEntry type="library" exported="" name="tinyrtsp" level="project" />
|
||||
<orderEntry type="library" exported="" name="limelight-common" level="project" />
|
||||
<orderEntry type="library" exported="" name="ion-2.0.5" level="project" />
|
||||
<orderEntry type="library" exported="" name="okhttp-2.2.0" level="project" />
|
||||
<orderEntry type="library" exported="" name="jcodec-0.1.9" level="project" />
|
||||
<orderEntry type="library" exported="" name="okio-1.2.0" level="project" />
|
||||
|
||||
+11
-7
@@ -11,8 +11,8 @@ android {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 21
|
||||
|
||||
versionName "3.1.2"
|
||||
versionCode = 56
|
||||
versionName "3.1-beta1"
|
||||
versionCode = 50
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
@@ -62,13 +62,17 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile group: 'org.jcodec', name: 'jcodec', version: '0.1.9'
|
||||
compile group: 'org.jcodec', name: 'jcodec', version: '+'
|
||||
|
||||
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
|
||||
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
|
||||
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '+'
|
||||
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '+'
|
||||
|
||||
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.2.0'
|
||||
compile group: 'com.squareup.okio', name:'okio', version:'1.2.0'
|
||||
compile group: 'com.google.android', name: 'support-v4', version:'+'
|
||||
compile group: 'com.koushikdutta.ion', name: 'ion', version:'+'
|
||||
compile group: 'com.google.code.gson', name: 'gson', version:'+'
|
||||
|
||||
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'+'
|
||||
compile group: 'com.squareup.okio', name:'okio', version:'+'
|
||||
|
||||
compile files('libs/jmdns-fixed.jar')
|
||||
compile files('libs/limelight-common.jar')
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,9 @@
|
||||
package com.limelight;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.StringReader;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
@@ -8,7 +11,10 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
||||
import com.limelight.computers.ComputerManagerListener;
|
||||
import com.limelight.computers.ComputerManagerService;
|
||||
import com.limelight.grid.AppGridAdapter;
|
||||
@@ -42,6 +48,7 @@ import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||
@@ -55,6 +62,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
private SpinnerDialog blockingLoadSpinner;
|
||||
private String lastRawApplist;
|
||||
|
||||
private int consecutiveAppListFailures = 0;
|
||||
private final static int CONSECUTIVE_FAILURE_LIMIT = 3;
|
||||
|
||||
private final static int START_OR_RESUME_ID = 1;
|
||||
private final static int QUIT_ID = 2;
|
||||
private final static int CANCEL_ID = 3;
|
||||
@@ -64,7 +74,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
public final static String UUID_EXTRA = "UUID";
|
||||
|
||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
private ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||
final ComputerManagerService.ComputerManagerBinder localBinder =
|
||||
((ComputerManagerService.ComputerManagerBinder)binder);
|
||||
@@ -125,11 +135,14 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
@Override
|
||||
public void notifyComputerUpdated(ComputerDetails details) {
|
||||
// Don't care about other computers
|
||||
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
|
||||
if (details != computer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (details.state == ComputerDetails.State.OFFLINE) {
|
||||
if (details.state != ComputerDetails.State.ONLINE) {
|
||||
consecutiveAppListFailures++;
|
||||
|
||||
if (consecutiveAppListFailures >= CONSECUTIVE_FAILURE_LIMIT) {
|
||||
// The PC is unreachable now
|
||||
AppView.this.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
@@ -139,10 +152,13 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
consecutiveAppListFailures = 0;
|
||||
|
||||
// App list is the same or empty; nothing to do
|
||||
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
|
||||
return;
|
||||
@@ -156,7 +172,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
blockingLoadSpinner.dismiss();
|
||||
blockingLoadSpinner = null;
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception e) {}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -174,10 +190,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
if (managerBinder != null) {
|
||||
managerBinder.stopPolling();
|
||||
}
|
||||
|
||||
if (appGridAdapter != null) {
|
||||
appGridAdapter.cancelQueuedOperations();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -260,6 +272,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
int runningAppId = -1;
|
||||
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||
AppObject app = (AppObject) appGridAdapter.getItem(i);
|
||||
if (app.app == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (app.app.getIsRunning()) {
|
||||
runningAppId = app.app.getAppId();
|
||||
break;
|
||||
@@ -274,6 +290,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
|
||||
if (selectedApp == null || selectedApp.app == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int runningAppId = getRunningAppId();
|
||||
if (runningAppId != -1) {
|
||||
if (runningAppId == selectedApp.app.getAppId()) {
|
||||
@@ -362,17 +382,20 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
public void run() {
|
||||
boolean updated = false;
|
||||
|
||||
// First handle app updates and additions
|
||||
for (NvApp app : appList) {
|
||||
boolean foundExistingApp = false;
|
||||
|
||||
// Try to update an existing app in the list first
|
||||
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||
if (existingApp.app == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existingApp.app.getAppId() == app.getAppId()) {
|
||||
// Found the app; update its properties
|
||||
if (existingApp.app.getIsRunning() != app.getIsRunning()) {
|
||||
existingApp.app.setIsRunning(app.getIsRunning());
|
||||
existingApp.app.setIsRunningBoolean(app.getIsRunning());
|
||||
updated = true;
|
||||
}
|
||||
if (!existingApp.app.getAppName().equals(app.getAppName())) {
|
||||
@@ -392,34 +415,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
}
|
||||
|
||||
// Next handle app removals
|
||||
int i = 0;
|
||||
while (i < appGridAdapter.getCount()) {
|
||||
boolean foundExistingApp = false;
|
||||
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||
|
||||
// Check if this app is in the latest list
|
||||
for (NvApp app : appList) {
|
||||
if (existingApp.app.getAppId() == app.getAppId()) {
|
||||
foundExistingApp = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// This app was removed in the latest app list
|
||||
if (!foundExistingApp) {
|
||||
appGridAdapter.removeApp(existingApp);
|
||||
updated = true;
|
||||
|
||||
// Check this same index again because the item at i+1 is now at i after
|
||||
// the removal
|
||||
continue;
|
||||
}
|
||||
|
||||
// Move on to the next item
|
||||
i++;
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
appGridAdapter.notifyDataSetChanged();
|
||||
}
|
||||
@@ -432,8 +427,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
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_APP, app.getAppName());
|
||||
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
||||
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
||||
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
||||
@@ -451,18 +445,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
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 (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()+")";
|
||||
message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName();
|
||||
}
|
||||
else {
|
||||
message = e.getMessage();
|
||||
message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName();
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
message = getResources().getString(R.string.error_unknown_host);
|
||||
@@ -503,6 +489,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||
long id) {
|
||||
AppObject app = (AppObject) appGridAdapter.getItem(pos);
|
||||
if (app == null || app.app == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only open the context menu if something is running, otherwise start it
|
||||
if (getRunningAppId() != -1) {
|
||||
@@ -513,16 +502,12 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
});
|
||||
registerForContextMenu(listView);
|
||||
listView.requestFocus();
|
||||
}
|
||||
|
||||
public class AppObject {
|
||||
public final NvApp app;
|
||||
public NvApp app;
|
||||
|
||||
public AppObject(NvApp app) {
|
||||
if (app == null) {
|
||||
throw new IllegalArgumentException("app must not be null");
|
||||
}
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import com.limelight.nvstream.NvConnection;
|
||||
import com.limelight.nvstream.NvConnectionListener;
|
||||
import com.limelight.nvstream.StreamConfiguration;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.input.KeyboardPacket;
|
||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||
import com.limelight.preferences.PreferenceConfiguration;
|
||||
@@ -26,7 +25,6 @@ import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Point;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.media.AudioManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.wifi.WifiManager;
|
||||
@@ -61,7 +59,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
private int lastButtonState = 0;
|
||||
|
||||
// Only 2 touches are supported
|
||||
private final TouchContext[] touchContextMap = new TouchContext[2];
|
||||
private TouchContext[] touchContextMap = new TouchContext[2];
|
||||
private long threeFingerDownTime = 0;
|
||||
|
||||
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
|
||||
@@ -70,7 +68,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
private KeyboardTranslator keybTranslator;
|
||||
|
||||
private PreferenceConfiguration prefConfig;
|
||||
private final Point screenSize = new Point(0, 0);
|
||||
private Point screenSize = new Point(0, 0);
|
||||
|
||||
private NvConnection conn;
|
||||
private SpinnerDialog spinner;
|
||||
@@ -90,8 +88,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
private int drFlags = 0;
|
||||
|
||||
public static final String EXTRA_HOST = "Host";
|
||||
public static final String EXTRA_APP_NAME = "AppName";
|
||||
public static final String EXTRA_APP_ID = "AppId";
|
||||
public static final String EXTRA_APP = "App";
|
||||
public static final String EXTRA_UNIQUEID = "UniqueId";
|
||||
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
||||
|
||||
@@ -173,23 +170,17 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
wifiLock.acquire();
|
||||
|
||||
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
|
||||
String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME);
|
||||
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
|
||||
String app = Game.this.getIntent().getStringExtra(EXTRA_APP);
|
||||
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
||||
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
|
||||
|
||||
if (appId == StreamConfiguration.INVALID_APP_ID) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
decoderRenderer = new ConfigurableDecoderRenderer();
|
||||
decoderRenderer.initializeWithFlags(drFlags);
|
||||
|
||||
StreamConfiguration config = new StreamConfiguration.Builder()
|
||||
.setResolution(prefConfig.width, prefConfig.height)
|
||||
.setRefreshRate(prefConfig.fps)
|
||||
.setApp(new NvApp(appName, appId))
|
||||
.setApp(app)
|
||||
.setBitrate(prefConfig.bitrate * 1000)
|
||||
.setEnableSops(prefConfig.enableSops)
|
||||
.enableAdaptiveResolution((decoderRenderer.getCapabilities() &
|
||||
@@ -202,10 +193,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
// Initialize the connection
|
||||
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
|
||||
keybTranslator = new KeyboardTranslator(conn);
|
||||
controllerHandler = new ControllerHandler(conn, this, prefConfig.multiController, prefConfig.deadzonePercentage);
|
||||
|
||||
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
||||
inputManager.registerInputDeviceListener(controllerHandler, null);
|
||||
controllerHandler = new ControllerHandler(conn, this, prefConfig.deadzonePercentage);
|
||||
|
||||
SurfaceHolder sh = sv.getHolder();
|
||||
if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) {
|
||||
@@ -234,12 +222,22 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
{
|
||||
// Get the visible width of the activity
|
||||
double visibleWidth = getWindow().getDecorView().getWidth();
|
||||
double visibleHeight = getWindow().getDecorView().getHeight();
|
||||
|
||||
ViewGroup.LayoutParams lp = sv.getLayoutParams();
|
||||
|
||||
double vidRatio = vidHeight / vidWidth;
|
||||
double visibleRatio = visibleHeight / visibleWidth;
|
||||
|
||||
// Calculate the new size of the SurfaceView
|
||||
if (vidRatio < visibleRatio) {
|
||||
lp.width = (int) visibleWidth;
|
||||
lp.height = (int) ((vidHeight / vidWidth) * visibleWidth);
|
||||
}
|
||||
else {
|
||||
lp.width = (int) ((vidWidth / vidHeight) * visibleHeight);
|
||||
lp.height = (int) visibleHeight;
|
||||
}
|
||||
|
||||
// Apply the size change
|
||||
sv.setLayoutParams(lp);
|
||||
@@ -254,7 +252,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private final Runnable hideSystemUi = new Runnable() {
|
||||
private Runnable hideSystemUi = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Use immersive mode on 4.4+ or standard low profile on previous builds
|
||||
@@ -290,9 +288,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
SpinnerDialog.closeDialogs(this);
|
||||
Dialog.closeDialogs();
|
||||
|
||||
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
||||
inputManager.unregisterInputDeviceListener(controllerHandler);
|
||||
|
||||
displayedFailureDialog = true;
|
||||
stopConnection();
|
||||
|
||||
@@ -323,7 +318,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
wifiLock.release();
|
||||
}
|
||||
|
||||
private final Runnable toggleGrab = new Runnable() {
|
||||
private Runnable toggleGrab = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.limelight.utils.Dialog;
|
||||
import com.limelight.utils.UiHelper;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
@@ -52,7 +53,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
private PcGridAdapter pcGridAdapter;
|
||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||
private boolean freezeUpdates, runningPolling;
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
private ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||
final ComputerManagerService.ComputerManagerBinder localBinder =
|
||||
((ComputerManagerService.ComputerManagerBinder)binder);
|
||||
@@ -237,7 +238,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
||||
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||
if (computer == null || computer.details == null ||
|
||||
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||
startComputerUpdates();
|
||||
return;
|
||||
}
|
||||
@@ -253,10 +255,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
else {
|
||||
menu.add(Menu.NONE, APP_LIST_ID, 1, 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, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
||||
menu.add(Menu.NONE, UNPAIR_ID, 2, getResources().getString(R.string.pcview_menu_unpair_pc));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,12 +516,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
if (details.equals(computer.details)) {
|
||||
pcGridAdapter.removeComputer(computer);
|
||||
pcGridAdapter.notifyDataSetChanged();
|
||||
|
||||
if (pcGridAdapter.getCount() == 0) {
|
||||
// Show the "Discovery in progress" view
|
||||
noPcFoundLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -535,7 +528,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
||||
|
||||
// Check if this is the same computer
|
||||
if (details.uuid.equals(computer.details.uuid)) {
|
||||
if (details.equals(computer.details)) {
|
||||
existingEntry = computer;
|
||||
break;
|
||||
}
|
||||
@@ -592,9 +585,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
public ComputerDetails details;
|
||||
|
||||
public ComputerObject(ComputerDetails details) {
|
||||
if (details == null) {
|
||||
throw new IllegalArgumentException("details must not be null");
|
||||
}
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||
|
||||
public class AndroidAudioRenderer implements AudioRenderer {
|
||||
|
||||
private static final int FRAME_SIZE = 960;
|
||||
public static final int FRAME_SIZE = 960;
|
||||
|
||||
private AudioTrack track;
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@ import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||
|
||||
public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
|
||||
private final File certFile;
|
||||
private final File keyFile;
|
||||
private File certFile;
|
||||
private File keyFile;
|
||||
|
||||
private X509Certificate cert;
|
||||
private RSAPrivateKey key;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.limelight.binding.input;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import android.hardware.input.InputManager;
|
||||
import android.os.SystemClock;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
@@ -15,10 +13,26 @@ import com.limelight.nvstream.input.ControllerPacket;
|
||||
import com.limelight.ui.GameGestures;
|
||||
import com.limelight.utils.Vector2d;
|
||||
|
||||
public class ControllerHandler implements InputManager.InputDeviceListener {
|
||||
public class ControllerHandler {
|
||||
private short inputMap = 0x0000;
|
||||
private byte leftTrigger = 0x00;
|
||||
private byte rightTrigger = 0x00;
|
||||
private short rightStickX = 0x0000;
|
||||
private short rightStickY = 0x0000;
|
||||
private short leftStickX = 0x0000;
|
||||
private short leftStickY = 0x0000;
|
||||
private int emulatingButtonFlags = 0;
|
||||
|
||||
// Used for OUYA bumper state tracking since they force all buttons
|
||||
// up when the OUYA button goes down. We watch the last time we get
|
||||
// a bumper up and compare that to our maximum delay when we receive
|
||||
// a Start button press to see if we should activate one of our
|
||||
// emulated button combos.
|
||||
private long lastLbUpTime = 0;
|
||||
private long lastRbUpTime = 0;
|
||||
private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
|
||||
|
||||
private long startDownTime = 0;
|
||||
private static final int START_DOWN_TIME_KEYB_MS = 750;
|
||||
|
||||
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
|
||||
@@ -29,23 +43,19 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
||||
private static final int EMULATED_SPECIAL_UP_DELAY_MS = 100;
|
||||
private static final int EMULATED_SELECT_UP_DELAY_MS = 30;
|
||||
|
||||
private final Vector2d inputVector = new Vector2d();
|
||||
private Vector2d inputVector = new Vector2d();
|
||||
|
||||
private final HashMap<String, ControllerContext> contexts = new HashMap<String, ControllerContext>();
|
||||
private HashMap<String, ControllerMapping> mappings = new HashMap<String, ControllerMapping>();
|
||||
|
||||
private final NvConnection conn;
|
||||
private final double stickDeadzone;
|
||||
private final ControllerContext defaultContext = new ControllerContext();
|
||||
private final GameGestures gestures;
|
||||
private NvConnection conn;
|
||||
private double stickDeadzone;
|
||||
private final ControllerMapping defaultMapping = new ControllerMapping();
|
||||
private GameGestures gestures;
|
||||
private boolean hasGameController;
|
||||
|
||||
private final boolean multiControllerEnabled;
|
||||
private short currentControllers;
|
||||
|
||||
public ControllerHandler(NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
|
||||
public ControllerHandler(NvConnection conn, GameGestures gestures, int deadzonePercentage) {
|
||||
this.conn = conn;
|
||||
this.gestures = gestures;
|
||||
this.multiControllerEnabled = multiControllerEnabled;
|
||||
|
||||
// HACK: For now we're hardcoding a 10% deadzone. Some deadzone
|
||||
// is required for controller batching support to work.
|
||||
@@ -72,16 +82,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
||||
|
||||
this.stickDeadzone = (double)deadzonePercentage / 100.0;
|
||||
|
||||
// Initialize the default context for events with no device
|
||||
defaultContext.leftStickXAxis = MotionEvent.AXIS_X;
|
||||
defaultContext.leftStickYAxis = MotionEvent.AXIS_Y;
|
||||
defaultContext.leftStickDeadzoneRadius = (float) stickDeadzone;
|
||||
defaultContext.rightStickXAxis = MotionEvent.AXIS_Z;
|
||||
defaultContext.rightStickYAxis = MotionEvent.AXIS_RZ;
|
||||
defaultContext.rightStickDeadzoneRadius = (float) stickDeadzone;
|
||||
defaultContext.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
||||
defaultContext.controllerNumber = (short) 0;
|
||||
// Initialize the default mapping for events with no device
|
||||
defaultMapping.leftStickXAxis = MotionEvent.AXIS_X;
|
||||
defaultMapping.leftStickYAxis = MotionEvent.AXIS_Y;
|
||||
defaultMapping.leftStickDeadzoneRadius = (float) stickDeadzone;
|
||||
defaultMapping.rightStickXAxis = MotionEvent.AXIS_Z;
|
||||
defaultMapping.rightStickYAxis = MotionEvent.AXIS_RZ;
|
||||
defaultMapping.rightStickDeadzoneRadius = (float) stickDeadzone;
|
||||
defaultMapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||
defaultMapping.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
||||
}
|
||||
|
||||
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
|
||||
@@ -97,63 +106,19 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
||||
return range;
|
||||
}
|
||||
|
||||
private short assignNewControllerNumber() {
|
||||
for (short i = 0; i < 4; i++) {
|
||||
if ((currentControllers & (1 << i)) == 0) {
|
||||
// Found an unused controller value
|
||||
currentControllers |= (1 << i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceAdded(int deviceId) {
|
||||
// Nothing happening here yet
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceRemoved(int deviceId) {
|
||||
for (Map.Entry<String, ControllerContext> device : contexts.entrySet()) {
|
||||
if (device.getValue().id == deviceId) {
|
||||
LimeLog.info("Removed controller: "+device.getValue().name);
|
||||
releaseControllerNumber(device.getValue().controllerNumber);
|
||||
contexts.remove(device.getKey());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceChanged(int deviceId) {
|
||||
// Remove and re-add
|
||||
onInputDeviceRemoved(deviceId);
|
||||
onInputDeviceAdded(deviceId);
|
||||
}
|
||||
|
||||
private void releaseControllerNumber(int controllerNumber) {
|
||||
LimeLog.info("Controller number "+controllerNumber+" is now available");
|
||||
currentControllers &= ~(1 << controllerNumber);
|
||||
}
|
||||
|
||||
private ControllerContext createContextForDevice(InputDevice dev) {
|
||||
ControllerContext context = new ControllerContext();
|
||||
private ControllerMapping createMappingForDevice(InputDevice dev) {
|
||||
ControllerMapping mapping = new ControllerMapping();
|
||||
String devName = dev.getName();
|
||||
|
||||
LimeLog.info("Creating controller context for device: "+devName);
|
||||
LimeLog.info("Creating controller mapping for device: "+devName);
|
||||
|
||||
context.name = devName;
|
||||
context.id = dev.getId();
|
||||
|
||||
context.leftStickXAxis = MotionEvent.AXIS_X;
|
||||
context.leftStickYAxis = MotionEvent.AXIS_Y;
|
||||
if (getMotionRangeForJoystickAxis(dev, context.leftStickXAxis) != null &&
|
||||
getMotionRangeForJoystickAxis(dev, context.leftStickYAxis) != null) {
|
||||
mapping.leftStickXAxis = MotionEvent.AXIS_X;
|
||||
mapping.leftStickYAxis = MotionEvent.AXIS_Y;
|
||||
if (getMotionRangeForJoystickAxis(dev, mapping.leftStickXAxis) != null &&
|
||||
getMotionRangeForJoystickAxis(dev, mapping.leftStickYAxis) != null) {
|
||||
// This is a gamepad
|
||||
hasGameController = true;
|
||||
context.hasJoystickAxes = true;
|
||||
mapping.hasJoystickAxes = true;
|
||||
}
|
||||
|
||||
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
|
||||
@@ -163,14 +128,14 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
||||
if (leftTriggerRange != null && rightTriggerRange != null)
|
||||
{
|
||||
// Some controllers use LTRIGGER and RTRIGGER (like Ouya)
|
||||
context.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER;
|
||||
context.rightTriggerAxis = MotionEvent.AXIS_RTRIGGER;
|
||||
mapping.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER;
|
||||
mapping.rightTriggerAxis = MotionEvent.AXIS_RTRIGGER;
|
||||
}
|
||||
else if (brakeRange != null && gasRange != null)
|
||||
{
|
||||
// Others use GAS and BRAKE (like Moga)
|
||||
context.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||
context.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
||||
mapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||
mapping.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -179,34 +144,34 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
||||
if (rxRange != null && ryRange != null && devName != null) {
|
||||
if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) {
|
||||
// Xbox controllers use RX and RY for right stick
|
||||
context.rightStickXAxis = MotionEvent.AXIS_RX;
|
||||
context.rightStickYAxis = MotionEvent.AXIS_RY;
|
||||
mapping.rightStickXAxis = MotionEvent.AXIS_RX;
|
||||
mapping.rightStickYAxis = MotionEvent.AXIS_RY;
|
||||
|
||||
// Xbox controllers use Z and RZ for triggers
|
||||
context.leftTriggerAxis = MotionEvent.AXIS_Z;
|
||||
context.rightTriggerAxis = MotionEvent.AXIS_RZ;
|
||||
context.triggersIdleNegative = true;
|
||||
context.isXboxController = true;
|
||||
mapping.leftTriggerAxis = MotionEvent.AXIS_Z;
|
||||
mapping.rightTriggerAxis = MotionEvent.AXIS_RZ;
|
||||
mapping.triggersIdleNegative = true;
|
||||
mapping.isXboxController = true;
|
||||
}
|
||||
else {
|
||||
// DS4 controller uses RX and RY for triggers
|
||||
context.leftTriggerAxis = MotionEvent.AXIS_RX;
|
||||
context.rightTriggerAxis = MotionEvent.AXIS_RY;
|
||||
context.triggersIdleNegative = true;
|
||||
mapping.leftTriggerAxis = MotionEvent.AXIS_RX;
|
||||
mapping.rightTriggerAxis = MotionEvent.AXIS_RY;
|
||||
mapping.triggersIdleNegative = true;
|
||||
|
||||
context.isDualShock4 = true;
|
||||
mapping.isDualShock4 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (context.rightStickXAxis == -1 && context.rightStickYAxis == -1) {
|
||||
if (mapping.rightStickXAxis == -1 && mapping.rightStickYAxis == -1) {
|
||||
InputDevice.MotionRange zRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z);
|
||||
InputDevice.MotionRange rzRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ);
|
||||
|
||||
// Most other controllers use Z and RZ for the right stick
|
||||
if (zRange != null && rzRange != null) {
|
||||
context.rightStickXAxis = MotionEvent.AXIS_Z;
|
||||
context.rightStickYAxis = MotionEvent.AXIS_RZ;
|
||||
mapping.rightStickXAxis = MotionEvent.AXIS_Z;
|
||||
mapping.rightStickYAxis = MotionEvent.AXIS_RZ;
|
||||
}
|
||||
else {
|
||||
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
||||
@@ -214,8 +179,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
||||
|
||||
// Try RX and RY now
|
||||
if (rxRange != null && ryRange != null) {
|
||||
context.rightStickXAxis = MotionEvent.AXIS_RX;
|
||||
context.rightStickYAxis = MotionEvent.AXIS_RY;
|
||||
mapping.rightStickXAxis = MotionEvent.AXIS_RX;
|
||||
mapping.rightStickYAxis = MotionEvent.AXIS_RY;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,30 +189,30 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
||||
InputDevice.MotionRange hatXRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_X);
|
||||
InputDevice.MotionRange hatYRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_Y);
|
||||
if (hatXRange != null && hatYRange != null) {
|
||||
context.hatXAxis = MotionEvent.AXIS_HAT_X;
|
||||
context.hatYAxis = MotionEvent.AXIS_HAT_Y;
|
||||
mapping.hatXAxis = MotionEvent.AXIS_HAT_X;
|
||||
mapping.hatYAxis = MotionEvent.AXIS_HAT_Y;
|
||||
}
|
||||
|
||||
if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
|
||||
context.leftStickDeadzoneRadius = (float) stickDeadzone;
|
||||
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
||||
mapping.leftStickDeadzoneRadius = (float) stickDeadzone;
|
||||
}
|
||||
|
||||
if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
|
||||
context.rightStickDeadzoneRadius = (float) stickDeadzone;
|
||||
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
||||
mapping.rightStickDeadzoneRadius = (float) stickDeadzone;
|
||||
}
|
||||
|
||||
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
|
||||
InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, context.leftTriggerAxis);
|
||||
InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, context.rightTriggerAxis);
|
||||
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
|
||||
InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, mapping.leftTriggerAxis);
|
||||
InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, mapping.rightTriggerAxis);
|
||||
|
||||
// It's important to have a valid deadzone so controller packet batching works properly
|
||||
context.triggerDeadzone = Math.max(Math.abs(ltRange.getFlat()), Math.abs(rtRange.getFlat()));
|
||||
mapping.triggerDeadzone = Math.max(Math.abs(ltRange.getFlat()), Math.abs(rtRange.getFlat()));
|
||||
|
||||
// For triggers without (valid) deadzones, we'll use 13% (around XInput's default)
|
||||
if (context.triggerDeadzone < 0.13f ||
|
||||
context.triggerDeadzone > 0.30f)
|
||||
if (mapping.triggerDeadzone < 0.13f ||
|
||||
mapping.triggerDeadzone > 0.30f)
|
||||
{
|
||||
context.triggerDeadzone = 0.13f;
|
||||
mapping.triggerDeadzone = 0.13f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,86 +226,71 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||
boolean[] hasStartKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_MENU, 0);
|
||||
if (!hasStartKey[0] && !hasStartKey[1]) {
|
||||
context.backIsStart = true;
|
||||
context.modeIsSelect = true;
|
||||
mapping.backIsStart = true;
|
||||
}
|
||||
}
|
||||
|
||||
// The ASUS Gamepad has triggers that sit far forward and are prone to false presses
|
||||
// so we increase the deadzone on them to minimize this
|
||||
context.triggerDeadzone = 0.30f;
|
||||
mapping.triggerDeadzone = 0.30f;
|
||||
}
|
||||
// Classify this device as a remote by name
|
||||
else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) {
|
||||
// It's only a remote if it doesn't any sticks
|
||||
if (!context.hasJoystickAxes) {
|
||||
context.isRemote = true;
|
||||
if (!mapping.hasJoystickAxes) {
|
||||
mapping.isRemote = true;
|
||||
}
|
||||
}
|
||||
// NYKO Playpad has a fake hat that mimics the left stick for some reason
|
||||
else if (devName.contains("NYKO PLAYPAD")) {
|
||||
context.hatXAxis = -1;
|
||||
context.hatYAxis = -1;
|
||||
mapping.hatXAxis = -1;
|
||||
mapping.hatYAxis = -1;
|
||||
}
|
||||
}
|
||||
|
||||
LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius);
|
||||
LimeLog.info("Trigger deadzone: "+context.triggerDeadzone);
|
||||
LimeLog.info("Analog stick deadzone: "+mapping.leftStickDeadzoneRadius+" "+mapping.rightStickDeadzoneRadius);
|
||||
LimeLog.info("Trigger deadzone: "+mapping.triggerDeadzone);
|
||||
|
||||
if (devName != null && devName.equals("gpio-keys")) {
|
||||
// This is the back button on Shield portable consoles
|
||||
context.controllerNumber = 0;
|
||||
}
|
||||
else if (multiControllerEnabled) {
|
||||
context.controllerNumber = assignNewControllerNumber();
|
||||
}
|
||||
else {
|
||||
context.controllerNumber = 0;
|
||||
}
|
||||
LimeLog.info("Assigned as controller "+context.controllerNumber);
|
||||
|
||||
return context;
|
||||
return mapping;
|
||||
}
|
||||
|
||||
private ControllerContext getContextForDevice(InputDevice dev) {
|
||||
// Unknown devices use the default context
|
||||
private ControllerMapping getMappingForDevice(InputDevice dev) {
|
||||
// Unknown devices use the default mapping
|
||||
if (dev == null) {
|
||||
return defaultContext;
|
||||
return defaultMapping;
|
||||
}
|
||||
|
||||
String descriptor = dev.getDescriptor();
|
||||
|
||||
// Return the existing context if it exists
|
||||
ControllerContext context = contexts.get(descriptor);
|
||||
if (context != null) {
|
||||
return context;
|
||||
// Return the existing mapping if it exists
|
||||
ControllerMapping mapping = mappings.get(descriptor);
|
||||
if (mapping != null) {
|
||||
return mapping;
|
||||
}
|
||||
|
||||
// Otherwise create a new context
|
||||
context = createContextForDevice(dev);
|
||||
contexts.put(descriptor, context);
|
||||
// Otherwise create a new mapping
|
||||
mapping = createMappingForDevice(dev);
|
||||
mappings.put(descriptor, mapping);
|
||||
|
||||
return context;
|
||||
return mapping;
|
||||
}
|
||||
|
||||
private void sendControllerInputPacket(ControllerContext context) {
|
||||
conn.sendControllerInput(context.controllerNumber, context.inputMap,
|
||||
context.leftTrigger, context.rightTrigger,
|
||||
context.leftStickX, context.leftStickY,
|
||||
context.rightStickX, context.rightStickY);
|
||||
private void sendControllerInputPacket() {
|
||||
conn.sendControllerInput(inputMap, leftTrigger, rightTrigger,
|
||||
leftStickX, leftStickY, rightStickX, rightStickY);
|
||||
}
|
||||
|
||||
// Return a valid keycode, 0 to consume, or -1 to not consume the event
|
||||
// Device MAY BE NULL
|
||||
private int handleRemapping(ControllerContext context, KeyEvent event) {
|
||||
private int handleRemapping(ControllerMapping mapping, KeyEvent event) {
|
||||
// For remotes, don't capture the back button
|
||||
if (context.isRemote) {
|
||||
if (mapping.isRemote) {
|
||||
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (context.isDualShock4) {
|
||||
if (mapping.isDualShock4) {
|
||||
switch (event.getKeyCode()) {
|
||||
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||
return KeyEvent.KEYCODE_BUTTON_L1;
|
||||
@@ -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()) {
|
||||
// These are duplicate dpad events for hat input
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||
@@ -390,9 +340,9 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
else if (context.hatXAxis == -1 &&
|
||||
context.hatYAxis == -1 &&
|
||||
context.isXboxController &&
|
||||
else if (mapping.hatXAxis == -1 &&
|
||||
mapping.hatYAxis == -1 &&
|
||||
mapping.isXboxController &&
|
||||
event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
|
||||
// If there's not a proper Xbox controller mapping, we'll translate the raw d-pad
|
||||
// scan codes into proper key codes
|
||||
@@ -425,20 +375,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
||||
if (keyCode == KeyEvent.KEYCODE_BUTTON_START ||
|
||||
keyCode == KeyEvent.KEYCODE_MENU) {
|
||||
// Ensure that we never use back as start if we have a real start
|
||||
context.backIsStart = false;
|
||||
mapping.backIsStart = false;
|
||||
}
|
||||
else if (keyCode == KeyEvent.KEYCODE_BUTTON_SELECT) {
|
||||
// Don't use mode as select if we have a select
|
||||
context.modeIsSelect = false;
|
||||
}
|
||||
else if (context.backIsStart && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
else if (mapping.backIsStart && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
// Emulate the start button with back
|
||||
return KeyEvent.KEYCODE_BUTTON_START;
|
||||
}
|
||||
else if (context.modeIsSelect && keyCode == KeyEvent.KEYCODE_BUTTON_MODE) {
|
||||
// Emulate the select button with mode
|
||||
return KeyEvent.KEYCODE_BUTTON_SELECT;
|
||||
}
|
||||
|
||||
return keyCode;
|
||||
}
|
||||
@@ -460,101 +402,101 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
||||
// evaluates the deadzone.
|
||||
}
|
||||
|
||||
private void handleAxisSet(ControllerContext context, float lsX, float lsY, float rsX,
|
||||
private void handleAxisSet(ControllerMapping mapping, float lsX, float lsY, float rsX,
|
||||
float rsY, float lt, float rt, float hatX, float hatY) {
|
||||
|
||||
if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
|
||||
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
||||
Vector2d leftStickVector = populateCachedVector(lsX, lsY);
|
||||
|
||||
handleDeadZone(leftStickVector, context.leftStickDeadzoneRadius);
|
||||
handleDeadZone(leftStickVector, mapping.leftStickDeadzoneRadius);
|
||||
|
||||
context.leftStickX = (short) (leftStickVector.getX() * 0x7FFE);
|
||||
context.leftStickY = (short) (-leftStickVector.getY() * 0x7FFE);
|
||||
leftStickX = (short) (leftStickVector.getX() * 0x7FFE);
|
||||
leftStickY = (short) (-leftStickVector.getY() * 0x7FFE);
|
||||
}
|
||||
|
||||
if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
|
||||
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
||||
Vector2d rightStickVector = populateCachedVector(rsX, rsY);
|
||||
|
||||
handleDeadZone(rightStickVector, context.rightStickDeadzoneRadius);
|
||||
handleDeadZone(rightStickVector, mapping.rightStickDeadzoneRadius);
|
||||
|
||||
context.rightStickX = (short) (rightStickVector.getX() * 0x7FFE);
|
||||
context.rightStickY = (short) (-rightStickVector.getY() * 0x7FFE);
|
||||
rightStickX = (short) (rightStickVector.getX() * 0x7FFE);
|
||||
rightStickY = (short) (-rightStickVector.getY() * 0x7FFE);
|
||||
}
|
||||
|
||||
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
|
||||
if (context.triggersIdleNegative) {
|
||||
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
|
||||
if (mapping.triggersIdleNegative) {
|
||||
lt = (lt + 1) / 2;
|
||||
rt = (rt + 1) / 2;
|
||||
}
|
||||
|
||||
if (lt <= context.triggerDeadzone) {
|
||||
if (lt <= mapping.triggerDeadzone) {
|
||||
lt = 0;
|
||||
}
|
||||
if (rt <= context.triggerDeadzone) {
|
||||
if (rt <= mapping.triggerDeadzone) {
|
||||
rt = 0;
|
||||
}
|
||||
|
||||
context.leftTrigger = (byte)(lt * 0xFF);
|
||||
context.rightTrigger = (byte)(rt * 0xFF);
|
||||
leftTrigger = (byte)(lt * 0xFF);
|
||||
rightTrigger = (byte)(rt * 0xFF);
|
||||
}
|
||||
|
||||
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
|
||||
context.inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
|
||||
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
|
||||
inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
|
||||
if (hatX < -0.5) {
|
||||
context.inputMap |= ControllerPacket.LEFT_FLAG;
|
||||
inputMap |= ControllerPacket.LEFT_FLAG;
|
||||
}
|
||||
else if (hatX > 0.5) {
|
||||
context.inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||
inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||
}
|
||||
|
||||
context.inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
|
||||
inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
|
||||
if (hatY < -0.5) {
|
||||
context.inputMap |= ControllerPacket.UP_FLAG;
|
||||
inputMap |= ControllerPacket.UP_FLAG;
|
||||
}
|
||||
else if (hatY > 0.5) {
|
||||
context.inputMap |= ControllerPacket.DOWN_FLAG;
|
||||
inputMap |= ControllerPacket.DOWN_FLAG;
|
||||
}
|
||||
}
|
||||
|
||||
sendControllerInputPacket(context);
|
||||
sendControllerInputPacket();
|
||||
}
|
||||
|
||||
public boolean handleMotionEvent(MotionEvent event) {
|
||||
ControllerContext context = getContextForDevice(event.getDevice());
|
||||
ControllerMapping mapping = getMappingForDevice(event.getDevice());
|
||||
float lsX = 0, lsY = 0, rsX = 0, rsY = 0, rt = 0, lt = 0, hatX = 0, hatY = 0;
|
||||
|
||||
// We purposefully ignore the historical values in the motion event as it makes
|
||||
// the controller feel sluggish for some users.
|
||||
|
||||
if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
|
||||
lsX = event.getAxisValue(context.leftStickXAxis);
|
||||
lsY = event.getAxisValue(context.leftStickYAxis);
|
||||
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
||||
lsX = event.getAxisValue(mapping.leftStickXAxis);
|
||||
lsY = event.getAxisValue(mapping.leftStickYAxis);
|
||||
}
|
||||
|
||||
if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
|
||||
rsX = event.getAxisValue(context.rightStickXAxis);
|
||||
rsY = event.getAxisValue(context.rightStickYAxis);
|
||||
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
||||
rsX = event.getAxisValue(mapping.rightStickXAxis);
|
||||
rsY = event.getAxisValue(mapping.rightStickYAxis);
|
||||
}
|
||||
|
||||
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
|
||||
lt = event.getAxisValue(context.leftTriggerAxis);
|
||||
rt = event.getAxisValue(context.rightTriggerAxis);
|
||||
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
|
||||
lt = event.getAxisValue(mapping.leftTriggerAxis);
|
||||
rt = event.getAxisValue(mapping.rightTriggerAxis);
|
||||
}
|
||||
|
||||
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
|
||||
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
|
||||
hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
|
||||
hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
|
||||
}
|
||||
|
||||
handleAxisSet(context, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY);
|
||||
handleAxisSet(mapping, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean handleButtonUp(KeyEvent event) {
|
||||
ControllerContext context = getContextForDevice(event.getDevice());
|
||||
ControllerMapping mapping = getMappingForDevice(event.getDevice());
|
||||
|
||||
int keyCode = handleRemapping(context, event);
|
||||
int keyCode = handleRemapping(mapping, event);
|
||||
if (keyCode == 0) {
|
||||
return true;
|
||||
}
|
||||
@@ -573,80 +515,78 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
||||
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_BUTTON_MODE:
|
||||
context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_START:
|
||||
case KeyEvent.KEYCODE_MENU:
|
||||
if (SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) {
|
||||
// FIXME: The stock keyboard doesn't have controller focus so isn't usable. I'm not enabling this shortcut
|
||||
// until we have a custom keyboard or some other fix
|
||||
//gestures.showKeyboard();
|
||||
if (SystemClock.uptimeMillis() - startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) {
|
||||
gestures.showKeyboard();
|
||||
}
|
||||
context.inputMap &= ~ControllerPacket.PLAY_FLAG;
|
||||
inputMap &= ~ControllerPacket.PLAY_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BACK:
|
||||
case KeyEvent.KEYCODE_BUTTON_SELECT:
|
||||
context.inputMap &= ~ControllerPacket.BACK_FLAG;
|
||||
inputMap &= ~ControllerPacket.BACK_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||
context.inputMap &= ~ControllerPacket.LEFT_FLAG;
|
||||
inputMap &= ~ControllerPacket.LEFT_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||
context.inputMap &= ~ControllerPacket.RIGHT_FLAG;
|
||||
inputMap &= ~ControllerPacket.RIGHT_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_UP:
|
||||
context.inputMap &= ~ControllerPacket.UP_FLAG;
|
||||
inputMap &= ~ControllerPacket.UP_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||
context.inputMap &= ~ControllerPacket.DOWN_FLAG;
|
||||
inputMap &= ~ControllerPacket.DOWN_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_B:
|
||||
context.inputMap &= ~ControllerPacket.B_FLAG;
|
||||
inputMap &= ~ControllerPacket.B_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_CENTER:
|
||||
case KeyEvent.KEYCODE_BUTTON_A:
|
||||
context.inputMap &= ~ControllerPacket.A_FLAG;
|
||||
inputMap &= ~ControllerPacket.A_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_X:
|
||||
context.inputMap &= ~ControllerPacket.X_FLAG;
|
||||
inputMap &= ~ControllerPacket.X_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||
context.inputMap &= ~ControllerPacket.Y_FLAG;
|
||||
inputMap &= ~ControllerPacket.Y_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_L1:
|
||||
context.inputMap &= ~ControllerPacket.LB_FLAG;
|
||||
context.lastLbUpTime = SystemClock.uptimeMillis();
|
||||
inputMap &= ~ControllerPacket.LB_FLAG;
|
||||
lastLbUpTime = SystemClock.uptimeMillis();
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_R1:
|
||||
context.inputMap &= ~ControllerPacket.RB_FLAG;
|
||||
context.lastRbUpTime = SystemClock.uptimeMillis();
|
||||
inputMap &= ~ControllerPacket.RB_FLAG;
|
||||
lastRbUpTime = SystemClock.uptimeMillis();
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_THUMBL:
|
||||
context.inputMap &= ~ControllerPacket.LS_CLK_FLAG;
|
||||
inputMap &= ~ControllerPacket.LS_CLK_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_THUMBR:
|
||||
context.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
|
||||
inputMap &= ~ControllerPacket.RS_CLK_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_L2:
|
||||
context.leftTrigger = 0;
|
||||
leftTrigger = 0;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_R2:
|
||||
context.rightTrigger = 0;
|
||||
rightTrigger = 0;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we're emulating the select button
|
||||
if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
|
||||
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
|
||||
{
|
||||
// If either start or LB is up, select comes up too
|
||||
if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
|
||||
(context.inputMap & ControllerPacket.LB_FLAG) == 0)
|
||||
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
|
||||
(inputMap & ControllerPacket.LB_FLAG) == 0)
|
||||
{
|
||||
context.inputMap &= ~ControllerPacket.BACK_FLAG;
|
||||
inputMap &= ~ControllerPacket.BACK_FLAG;
|
||||
|
||||
context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT;
|
||||
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT;
|
||||
|
||||
try {
|
||||
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
|
||||
@@ -655,16 +595,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
||||
}
|
||||
|
||||
// Check if we're emulating the special button
|
||||
if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0)
|
||||
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0)
|
||||
{
|
||||
// If either start or select and RB is up, the special button comes up too
|
||||
if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
|
||||
((context.inputMap & ControllerPacket.BACK_FLAG) == 0 &&
|
||||
(context.inputMap & ControllerPacket.RB_FLAG) == 0))
|
||||
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
|
||||
((inputMap & ControllerPacket.BACK_FLAG) == 0 &&
|
||||
(inputMap & ControllerPacket.RB_FLAG) == 0))
|
||||
{
|
||||
context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
|
||||
context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL;
|
||||
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL;
|
||||
|
||||
try {
|
||||
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
|
||||
@@ -672,115 +612,112 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
||||
}
|
||||
}
|
||||
|
||||
sendControllerInputPacket(context);
|
||||
sendControllerInputPacket();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean handleButtonDown(KeyEvent event) {
|
||||
ControllerContext context = getContextForDevice(event.getDevice());
|
||||
ControllerMapping mapping = getMappingForDevice(event.getDevice());
|
||||
|
||||
int keyCode = handleRemapping(context, event);
|
||||
int keyCode = handleRemapping(mapping, event);
|
||||
if (keyCode == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_BUTTON_MODE:
|
||||
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_START:
|
||||
case KeyEvent.KEYCODE_MENU:
|
||||
if (event.getRepeatCount() == 0) {
|
||||
context.startDownTime = SystemClock.uptimeMillis();
|
||||
startDownTime = SystemClock.uptimeMillis();
|
||||
}
|
||||
context.inputMap |= ControllerPacket.PLAY_FLAG;
|
||||
inputMap |= ControllerPacket.PLAY_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BACK:
|
||||
case KeyEvent.KEYCODE_BUTTON_SELECT:
|
||||
context.inputMap |= ControllerPacket.BACK_FLAG;
|
||||
inputMap |= ControllerPacket.BACK_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||
context.inputMap |= ControllerPacket.LEFT_FLAG;
|
||||
inputMap |= ControllerPacket.LEFT_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||
context.inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||
inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_UP:
|
||||
context.inputMap |= ControllerPacket.UP_FLAG;
|
||||
inputMap |= ControllerPacket.UP_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||
context.inputMap |= ControllerPacket.DOWN_FLAG;
|
||||
inputMap |= ControllerPacket.DOWN_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_B:
|
||||
context.inputMap |= ControllerPacket.B_FLAG;
|
||||
inputMap |= ControllerPacket.B_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_CENTER:
|
||||
case KeyEvent.KEYCODE_BUTTON_A:
|
||||
context.inputMap |= ControllerPacket.A_FLAG;
|
||||
inputMap |= ControllerPacket.A_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_X:
|
||||
context.inputMap |= ControllerPacket.X_FLAG;
|
||||
inputMap |= ControllerPacket.X_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||
context.inputMap |= ControllerPacket.Y_FLAG;
|
||||
inputMap |= ControllerPacket.Y_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_L1:
|
||||
context.inputMap |= ControllerPacket.LB_FLAG;
|
||||
inputMap |= ControllerPacket.LB_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_R1:
|
||||
context.inputMap |= ControllerPacket.RB_FLAG;
|
||||
inputMap |= ControllerPacket.RB_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_THUMBL:
|
||||
context.inputMap |= ControllerPacket.LS_CLK_FLAG;
|
||||
inputMap |= ControllerPacket.LS_CLK_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_THUMBR:
|
||||
context.inputMap |= ControllerPacket.RS_CLK_FLAG;
|
||||
inputMap |= ControllerPacket.RS_CLK_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_L2:
|
||||
context.leftTrigger = (byte)0xFF;
|
||||
leftTrigger = (byte)0xFF;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_R2:
|
||||
context.rightTrigger = (byte)0xFF;
|
||||
rightTrigger = (byte)0xFF;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start+LB acts like select for controllers with one button
|
||||
if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
|
||||
((context.inputMap & ControllerPacket.LB_FLAG) != 0 ||
|
||||
SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
|
||||
if ((inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
|
||||
((inputMap & ControllerPacket.LB_FLAG) != 0 ||
|
||||
SystemClock.uptimeMillis() - lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
|
||||
{
|
||||
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
|
||||
context.inputMap |= ControllerPacket.BACK_FLAG;
|
||||
inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
|
||||
inputMap |= ControllerPacket.BACK_FLAG;
|
||||
|
||||
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
|
||||
emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
|
||||
}
|
||||
|
||||
// We detect select+start or start+RB as the special button combo
|
||||
if (((context.inputMap & ControllerPacket.RB_FLAG) != 0 ||
|
||||
(SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
|
||||
(context.inputMap & ControllerPacket.BACK_FLAG) != 0) &&
|
||||
(context.inputMap & ControllerPacket.PLAY_FLAG) != 0)
|
||||
if (((inputMap & ControllerPacket.RB_FLAG) != 0 ||
|
||||
(SystemClock.uptimeMillis() - lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
|
||||
(inputMap & ControllerPacket.BACK_FLAG) != 0) &&
|
||||
(inputMap & ControllerPacket.PLAY_FLAG) != 0)
|
||||
{
|
||||
context.inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
|
||||
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
|
||||
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
|
||||
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
||||
emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
||||
}
|
||||
|
||||
// Send a new input packet if this is the first instance of a button down event
|
||||
// or anytime if we're emulating a button
|
||||
if (event.getRepeatCount() == 0 || context.emulatingButtonFlags != 0) {
|
||||
sendControllerInputPacket(context);
|
||||
if (event.getRepeatCount() == 0 || emulatingButtonFlags != 0) {
|
||||
sendControllerInputPacket();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
class ControllerContext {
|
||||
public String name;
|
||||
public int id;
|
||||
|
||||
class ControllerMapping {
|
||||
public int leftStickXAxis = -1;
|
||||
public int leftStickYAxis = -1;
|
||||
public float leftStickDeadzoneRadius;
|
||||
@@ -800,29 +737,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
|
||||
public boolean isDualShock4;
|
||||
public boolean isXboxController;
|
||||
public boolean backIsStart;
|
||||
public boolean modeIsSelect;
|
||||
public boolean isRemote;
|
||||
public boolean hasJoystickAxes;
|
||||
|
||||
public short controllerNumber;
|
||||
|
||||
public short inputMap = 0x0000;
|
||||
public byte leftTrigger = 0x00;
|
||||
public byte rightTrigger = 0x00;
|
||||
public short rightStickX = 0x0000;
|
||||
public short rightStickY = 0x0000;
|
||||
public short leftStickX = 0x0000;
|
||||
public short leftStickY = 0x0000;
|
||||
public int emulatingButtonFlags = 0;
|
||||
|
||||
// Used for OUYA bumper state tracking since they force all buttons
|
||||
// up when the OUYA button goes down. We watch the last time we get
|
||||
// a bumper up and compare that to our maximum delay when we receive
|
||||
// a Start button press to see if we should activate one of our
|
||||
// emulated button combos.
|
||||
public long lastLbUpTime = 0;
|
||||
public long lastRbUpTime = 0;
|
||||
|
||||
public long startDownTime = 0;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
||||
/**
|
||||
* GFE's prefix for every key code
|
||||
*/
|
||||
private static final short KEY_PREFIX = (short) 0x80;
|
||||
public static final short KEY_PREFIX = (short) 0x80;
|
||||
|
||||
public static final int VK_0 = 48;
|
||||
public static final int VK_9 = 57;
|
||||
|
||||
@@ -11,10 +11,9 @@ public class TouchContext {
|
||||
private long originalTouchTime = 0;
|
||||
private boolean cancelled;
|
||||
|
||||
private final NvConnection conn;
|
||||
private final int actionIndex;
|
||||
private final double xFactor;
|
||||
private final double yFactor;
|
||||
private NvConnection conn;
|
||||
private int actionIndex;
|
||||
private double xFactor, yFactor;
|
||||
|
||||
private static final int TAP_MOVEMENT_THRESHOLD = 10;
|
||||
private static final int TAP_TIME_THRESHOLD = 250;
|
||||
|
||||
@@ -29,9 +29,9 @@ public class EvdevEvent {
|
||||
/* Keys */
|
||||
public static final short KEY_Q = 16;
|
||||
|
||||
public final short type;
|
||||
public final short code;
|
||||
public final int value;
|
||||
public short type;
|
||||
public short code;
|
||||
public int value;
|
||||
|
||||
public EvdevEvent(short type, short code, int value) {
|
||||
this.type = type;
|
||||
|
||||
@@ -7,12 +7,12 @@ import com.limelight.LimeLog;
|
||||
|
||||
public class EvdevHandler {
|
||||
|
||||
private final String absolutePath;
|
||||
private final EvdevListener listener;
|
||||
private String absolutePath;
|
||||
private EvdevListener listener;
|
||||
private boolean shutdown = false;
|
||||
private int fd = -1;
|
||||
|
||||
private final Thread handlerThread = new Thread() {
|
||||
private Thread handlerThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// All the finally blocks here make this code look like a mess
|
||||
|
||||
@@ -26,7 +26,7 @@ public class EvdevReader {
|
||||
// 4.4 and later to do live SELinux policy changes.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
EvdevShell shell = EvdevShell.getInstance();
|
||||
shell.runCommand("supolicy --live \"allow untrusted_app input_device dir { open getattr read search }\" " +
|
||||
shell.runCommand("supolicy --live \"allow untrusted_app input_device dir { getattr read search }\" " +
|
||||
"\"allow untrusted_app input_device chr_file { open read write ioctl }\"");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.view.KeyEvent;
|
||||
|
||||
public class EvdevTranslator {
|
||||
|
||||
private static final short[] EVDEV_KEY_CODES = {
|
||||
public static final short EVDEV_KEY_CODES[] = {
|
||||
0, //KeyEvent.VK_RESERVED
|
||||
KeyEvent.KEYCODE_ESCAPE,
|
||||
KeyEvent.KEYCODE_1,
|
||||
|
||||
@@ -36,7 +36,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
|
||||
private int totalFrames;
|
||||
private long totalTimeMs;
|
||||
|
||||
private final int cpuCount = Runtime.getRuntime().availableProcessors();
|
||||
private int cpuCount = Runtime.getRuntime().availableProcessors();
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private int findOptimalPerformanceLevel() {
|
||||
@@ -178,6 +178,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
|
||||
@Override
|
||||
public void run() {
|
||||
long nextFrameTime = System.currentTimeMillis();
|
||||
DecodeUnit du;
|
||||
while (!isInterrupted())
|
||||
{
|
||||
long diff = nextFrameTime - System.currentTimeMillis();
|
||||
|
||||
@@ -558,8 +558,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
||||
public class RendererException extends RuntimeException {
|
||||
private static final long serialVersionUID = 8985937536997012406L;
|
||||
|
||||
private final Exception originalException;
|
||||
private final MediaCodecDecoderRenderer renderer;
|
||||
private Exception originalException;
|
||||
private MediaCodecDecoderRenderer renderer;
|
||||
private ByteBuffer currentBuffer;
|
||||
private int currentCodecFlags;
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@ import com.limelight.LimeLog;
|
||||
|
||||
public class MediaCodecHelper {
|
||||
|
||||
private static final List<String> preferredDecoders;
|
||||
public static final List<String> preferredDecoders;
|
||||
|
||||
private static final List<String> blacklistedDecoderPrefixes;
|
||||
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
||||
private static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
||||
private static final List<String> baselineProfileHackPrefixes;
|
||||
public static final List<String> blacklistedDecoderPrefixes;
|
||||
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
||||
public static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
||||
public static final List<String> baselineProfileHackPrefixes;
|
||||
|
||||
static {
|
||||
preferredDecoders = new LinkedList<String>();
|
||||
@@ -146,7 +146,7 @@ public class MediaCodecHelper {
|
||||
return str;
|
||||
}
|
||||
|
||||
private static MediaCodecInfo findPreferredDecoder() {
|
||||
public static MediaCodecInfo findPreferredDecoder() {
|
||||
// This is a different algorithm than the other findXXXDecoder functions,
|
||||
// because we want to evaluate the decoders in our list's order
|
||||
// rather than MediaCodecList's order
|
||||
@@ -217,7 +217,7 @@ public class MediaCodecHelper {
|
||||
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
|
||||
// and we want to be sure all callers are handling this possibility
|
||||
@SuppressWarnings("RedundantThrows")
|
||||
private static MediaCodecInfo findKnownSafeDecoder() throws Exception {
|
||||
public static MediaCodecInfo findKnownSafeDecoder() throws Exception {
|
||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||
// Skip encoders
|
||||
if (codecInfo.isEncoder()) {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package com.limelight.computers;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.StringReader;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -33,18 +32,16 @@ import org.xmlpull.v1.XmlPullParserException;
|
||||
public class ComputerManagerService extends Service {
|
||||
private static final int POLLING_PERIOD_MS = 3000;
|
||||
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||
private static final int FAST_POLL_TIMEOUT = 500;
|
||||
private static final int OFFLINE_POLL_TRIES = 3;
|
||||
|
||||
private final ComputerManagerBinder binder = new ComputerManagerBinder();
|
||||
private ComputerManagerBinder binder = new ComputerManagerBinder();
|
||||
|
||||
private ComputerDatabaseManager dbManager;
|
||||
private final AtomicInteger dbRefCount = new AtomicInteger(0);
|
||||
private AtomicInteger dbRefCount = new AtomicInteger(0);
|
||||
|
||||
private IdentityManager idManager;
|
||||
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
|
||||
private ComputerManagerListener listener = null;
|
||||
private final AtomicInteger activePolls = new AtomicInteger(0);
|
||||
private AtomicInteger activePolls = new AtomicInteger(0);
|
||||
private boolean pollingActive = false;
|
||||
|
||||
private DiscoveryService.DiscoveryBinder discoveryBinder;
|
||||
@@ -68,7 +65,10 @@ public class ComputerManagerService extends Service {
|
||||
};
|
||||
|
||||
// Returns true if the details object was modified
|
||||
private boolean runPoll(ComputerDetails details, boolean newPc, int offlineCount) throws InterruptedException {
|
||||
private boolean runPoll(ComputerDetails details)
|
||||
{
|
||||
boolean newPc = details.name.isEmpty();
|
||||
|
||||
if (!getLocalDatabaseReference()) {
|
||||
return false;
|
||||
}
|
||||
@@ -76,22 +76,12 @@ public class ComputerManagerService extends Service {
|
||||
activePolls.incrementAndGet();
|
||||
|
||||
// Poll the machine
|
||||
try {
|
||||
if (!pollComputer(details)) {
|
||||
if (!newPc && offlineCount < OFFLINE_POLL_TRIES) {
|
||||
// Return without calling the listener
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!doPollMachine(details)) {
|
||||
details.state = ComputerDetails.State.OFFLINE;
|
||||
details.reachability = ComputerDetails.Reachability.OFFLINE;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
releaseLocalDatabaseReference();
|
||||
throw e;
|
||||
} finally {
|
||||
|
||||
activePolls.decrementAndGet();
|
||||
}
|
||||
|
||||
// If it's online, update our persistent state
|
||||
if (details.state == ComputerDetails.State.ONLINE) {
|
||||
@@ -101,7 +91,7 @@ public class ComputerManagerService extends Service {
|
||||
if (dbManager.getComputerByName(details.name) == null) {
|
||||
// It's gone
|
||||
releaseLocalDatabaseReference();
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,21 +111,13 @@ public class ComputerManagerService extends Service {
|
||||
Thread t = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
int offlineCount = 0;
|
||||
while (!isInterrupted() && pollingActive) {
|
||||
try {
|
||||
// Check if this poll has modified the details
|
||||
if (!runPoll(details, false, offlineCount)) {
|
||||
LimeLog.warning(details.name + " is offline (try " + offlineCount + ")");
|
||||
offlineCount++;
|
||||
}
|
||||
else {
|
||||
offlineCount = 0;
|
||||
}
|
||||
runPoll(details);
|
||||
|
||||
// Wait until the next polling interval
|
||||
Thread.sleep(POLLING_PERIOD_MS / ((offlineCount > 0) ? 2 : 1));
|
||||
try {
|
||||
Thread.sleep(POLLING_PERIOD_MS);
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
@@ -195,6 +177,10 @@ public class ComputerManagerService extends Service {
|
||||
return ComputerManagerService.this.addComputerBlocking(addr);
|
||||
}
|
||||
|
||||
public void addComputer(InetAddress addr) {
|
||||
ComputerManagerService.this.addComputer(addr);
|
||||
}
|
||||
|
||||
public void removeComputer(String name) {
|
||||
ComputerManagerService.this.removeComputer(name);
|
||||
}
|
||||
@@ -253,7 +239,7 @@ public class ComputerManagerService extends Service {
|
||||
@Override
|
||||
public void notifyComputerAdded(MdnsComputer computer) {
|
||||
// Kick off a serverinfo poll on this machine
|
||||
addComputerBlocking(computer.getAddress());
|
||||
addComputer(computer.getAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -269,11 +255,28 @@ public class ComputerManagerService extends Service {
|
||||
};
|
||||
}
|
||||
|
||||
public void addComputer(InetAddress addr) {
|
||||
// Setup a placeholder
|
||||
ComputerDetails fakeDetails = new ComputerDetails();
|
||||
fakeDetails.localIp = addr;
|
||||
fakeDetails.remoteIp = addr;
|
||||
fakeDetails.name = "";
|
||||
|
||||
addTuple(fakeDetails);
|
||||
}
|
||||
|
||||
private void addTuple(ComputerDetails details) {
|
||||
synchronized (pollingTuples) {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
// Check if this is the same computer
|
||||
if (tuple.computer.uuid.equals(details.uuid)) {
|
||||
if (tuple.computer == details ||
|
||||
// If there's no name on one of these computers, compare with the local IP
|
||||
((details.name.isEmpty() || tuple.computer.name.isEmpty()) &&
|
||||
tuple.computer.localIp.equals(details.localIp)) ||
|
||||
// If there is a name on both computers, compare with name
|
||||
((!details.name.isEmpty() && !tuple.computer.name.isEmpty()) &&
|
||||
tuple.computer.name.equals(details.name))) {
|
||||
|
||||
// Update details anyway in case this machine has been re-added by IP
|
||||
// after not being reachable by our existing information
|
||||
tuple.computer.localIp = details.localIp;
|
||||
@@ -304,18 +307,13 @@ public class ComputerManagerService extends Service {
|
||||
ComputerDetails fakeDetails = new ComputerDetails();
|
||||
fakeDetails.localIp = addr;
|
||||
fakeDetails.remoteIp = addr;
|
||||
fakeDetails.name = "";
|
||||
|
||||
// Block while we try to fill the details
|
||||
try {
|
||||
runPoll(fakeDetails, true, 0);
|
||||
} catch (InterruptedException e) {
|
||||
return false;
|
||||
}
|
||||
runPoll(fakeDetails);
|
||||
|
||||
// If the machine is reachable, it was successful
|
||||
if (fakeDetails.state == ComputerDetails.State.ONLINE) {
|
||||
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
|
||||
|
||||
// Start a polling thread for this machine
|
||||
addTuple(fakeDetails);
|
||||
return true;
|
||||
@@ -386,91 +384,14 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
// Just try to establish a TCP connection to speculatively detect a running
|
||||
// GFE server
|
||||
private boolean fastPollIp(InetAddress addr) {
|
||||
Socket s = new Socket();
|
||||
try {
|
||||
s.connect(new InetSocketAddress(addr, NvHTTP.PORT), FAST_POLL_TIMEOUT);
|
||||
s.close();
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void startFastPollThread(final InetAddress addr, final boolean[] info) {
|
||||
Thread t = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
boolean pollRes = fastPollIp(addr);
|
||||
|
||||
synchronized (info) {
|
||||
info[0] = true; // Done
|
||||
info[1] = pollRes; // Polling result
|
||||
|
||||
info.notify();
|
||||
}
|
||||
}
|
||||
};
|
||||
t.setName("Fast Poll - "+addr.getHostAddress());
|
||||
t.start();
|
||||
}
|
||||
|
||||
private ComputerDetails.Reachability fastPollPc(final InetAddress local, final InetAddress remote) throws InterruptedException {
|
||||
final boolean[] remoteInfo = new boolean[2];
|
||||
final boolean[] localInfo = new boolean[2];
|
||||
|
||||
startFastPollThread(local, localInfo);
|
||||
startFastPollThread(remote, remoteInfo);
|
||||
|
||||
// Check local first
|
||||
synchronized (localInfo) {
|
||||
while (!localInfo[0]) {
|
||||
localInfo.wait(500);
|
||||
}
|
||||
|
||||
if (localInfo[1]) {
|
||||
return ComputerDetails.Reachability.LOCAL;
|
||||
}
|
||||
}
|
||||
|
||||
// Now remote
|
||||
synchronized (remoteInfo) {
|
||||
while (!remoteInfo[0]) {
|
||||
remoteInfo.wait(500);
|
||||
}
|
||||
|
||||
if (remoteInfo[1]) {
|
||||
return ComputerDetails.Reachability.REMOTE;
|
||||
}
|
||||
}
|
||||
|
||||
return ComputerDetails.Reachability.OFFLINE;
|
||||
}
|
||||
|
||||
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
|
||||
private boolean pollComputer(ComputerDetails details, boolean localFirst) {
|
||||
ComputerDetails polledDetails;
|
||||
ComputerDetails.Reachability reachability;
|
||||
|
||||
// If the local address is routable across the Internet,
|
||||
// always consider this PC remote to be conservative
|
||||
if (details.localIp.equals(details.remoteIp)) {
|
||||
reachability = ComputerDetails.Reachability.REMOTE;
|
||||
localFirst = false;
|
||||
}
|
||||
else {
|
||||
// Do a TCP-level connection to the HTTP server to see if it's listening
|
||||
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localIp+", "+details.remoteIp+")");
|
||||
reachability = fastPollPc(details.localIp, details.remoteIp);
|
||||
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
|
||||
}
|
||||
|
||||
// If no connection could be established to either IP address, there's nothing we can do
|
||||
if (reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
|
||||
|
||||
if (localFirst) {
|
||||
polledDetails = tryPollIp(details, details.localIp);
|
||||
@@ -504,19 +425,24 @@ public class ComputerManagerService extends Service {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save the old MAC address
|
||||
String savedMacAddress = details.macAddress;
|
||||
|
||||
// If we got here, it's reachable
|
||||
details.update(polledDetails);
|
||||
|
||||
// If the new MAC address is empty, restore the old one (workaround for GFE bug)
|
||||
if (details.macAddress.equals("00:00:00:00:00:00") && savedMacAddress != null) {
|
||||
LimeLog.info("MAC address was empty; using existing value: "+savedMacAddress);
|
||||
details.macAddress = savedMacAddress;
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
private boolean doPollMachine(ComputerDetails details) {
|
||||
if (details.reachability == ComputerDetails.Reachability.UNKNOWN ||
|
||||
details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||
// Always try local first to avoid potential UDP issues when
|
||||
// attempting to stream via the router's external IP address
|
||||
// behind its NAT
|
||||
return pollComputer(details, true);
|
||||
}
|
||||
else {
|
||||
// If we're already reached a machine via a particular IP address,
|
||||
// always try that one first
|
||||
return pollComputer(details, details.reachability == ComputerDetails.Reachability.LOCAL);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -565,8 +491,8 @@ public class ComputerManagerService extends Service {
|
||||
|
||||
public class ApplistPoller {
|
||||
private Thread thread;
|
||||
private final ComputerDetails computer;
|
||||
private final Object pollEvent = new Object();
|
||||
private ComputerDetails computer;
|
||||
private Object pollEvent = new Object();
|
||||
|
||||
public ApplistPoller(ComputerDetails computer) {
|
||||
this.computer = computer;
|
||||
@@ -626,19 +552,9 @@ public class ComputerManagerService extends Service {
|
||||
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
|
||||
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
|
||||
// Open the cache file
|
||||
OutputStream cacheOut = null;
|
||||
try {
|
||||
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
|
||||
FileOutputStream cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
|
||||
CacheHelper.writeStringToOutputStream(cacheOut, appList);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
if (cacheOut != null) {
|
||||
cacheOut.close();
|
||||
}
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
|
||||
// Update the computer
|
||||
computer.rawAppList = appList;
|
||||
@@ -677,7 +593,7 @@ public class ComputerManagerService extends Service {
|
||||
|
||||
class PollingTuple {
|
||||
public Thread thread;
|
||||
public final ComputerDetails computer;
|
||||
public ComputerDetails computer;
|
||||
|
||||
public PollingTuple(ComputerDetails computer, Thread thread) {
|
||||
this.computer = computer;
|
||||
|
||||
@@ -70,7 +70,7 @@ public class DiscoveryService extends Service {
|
||||
});
|
||||
}
|
||||
|
||||
private final DiscoveryBinder binder = new DiscoveryBinder();
|
||||
private DiscoveryBinder binder = new DiscoveryBinder();
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
|
||||
@@ -1,84 +1,118 @@
|
||||
package com.limelight.grid;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.AsyncTask;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.koushikdutta.async.future.FutureCallback;
|
||||
import com.koushikdutta.ion.ImageViewBitmapInfo;
|
||||
import com.koushikdutta.ion.Ion;
|
||||
import com.limelight.AppView;
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.R;
|
||||
import com.limelight.grid.assets.CachedAppAssetLoader;
|
||||
import com.limelight.grid.assets.DiskAssetLoader;
|
||||
import com.limelight.grid.assets.MemoryAssetLoader;
|
||||
import com.limelight.grid.assets.NetworkAssetLoader;
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||
import com.limelight.utils.CacheHelper;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collection;
|
||||
import java.security.Principal;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509KeyManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
private final Activity activity;
|
||||
|
||||
private static final int ART_WIDTH_PX = 300;
|
||||
private static final int SMALL_WIDTH_DP = 100;
|
||||
private static final int LARGE_WIDTH_DP = 150;
|
||||
private ComputerDetails computer;
|
||||
private String uniqueId;
|
||||
private LimelightCryptoProvider cryptoProvider;
|
||||
private SSLContext sslContext;
|
||||
private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>();
|
||||
|
||||
private final CachedAppAssetLoader loader;
|
||||
private final ConcurrentHashMap<WeakReference<ImageView>, CachedAppAssetLoader.LoaderTuple> loadingTuples = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Object, CachedAppAssetLoader.LoaderTuple> backgroundLoadingTuples = new ConcurrentHashMap<>();
|
||||
public AppGridAdapter(Context context, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException {
|
||||
super(context, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
|
||||
|
||||
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws KeyManagementException, NoSuchAlgorithmException {
|
||||
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
|
||||
this.computer = computer;
|
||||
this.uniqueId = uniqueId;
|
||||
|
||||
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
|
||||
int dp;
|
||||
cryptoProvider = PlatformBinding.getCryptoProvider(context);
|
||||
|
||||
if (small) {
|
||||
dp = SMALL_WIDTH_DP;
|
||||
}
|
||||
else {
|
||||
dp = LARGE_WIDTH_DP;
|
||||
sslContext = SSLContext.getInstance("SSL");
|
||||
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom());
|
||||
}
|
||||
|
||||
double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160));
|
||||
if (scalingDivisor < 1.0) {
|
||||
// We don't want to make them bigger before draw-time
|
||||
scalingDivisor = 1.0;
|
||||
TrustManager[] trustAllCerts = new TrustManager[] {
|
||||
new X509TrustManager() {
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
LimeLog.info("Art scaling divisor: " + scalingDivisor);
|
||||
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
|
||||
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
|
||||
}};
|
||||
|
||||
this.activity = activity;
|
||||
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
||||
new NetworkAssetLoader(context, uniqueId),
|
||||
new MemoryAssetLoader(), new DiskAssetLoader(context.getCacheDir()));
|
||||
KeyManager[] ourKeyman = new KeyManager[] {
|
||||
new X509KeyManager() {
|
||||
public String chooseClientAlias(String[] keyTypes,
|
||||
Principal[] issuers, Socket socket) {
|
||||
return "Limelight-RSA";
|
||||
}
|
||||
|
||||
private static void cancelTuples(ConcurrentHashMap<?, CachedAppAssetLoader.LoaderTuple> map) {
|
||||
Collection<CachedAppAssetLoader.LoaderTuple> tuples = map.values();
|
||||
|
||||
for (CachedAppAssetLoader.LoaderTuple tuple : tuples) {
|
||||
tuple.cancel();
|
||||
public String chooseServerAlias(String keyType, Principal[] issuers,
|
||||
Socket socket) {
|
||||
return null;
|
||||
}
|
||||
|
||||
map.clear();
|
||||
public X509Certificate[] getCertificateChain(String alias) {
|
||||
return new X509Certificate[] {cryptoProvider.getClientCertificate()};
|
||||
}
|
||||
|
||||
public void cancelQueuedOperations() {
|
||||
cancelTuples(loadingTuples);
|
||||
cancelTuples(backgroundLoadingTuples);
|
||||
|
||||
loader.freeCacheMemory();
|
||||
public String[] getClientAliases(String keyType, Principal[] issuers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public PrivateKey getPrivateKey(String alias) {
|
||||
return cryptoProvider.getClientPrivateKey();
|
||||
}
|
||||
|
||||
public String[] getServerAliases(String keyType, Principal[] issuers) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Ignore differences between given hostname and certificate hostname
|
||||
HostnameVerifier hv = new HostnameVerifier() {
|
||||
public boolean verify(String hostname, SSLSession session) { return true; }
|
||||
};
|
||||
|
||||
private void sortList() {
|
||||
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
||||
@Override
|
||||
@@ -88,117 +122,62 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
});
|
||||
}
|
||||
|
||||
public void addApp(AppView.AppObject app) {
|
||||
// Queue a request to fetch this bitmap in the background
|
||||
Object tupleKey = new Object();
|
||||
CachedAppAssetLoader.LoaderTuple tuple =
|
||||
loader.loadBitmapWithContextInBackground(app.app, tupleKey, backgroundLoadListener);
|
||||
if (tuple != null) {
|
||||
backgroundLoadingTuples.put(tupleKey, tuple);
|
||||
private InetAddress getCurrentAddress() {
|
||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||
return computer.localIp;
|
||||
}
|
||||
else {
|
||||
return computer.remoteIp;
|
||||
}
|
||||
}
|
||||
|
||||
public void addApp(AppView.AppObject app) {
|
||||
itemList.add(app);
|
||||
sortList();
|
||||
}
|
||||
|
||||
public void removeApp(AppView.AppObject app) {
|
||||
itemList.remove(app);
|
||||
public void abortPendingRequests() {
|
||||
HashMap<ImageView, Future> tempMap;
|
||||
|
||||
synchronized (pendingRequests) {
|
||||
// Copy the pending requests under a lock
|
||||
tempMap = new HashMap<ImageView, Future>(pendingRequests);
|
||||
}
|
||||
|
||||
private final CachedAppAssetLoader.LoadListener imageViewLoadListener = new CachedAppAssetLoader.LoadListener() {
|
||||
@Override
|
||||
public void notifyLongLoad(Object object) {
|
||||
final WeakReference<ImageView> viewRef = (WeakReference<ImageView>) object;
|
||||
|
||||
// If the view isn't there anymore, don't bother scheduling on the UI thread
|
||||
if (viewRef.get() == null) {
|
||||
return;
|
||||
for (Future f : tempMap.values()) {
|
||||
if (!f.isCancelled() && !f.isDone()) {
|
||||
f.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ImageView view = viewRef.get();
|
||||
if (view != null) {
|
||||
view.setImageResource(R.drawable.image_loading);
|
||||
fadeInImage(view);
|
||||
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 void notifyLoadComplete(Object object, final Bitmap bitmap) {
|
||||
final WeakReference<ImageView> viewRef = (WeakReference<ImageView>) object;
|
||||
|
||||
loadingTuples.remove(viewRef);
|
||||
|
||||
// Just leave the loading icon in place
|
||||
if (bitmap == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the view isn't there anymore, don't bother scheduling on the UI thread
|
||||
if (viewRef.get() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ImageView view = viewRef.get();
|
||||
if (view != null) {
|
||||
view.setImageBitmap(bitmap);
|
||||
fadeInImage(view);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private final CachedAppAssetLoader.LoadListener backgroundLoadListener = new CachedAppAssetLoader.LoadListener() {
|
||||
@Override
|
||||
public void notifyLongLoad(Object object) {}
|
||||
|
||||
@Override
|
||||
public void notifyLoadComplete(Object object, final Bitmap bitmap) {
|
||||
backgroundLoadingTuples.remove(object);
|
||||
}
|
||||
};
|
||||
|
||||
private void reapLoaderTuples(ImageView view) {
|
||||
// Poor HashMap doesn't deserve this...
|
||||
Iterator<Map.Entry<WeakReference<ImageView>, CachedAppAssetLoader.LoaderTuple>> i = loadingTuples.entrySet().iterator();
|
||||
while (i.hasNext()) {
|
||||
Map.Entry<WeakReference<ImageView>, CachedAppAssetLoader.LoaderTuple> entry = i.next();
|
||||
ImageView imageView = entry.getKey().get();
|
||||
|
||||
// Remove tuples that refer to this view or no view
|
||||
if (imageView == null || imageView == view) {
|
||||
// FIXME: There's a small chance that this can race if we've already gone down
|
||||
// the path to notification but haven't been notified yet
|
||||
entry.getValue().cancel();
|
||||
|
||||
// Remove it from the tuple list
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) {
|
||||
// Cancel pending loads on this image view
|
||||
reapLoaderTuples(imgView);
|
||||
// Hide the image view while we're loading the image from disk cache
|
||||
imgView.setVisibility(View.INVISIBLE);
|
||||
|
||||
// Clear existing contents of the image view
|
||||
imgView.setAlpha(0.0f);
|
||||
// Check the on-disk cache
|
||||
new ImageCacheRequest(imgView, obj.app.getAppId()).execute();
|
||||
|
||||
// Start loading the bitmap
|
||||
WeakReference<ImageView> viewRef = new WeakReference<>(imgView);
|
||||
CachedAppAssetLoader.LoaderTuple tuple = loader.loadBitmapWithContext(obj.app, viewRef, imageViewLoadListener);
|
||||
if (tuple != null) {
|
||||
// The load was issued asynchronously
|
||||
loadingTuples.put(viewRef, tuple);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -223,7 +202,83 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void fadeInImage(ImageView view) {
|
||||
view.animate().alpha(1.0f).setDuration(100).start();
|
||||
private class ImageCacheRequest extends AsyncTask<Void, Void, Bitmap> {
|
||||
private ImageView view;
|
||||
private int appId;
|
||||
|
||||
public ImageCacheRequest(ImageView view, int appId) {
|
||||
this.view = view;
|
||||
this.appId = appId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitmap doInBackground(Void... v) {
|
||||
InputStream in = null;
|
||||
try {
|
||||
in = CacheHelper.openCacheFileForInput(context.getCacheDir(), "boxart", computer.uuid.toString(), appId + ".png");
|
||||
return BitmapFactory.decodeStream(in);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (in != null) {
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Bitmap result) {
|
||||
if (result != null) {
|
||||
// Disk cache was read successfully
|
||||
LimeLog.info("Image disk cache hit for ("+computer.uuid+", "+appId+")");
|
||||
view.setImageBitmap(result);
|
||||
view.setVisibility(View.VISIBLE);
|
||||
}
|
||||
else {
|
||||
LimeLog.info("Image disk cache miss for ("+computer.uuid+", "+appId+")");
|
||||
LimeLog.info("Requesting: "+"https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
|
||||
appId + "&AssetType=2&AssetIdx=0");
|
||||
|
||||
// Load the placeholder image
|
||||
view.setImageResource(defaultImageRes);
|
||||
view.setVisibility(View.VISIBLE);
|
||||
|
||||
// Set SSL contexts correctly to allow us to authenticate
|
||||
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts);
|
||||
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext);
|
||||
|
||||
// Kick off the deferred image load
|
||||
synchronized (pendingRequests) {
|
||||
Future<Bitmap> f = Ion.with(context)
|
||||
.load("https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
|
||||
appId + "&AssetType=2&AssetIdx=0")
|
||||
.asBitmap()
|
||||
.setCallback(new FutureCallback<Bitmap>() {
|
||||
@Override
|
||||
public void onCompleted(Exception e, Bitmap result) {
|
||||
synchronized (pendingRequests) {
|
||||
pendingRequests.remove(view);
|
||||
}
|
||||
|
||||
if (result != null) {
|
||||
// Make the view visible now
|
||||
view.setImageBitmap(result);
|
||||
view.setVisibility(View.VISIBLE);
|
||||
|
||||
// Populate the disk cache if we got an image back
|
||||
populateBitmapCache(computer.uuid, appId, result);
|
||||
}
|
||||
else {
|
||||
// Leave the loading icon as is (probably should change this eventually...)
|
||||
}
|
||||
}
|
||||
});
|
||||
pendingRequests.put(view, f);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -13,11 +13,11 @@ import com.limelight.R;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
||||
protected final Context context;
|
||||
protected final int defaultImageRes;
|
||||
protected final int layoutId;
|
||||
protected final ArrayList<T> itemList = new ArrayList<T>();
|
||||
protected final LayoutInflater inflater;
|
||||
protected Context context;
|
||||
protected int defaultImageRes;
|
||||
protected int layoutId;
|
||||
protected ArrayList<T> itemList = new ArrayList<T>();
|
||||
protected LayoutInflater inflater;
|
||||
|
||||
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
|
||||
this.context = context;
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
package com.limelight.grid.assets;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class CachedAppAssetLoader {
|
||||
private final ComputerDetails computer;
|
||||
private final double scalingDivider;
|
||||
private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(8, 8, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>());
|
||||
private final ThreadPoolExecutor backgroundExecutor = new ThreadPoolExecutor(2, 2, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>());
|
||||
private final NetworkAssetLoader networkLoader;
|
||||
private final MemoryAssetLoader memoryLoader;
|
||||
private final DiskAssetLoader diskLoader;
|
||||
|
||||
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
||||
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
||||
DiskAssetLoader diskLoader) {
|
||||
this.computer = computer;
|
||||
this.scalingDivider = scalingDivider;
|
||||
|
||||
this.networkLoader = networkLoader;
|
||||
this.memoryLoader = memoryLoader;
|
||||
this.diskLoader = diskLoader;
|
||||
}
|
||||
|
||||
public void freeCacheMemory() {
|
||||
memoryLoader.clearCache();
|
||||
}
|
||||
|
||||
private Runnable createLoaderRunnable(final LoaderTuple tuple, final Object context, final LoadListener listener) {
|
||||
return new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Abort if we've been cancelled
|
||||
if (tuple.cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||
if (bmp == null) {
|
||||
// Notify the listener that this may take a while
|
||||
listener.notifyLongLoad(context);
|
||||
|
||||
// Try 5 times maximum
|
||||
for (int i = 0; i < 5; i++) {
|
||||
// Check again whether we've been cancelled
|
||||
if (tuple.cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
InputStream in = networkLoader.getBitmapStream(tuple);
|
||||
if (in != null) {
|
||||
// Write the stream straight to disk
|
||||
diskLoader.populateCacheWithStream(tuple, in);
|
||||
|
||||
// Read it back scaled
|
||||
bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||
if (bmp != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait 1 second with a bit of fuzz
|
||||
try {
|
||||
Thread.sleep((int) (1000 + (Math.random() * 500)));
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bmp != null) {
|
||||
// Populate the memory cache
|
||||
memoryLoader.populateCache(tuple, bmp);
|
||||
}
|
||||
|
||||
// Check one last time whether we've been cancelled
|
||||
synchronized (tuple) {
|
||||
if (tuple.cancelled) {
|
||||
return;
|
||||
}
|
||||
else {
|
||||
tuple.notified = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Call the load complete callback (possible with a null bitmap)
|
||||
listener.notifyLoadComplete(context, bmp);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public LoaderTuple loadBitmapWithContext(NvApp app, Object context, LoadListener listener) {
|
||||
return loadBitmapWithContext(app, context, listener, false);
|
||||
}
|
||||
|
||||
public LoaderTuple loadBitmapWithContextInBackground(NvApp app, Object context, LoadListener listener) {
|
||||
return loadBitmapWithContext(app, context, listener, true);
|
||||
}
|
||||
|
||||
private LoaderTuple loadBitmapWithContext(NvApp app, Object context, LoadListener listener, boolean background) {
|
||||
LoaderTuple tuple = new LoaderTuple(computer, app);
|
||||
|
||||
// First, try the memory cache in the current context
|
||||
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
|
||||
if (bmp != null) {
|
||||
// The caller never sees our tuple in this case
|
||||
listener.notifyLoadComplete(context, bmp);
|
||||
return null;
|
||||
}
|
||||
|
||||
// If it's not in memory, throw this in our executor
|
||||
if (background) {
|
||||
backgroundExecutor.execute(createLoaderRunnable(tuple, context, listener));
|
||||
}
|
||||
else {
|
||||
foregroundExecutor.execute(createLoaderRunnable(tuple, context, listener));
|
||||
}
|
||||
return tuple;
|
||||
}
|
||||
|
||||
public class LoaderTuple {
|
||||
public final ComputerDetails computer;
|
||||
public final NvApp app;
|
||||
|
||||
public boolean notified;
|
||||
public boolean cancelled;
|
||||
|
||||
public LoaderTuple(ComputerDetails computer, NvApp app) {
|
||||
this.computer = computer;
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
public boolean cancel() {
|
||||
synchronized (this) {
|
||||
cancelled = true;
|
||||
return !notified;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "("+computer.uuid+", "+app.getAppId()+")";
|
||||
}
|
||||
}
|
||||
|
||||
public interface LoadListener {
|
||||
// Notifies that the load didn't hit any cache and is about to be dispatched
|
||||
// over the network
|
||||
public void notifyLongLoad(Object context);
|
||||
|
||||
// Bitmap may be null if the load failed
|
||||
public void notifyLoadComplete(Object context, Bitmap bitmap);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +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.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class DiskAssetLoader {
|
||||
private final File cacheDir;
|
||||
|
||||
public DiskAssetLoader(File cacheDir) {
|
||||
this.cacheDir = cacheDir;
|
||||
}
|
||||
|
||||
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 (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 / 12) {
|
||||
@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,51 +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;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public class NetworkAssetLoader {
|
||||
private final Context context;
|
||||
private final String uniqueId;
|
||||
|
||||
public NetworkAssetLoader(Context context, String uniqueId) {
|
||||
this.context = context;
|
||||
this.uniqueId = uniqueId;
|
||||
}
|
||||
|
||||
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||
NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
|
||||
|
||||
InputStream in = null;
|
||||
try {
|
||||
in = http.getBoxArt(tuple.app);
|
||||
} catch (IOException e) {}
|
||||
|
||||
if (in != null) {
|
||||
LimeLog.info("Network asset load complete: " + tuple);
|
||||
}
|
||||
else {
|
||||
LimeLog.info("Network asset load failed: " + tuple);
|
||||
}
|
||||
|
||||
return in;
|
||||
}
|
||||
|
||||
private static InetAddress getCurrentAddress(ComputerDetails computer) {
|
||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||
return computer.localIp;
|
||||
}
|
||||
else {
|
||||
return computer.remoteIp;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import com.limelight.utils.UiHelper;
|
||||
import android.app.Activity;
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.res.Configuration;
|
||||
@@ -22,16 +21,15 @@ import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
public class AddComputerManually extends Activity {
|
||||
private TextView hostText;
|
||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
|
||||
private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
|
||||
private Thread addThread;
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
private ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName className, final IBinder binder) {
|
||||
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
|
||||
startAddThread();
|
||||
@@ -163,12 +161,6 @@ public class AddComputerManually extends Activity {
|
||||
|
||||
computersToAdd.add(hostText.getText().toString().trim());
|
||||
}
|
||||
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
|
||||
// This is how the Fire TV dismisses the keyboard
|
||||
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(hostText.getWindowToken(), 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@ package com.limelight.preferences;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
public class PreferenceConfiguration {
|
||||
static final String RES_FPS_PREF_STRING = "list_resolution_fps";
|
||||
static final String RESX_PREF_STRING = "resolution_x";
|
||||
static final String RESY_PREF_STRING = "resolution_y";
|
||||
private static final String FPS_PREF_STRING = "fps";
|
||||
private static final String DECODER_PREF_STRING = "list_decoders";
|
||||
static final String BITRATE_PREF_STRING = "seekbar_bitrate";
|
||||
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
|
||||
@@ -17,16 +18,17 @@ public class PreferenceConfiguration {
|
||||
private static final String LANGUAGE_PREF_STRING = "list_languages";
|
||||
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
|
||||
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
|
||||
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
|
||||
|
||||
private static final int BITRATE_DEFAULT_720_30 = 5;
|
||||
private static final int BITRATE_DEFAULT_720_60 = 10;
|
||||
private static final int BITRATE_DEFAULT_1080_30 = 10;
|
||||
private static final int BITRATE_DEFAULT_1080_60 = 20;
|
||||
private static final int BITRATE_DEFAULT_MAX = 35;
|
||||
|
||||
private static final String DEFAULT_RES_FPS = "720p60";
|
||||
private static final int DEFAULT_RESX = 1280;
|
||||
private static final int DEFAULT_RESY = 720;
|
||||
private static final String DEFAULT_FPS = "60";
|
||||
private static final String DEFAULT_DECODER = "auto";
|
||||
private static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
|
||||
private static final boolean DEFAULT_STRETCH = false;
|
||||
private static final boolean DEFAULT_SOPS = true;
|
||||
private static final boolean DEFAULT_DISABLE_TOASTS = false;
|
||||
@@ -34,7 +36,6 @@ public class PreferenceConfiguration {
|
||||
private static final int DEFAULT_DEADZONE = 15;
|
||||
public static final String DEFAULT_LANGUAGE = "default";
|
||||
private static final boolean DEFAULT_LIST_MODE = false;
|
||||
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
|
||||
|
||||
public static final int FORCE_HARDWARE_DECODER = -1;
|
||||
public static final int AUTOSELECT_DECODER = 0;
|
||||
@@ -46,57 +47,42 @@ public class PreferenceConfiguration {
|
||||
public int deadzonePercentage;
|
||||
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
|
||||
public String language;
|
||||
public boolean listMode, smallIconMode, multiController;
|
||||
|
||||
public static int getDefaultBitrate(String resFpsString) {
|
||||
if (resFpsString.equals("720p30")) {
|
||||
return BITRATE_DEFAULT_720_30;
|
||||
}
|
||||
else if (resFpsString.equals("720p60")) {
|
||||
return BITRATE_DEFAULT_720_60;
|
||||
}
|
||||
else if (resFpsString.equals("1080p30")) {
|
||||
return BITRATE_DEFAULT_1080_30;
|
||||
}
|
||||
else if (resFpsString.equals("1080p60")) {
|
||||
return BITRATE_DEFAULT_1080_60;
|
||||
}
|
||||
else {
|
||||
// Should never get here
|
||||
return DEFAULT_BITRATE;
|
||||
}
|
||||
}
|
||||
public boolean listMode, smallIconMode;
|
||||
|
||||
public static boolean getDefaultSmallMode(Context context) {
|
||||
PackageManager manager = context.getPackageManager();
|
||||
if (manager != null && manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
|
||||
// TVs shouldn't use small mode by default
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use small mode on anything smaller than a 7" tablet
|
||||
return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
|
||||
}
|
||||
|
||||
private static int getFpsInt(SharedPreferences prefs) {
|
||||
try {
|
||||
return Integer.parseInt(prefs.getString(FPS_PREF_STRING, DEFAULT_FPS));
|
||||
} catch (NumberFormatException e) {
|
||||
return Integer.parseInt(DEFAULT_FPS);
|
||||
}
|
||||
}
|
||||
|
||||
public static int getDefaultBitrate(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
|
||||
if (str.equals("720p30")) {
|
||||
long width = prefs.getInt(RESX_PREF_STRING, DEFAULT_RESX);
|
||||
long height = prefs.getInt(RESY_PREF_STRING, DEFAULT_RESY);
|
||||
|
||||
long product = width * height * getFpsInt(prefs);
|
||||
if (product <= 1280 * 720 * 30) {
|
||||
return BITRATE_DEFAULT_720_30;
|
||||
}
|
||||
else if (str.equals("720p60")) {
|
||||
else if (product <= 1280 * 720 * 60) {
|
||||
return BITRATE_DEFAULT_720_60;
|
||||
}
|
||||
else if (str.equals("1080p30")) {
|
||||
else if (product <= 1920 * 1080 * 30) {
|
||||
return BITRATE_DEFAULT_1080_30;
|
||||
}
|
||||
else if (str.equals("1080p60")) {
|
||||
else if (product <= 1920 * 1080 * 60) {
|
||||
return BITRATE_DEFAULT_1080_60;
|
||||
}
|
||||
else {
|
||||
// Should never get here
|
||||
return DEFAULT_BITRATE;
|
||||
return BITRATE_DEFAULT_MAX;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,33 +110,9 @@ public class PreferenceConfiguration {
|
||||
PreferenceConfiguration config = new PreferenceConfiguration();
|
||||
|
||||
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, getDefaultBitrate(context));
|
||||
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
|
||||
if (str.equals("720p30")) {
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("720p60")) {
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 60;
|
||||
}
|
||||
else if (str.equals("1080p30")) {
|
||||
config.width = 1920;
|
||||
config.height = 1080;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("1080p60")) {
|
||||
config.width = 1920;
|
||||
config.height = 1080;
|
||||
config.fps = 60;
|
||||
}
|
||||
else {
|
||||
// Should never get here
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 60;
|
||||
}
|
||||
config.width = prefs.getInt(RESX_PREF_STRING, DEFAULT_RESX);
|
||||
config.height = prefs.getInt(RESY_PREF_STRING, DEFAULT_RESY);
|
||||
config.fps = getFpsInt(prefs);
|
||||
|
||||
config.decoder = getDecoderValue(context);
|
||||
|
||||
@@ -165,7 +127,6 @@ public class PreferenceConfiguration {
|
||||
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
|
||||
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
|
||||
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
|
||||
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
package com.limelight.preferences;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.DialogPreference;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.limelight.R;
|
||||
|
||||
public class ResolutionPreference extends DialogPreference {
|
||||
private RadioButton a4_3;
|
||||
private RadioButton a16_9;
|
||||
private RadioButton a16_10;
|
||||
private SeekBar resolutionSeekBar;
|
||||
private TextView resolutionText;
|
||||
|
||||
private int resolutionX;
|
||||
private int resolutionY;
|
||||
|
||||
public ResolutionPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
setDialogLayoutResource(R.layout.dialog_resolution);
|
||||
setPositiveButtonText(android.R.string.ok);
|
||||
setNegativeButtonText(android.R.string.cancel);
|
||||
|
||||
setDialogIcon(null);
|
||||
}
|
||||
|
||||
private int getPreferenceState() {
|
||||
int checkboxState;
|
||||
int sliderState;
|
||||
|
||||
if (a4_3.isChecked()) {
|
||||
checkboxState = 1;
|
||||
}
|
||||
else if (a16_10.isChecked()) {
|
||||
checkboxState = 2;
|
||||
}
|
||||
else {
|
||||
checkboxState = 0;
|
||||
}
|
||||
|
||||
sliderState = resolutionSeekBar.getProgress();
|
||||
|
||||
return checkboxState << 24 | sliderState;
|
||||
}
|
||||
|
||||
private void setPreferenceState(int state) {
|
||||
int checkboxState;
|
||||
int sliderState;
|
||||
|
||||
checkboxState = state >> 24;
|
||||
sliderState = state & 0x00FFFFFF;
|
||||
|
||||
a4_3.setChecked(false);
|
||||
a16_10.setChecked(false);
|
||||
a16_9.setChecked(false);
|
||||
|
||||
if (checkboxState == 1) {
|
||||
a4_3.setChecked(true);
|
||||
}
|
||||
else if (checkboxState == 2) {
|
||||
a16_10.setChecked(true);
|
||||
}
|
||||
else {
|
||||
a16_9.setChecked(true);
|
||||
}
|
||||
|
||||
resolutionSeekBar.setProgress(sliderState);
|
||||
}
|
||||
|
||||
private void updateResolution() {
|
||||
double aspectFactor;
|
||||
int step = resolutionSeekBar.getProgress() + 1;
|
||||
|
||||
if (a4_3.isChecked()) {
|
||||
aspectFactor = 4.0/3.0;
|
||||
|
||||
switch (step) {
|
||||
case 1:
|
||||
// 640x480
|
||||
resolutionY = 480;
|
||||
break;
|
||||
case 2:
|
||||
// 800x600
|
||||
resolutionY = 600;
|
||||
break;
|
||||
case 3:
|
||||
// 1024x768
|
||||
resolutionY = 768;
|
||||
break;
|
||||
case 4:
|
||||
// 1600x1200
|
||||
resolutionY = 1200;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (a16_9.isChecked()) {
|
||||
aspectFactor = 16.0/9.0;
|
||||
|
||||
switch (step) {
|
||||
case 1:
|
||||
// 1280x720
|
||||
resolutionY = 720;
|
||||
break;
|
||||
case 2:
|
||||
// 1920x1080
|
||||
resolutionY = 1080;
|
||||
break;
|
||||
case 3:
|
||||
// 2560x1440
|
||||
resolutionY = 1440;
|
||||
break;
|
||||
case 4:
|
||||
// 3840x2160
|
||||
resolutionY = 2160;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else /* if (a16_10.isChecked() */ {
|
||||
aspectFactor = 16.0/10.0;
|
||||
|
||||
switch (step) {
|
||||
case 1:
|
||||
// 1280x800
|
||||
resolutionY = 800;
|
||||
break;
|
||||
case 2:
|
||||
// 1920x1200
|
||||
resolutionY = 1200;
|
||||
break;
|
||||
case 3:
|
||||
// 2560x1600
|
||||
resolutionY = 1600;
|
||||
break;
|
||||
case 4:
|
||||
// 3840x2400
|
||||
resolutionY = 2400;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
resolutionX = (int)(resolutionY * aspectFactor);
|
||||
|
||||
resolutionText.setText(resolutionX+"x"+resolutionY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDialogClosed(boolean positiveResult) {
|
||||
if (positiveResult) {
|
||||
SharedPreferences.Editor e = getPreferenceManager().getSharedPreferences().edit();
|
||||
e.putInt(PreferenceConfiguration.RESX_PREF_STRING, resolutionX);
|
||||
e.putInt(PreferenceConfiguration.RESY_PREF_STRING, resolutionY);
|
||||
persistInt(getPreferenceState());
|
||||
e.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateDialogView() {
|
||||
View v = super.onCreateDialogView();
|
||||
|
||||
a4_3 = (RadioButton) v.findViewById(R.id.aspect_4_3);
|
||||
a16_9 = (RadioButton) v.findViewById(R.id.aspect_16_9);
|
||||
a16_10 = (RadioButton) v.findViewById(R.id.aspect_16_10);
|
||||
resolutionText = (TextView) v.findViewById(R.id.resolutionText);
|
||||
resolutionSeekBar = (SeekBar) v.findViewById(R.id.resolutionSeekbar);
|
||||
|
||||
CompoundButton.OnCheckedChangeListener ccListener = new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
updateResolution();
|
||||
}
|
||||
};
|
||||
a4_3.setOnCheckedChangeListener(ccListener);
|
||||
a16_9.setOnCheckedChangeListener(ccListener);
|
||||
a16_10.setOnCheckedChangeListener(ccListener);
|
||||
|
||||
resolutionSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
updateResolution();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
});
|
||||
|
||||
// Check the correct checkbox
|
||||
setPreferenceState(getPersistedInt(0));
|
||||
|
||||
// Set initial resolution value
|
||||
updateResolution();
|
||||
|
||||
return v;
|
||||
}
|
||||
}
|
||||
@@ -20,14 +20,10 @@ public class SeekBarPreference extends DialogPreference
|
||||
|
||||
private SeekBar seekBar;
|
||||
private TextView valueText;
|
||||
private final Context context;
|
||||
private Context context;
|
||||
|
||||
private final String dialogMessage;
|
||||
private final String suffix;
|
||||
private final int defaultValue;
|
||||
private final int maxValue;
|
||||
private final int minValue;
|
||||
private int currentValue;
|
||||
private String dialogMessage, suffix;
|
||||
private int defaultValue, maxValue, minValue, currentValue;
|
||||
|
||||
public SeekBarPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
@@ -131,6 +127,13 @@ public class SeekBarPreference extends DialogPreference
|
||||
}
|
||||
}
|
||||
|
||||
public void setMax(int max) {
|
||||
this.maxValue = max;
|
||||
}
|
||||
public int getMax() {
|
||||
return this.maxValue;
|
||||
}
|
||||
|
||||
public void setProgress(int progress) {
|
||||
this.currentValue = progress;
|
||||
if (seekBar != null) {
|
||||
|
||||
@@ -51,26 +51,6 @@ public class StreamSettings extends Activity {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
// Add a listener to the FPS and resolution preference
|
||||
// so the bitrate can be auto-adjusted
|
||||
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING);
|
||||
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
String valueStr = (String) newValue;
|
||||
|
||||
// Write the new bitrate value
|
||||
prefs.edit()
|
||||
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
|
||||
PreferenceConfiguration.getDefaultBitrate(valueStr))
|
||||
.apply();
|
||||
|
||||
// Allow the original preference change to take place
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.limelight.utils;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
@@ -11,6 +9,8 @@ import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Reader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Scanner;
|
||||
|
||||
public class CacheHelper {
|
||||
private static File openPath(boolean createPath, File root, String... path) {
|
||||
@@ -30,21 +30,12 @@ public class CacheHelper {
|
||||
return f;
|
||||
}
|
||||
|
||||
public static InputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
|
||||
return new BufferedInputStream(new FileInputStream(openPath(false, root, path)));
|
||||
public static FileInputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
|
||||
return new FileInputStream(openPath(false, root, path));
|
||||
}
|
||||
|
||||
public static OutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
|
||||
return new BufferedOutputStream(new FileOutputStream(openPath(true, root, path)));
|
||||
}
|
||||
|
||||
public static void writeInputStreamToOutputStream(InputStream in, OutputStream out) throws IOException {
|
||||
byte[] buf = new byte[4096];
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = in.read(buf)) != -1) {
|
||||
out.write(buf, 0, bytesRead);
|
||||
}
|
||||
public static FileOutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
|
||||
return new FileOutputStream(openPath(true, root, path));
|
||||
}
|
||||
|
||||
public static String readInputStreamToString(InputStream in) throws IOException {
|
||||
|
||||
@@ -7,16 +7,15 @@ import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
|
||||
public class Dialog implements Runnable {
|
||||
private final String title;
|
||||
private final String message;
|
||||
private final Activity activity;
|
||||
private final boolean endAfterDismiss;
|
||||
private String title, message;
|
||||
private Activity activity;
|
||||
private boolean endAfterDismiss;
|
||||
|
||||
private AlertDialog alert;
|
||||
|
||||
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
|
||||
|
||||
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||
public Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||
{
|
||||
this.activity = activity;
|
||||
this.title = title;
|
||||
|
||||
@@ -9,15 +9,14 @@ import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnCancelListener;
|
||||
|
||||
public class SpinnerDialog implements Runnable,OnCancelListener {
|
||||
private final String title;
|
||||
private final String message;
|
||||
private final Activity activity;
|
||||
private String title, message;
|
||||
private Activity activity;
|
||||
private ProgressDialog progress;
|
||||
private final boolean finish;
|
||||
private boolean finish;
|
||||
|
||||
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
|
||||
|
||||
private SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
||||
public SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
||||
{
|
||||
this.activity = activity;
|
||||
this.title = title;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -10,4 +10,4 @@
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
</merge>
|
||||
</FrameLayout>
|
||||
@@ -9,7 +9,5 @@
|
||||
android:numColumns="auto_fit"
|
||||
android:columnWidth="160dp"
|
||||
android:stretchMode="spacingWidth"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:gravity="center"/>
|
||||
</LinearLayout>
|
||||
@@ -9,7 +9,5 @@
|
||||
android:numColumns="auto_fit"
|
||||
android:columnWidth="105dp"
|
||||
android:stretchMode="spacingWidth"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:gravity="center"/>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/dialog_resolution"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<RadioGroup
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:id="@+id/aspectGroup">
|
||||
<RadioButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/aspect_4_3"
|
||||
android:text="@string/aspect_4_3"/>
|
||||
<RadioButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/aspect_16_9"
|
||||
android:text="@string/aspect_16_9"/>
|
||||
<RadioButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp"
|
||||
android:id="@+id/aspect_16_10"
|
||||
android:text="@string/aspect_16_10"/>
|
||||
</RadioGroup>
|
||||
|
||||
<SeekBar
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/aspectGroup"
|
||||
android:max="3"
|
||||
android:id="@+id/resolutionSeekbar"/>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:textSize="20sp"
|
||||
android:id="@+id/resolutionText"
|
||||
android:layout_below="@+id/resolutionSeekbar"/>
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -6,12 +6,10 @@
|
||||
<ListView
|
||||
android:id="@+id/fragmentView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/list_view_unselected"
|
||||
android:fastScrollEnabled="true"
|
||||
android:longClickable="false"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:stackFromBottom="false" >
|
||||
|
||||
</ListView>
|
||||
|
||||
@@ -8,7 +8,5 @@
|
||||
android:layout_height="fill_parent"
|
||||
android:numColumns="auto_fit"
|
||||
android:columnWidth="160dp"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:gravity="center"/>
|
||||
</LinearLayout>
|
||||
@@ -8,7 +8,5 @@
|
||||
android:layout_height="fill_parent"
|
||||
android:numColumns="auto_fit"
|
||||
android:columnWidth="105dp"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:gravity="center"/>
|
||||
</LinearLayout>
|
||||
@@ -95,8 +95,6 @@
|
||||
<string name="summary_checkbox_disable_warnings">Disabilita i messaggi di warning sullo schermo durante lo streaming</string>
|
||||
|
||||
<string name="category_gamepad_settings">Impostazioni Gamepad</string>
|
||||
<string name="title_checkbox_multi_controller">Supporto controller multipli</string>
|
||||
<string name="summary_checkbox_multi_controller">Quando disabilitato, tutti i controllers appaiono come uno solo</string>
|
||||
<string name="title_seekbar_deadzone">Aggiusta deadzone degli stick analogici</string>
|
||||
<string name="suffix_seekbar_deadzone">%</string>
|
||||
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="resolution_names">
|
||||
<item>720p 30 FPS</item>
|
||||
<item>720p 60 FPS</item>
|
||||
<item>1080p 30 FPS</item>
|
||||
<item>1080p 60 FPS</item>
|
||||
<string-array name="fps_names">
|
||||
<item>30 FPS</item>
|
||||
<item>60 FPS</item>
|
||||
</string-array>
|
||||
<string-array name="resolution_values" translatable="false">
|
||||
<item>720p30</item>
|
||||
<item>720p60</item>
|
||||
<item>1080p30</item>
|
||||
<item>1080p60</item>
|
||||
<string-array name="fps_values" translatable="false">
|
||||
<item>30</item>
|
||||
<item>60</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="language_names" translatable="false">
|
||||
|
||||
@@ -85,8 +85,10 @@
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="category_basic_settings">Basic Settings</string>
|
||||
<string name="title_resolution_list">Select resolution and FPS target</string>
|
||||
<string name="summary_resolution_list">Setting values too high for your device may cause lag or crashing</string>
|
||||
<string name="title_resolution">Select stream resolution</string>
|
||||
<string name="summary_resolution">Some resolutions may not be supported on your device</string>
|
||||
<string name="title_fps_list">Select target FPS</string>
|
||||
<string name="summary_fps_list">Some FPS values may not be supported with high resolutions on your device</string>
|
||||
<string name="title_seekbar_bitrate">Select target video bitrate</string>
|
||||
<string name="summary_seekbar_bitrate">Lower bitrate to reduce stuttering. Raise bitrate to increase image quality.</string>
|
||||
<string name="suffix_seekbar_bitrate">Mbps</string>
|
||||
@@ -95,8 +97,6 @@
|
||||
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
|
||||
|
||||
<string name="category_gamepad_settings">Gamepad Settings</string>
|
||||
<string name="title_checkbox_multi_controller">Multiple controller support</string>
|
||||
<string name="summary_checkbox_multi_controller">When unchecked, all controllers appear as one</string>
|
||||
<string name="title_seekbar_deadzone">Adjust analog stick deadzone</string>
|
||||
<string name="suffix_seekbar_deadzone">%</string>
|
||||
|
||||
@@ -117,4 +117,9 @@
|
||||
<string name="category_advanced_settings">Advanced Settings</string>
|
||||
<string name="title_decoder_list">Change decoder</string>
|
||||
<string name="summary_decoder_list">Software decoding may improve video latency at lower streaming settings</string>
|
||||
|
||||
<!-- Resolution Preference -->
|
||||
<string name="aspect_4_3">4:3</string>
|
||||
<string name="aspect_16_9">16:9</string>
|
||||
<string name="aspect_16_10">16:10</string>
|
||||
</resources>
|
||||
@@ -2,13 +2,17 @@
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<PreferenceCategory android:title="@string/category_basic_settings">
|
||||
<com.limelight.preferences.ResolutionPreference
|
||||
android:key="resolution"
|
||||
android:title="@string/title_resolution"
|
||||
android:summary="@string/summary_resolution" />
|
||||
<ListPreference
|
||||
android:key="list_resolution_fps"
|
||||
android:title="@string/title_resolution_list"
|
||||
android:summary="@string/summary_resolution_list"
|
||||
android:entries="@array/resolution_names"
|
||||
android:entryValues="@array/resolution_values"
|
||||
android:defaultValue="720p60" />
|
||||
android:title="@string/title_fps_list"
|
||||
android:summary="@string/summary_fps_list"
|
||||
android:entries="@array/fps_names"
|
||||
android:entryValues="@array/fps_values"
|
||||
android:defaultValue="60" />
|
||||
<com.limelight.preferences.SeekBarPreference
|
||||
android:key="seekbar_bitrate"
|
||||
android:dialogMessage="@string/summary_seekbar_bitrate"
|
||||
@@ -26,19 +30,14 @@
|
||||
android:summary="@string/summary_checkbox_disable_warnings"
|
||||
android:defaultValue="false" />
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/category_gamepad_settings">
|
||||
<!--com.limelight.preferences.SeekBarPreference
|
||||
<!--PreferenceCategory android:title="@string/category_gamepad_settings">
|
||||
<com.limelight.preferences.SeekBarPreference
|
||||
android:key="seekbar_deadzone"
|
||||
android:defaultValue="15"
|
||||
android:max="50"
|
||||
android:text="@string/suffix_seekbar_deadzone"
|
||||
android:title="@string/title_seekbar_deadzone"/-->
|
||||
<CheckBoxPreference
|
||||
android:key="checkbox_multi_controller"
|
||||
android:title="@string/title_checkbox_multi_controller"
|
||||
android:summary="@string/summary_checkbox_multi_controller"
|
||||
android:defaultValue="true" />
|
||||
</PreferenceCategory>
|
||||
android:title="@string/title_seekbar_deadzone"/>
|
||||
</PreferenceCategory-->
|
||||
<PreferenceCategory android:title="@string/category_host_settings">
|
||||
<CheckBoxPreference
|
||||
android:key="checkbox_enable_sops"
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:1.1.0'
|
||||
classpath 'com.android.tools.build:gradle:1.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="false">
|
||||
<output url="file://$MODULE_DIR$/build/classes/main" />
|
||||
<output-test url="file://$MODULE_DIR$/build/classes/test" />
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
|
||||
|
||||
Reference in new issue
Block a user