Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce01223683 | |||
| e7501a488d | |||
| 5626e9663b | |||
| 01b35ccdd3 | |||
| e83bc747c8 |
+3
-2
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.limelight"
|
package="com.limelight"
|
||||||
android:versionCode="14"
|
android:versionCode="16"
|
||||||
android:versionName="2.2.1" >
|
android:versionName="2.2.1.2" >
|
||||||
|
|
||||||
<uses-sdk
|
<uses-sdk
|
||||||
android:minSdkVersion="16"
|
android:minSdkVersion="16"
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<category android:name="tv.ouya.intent.category.APP" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>na
|
|||||||
public static final int activity_vertical_margin=0x7f050001;
|
public static final int activity_vertical_margin=0x7f050001;
|
||||||
}
|
}
|
||||||
public static final class drawable {
|
public static final class drawable {
|
||||||
public static final int ic_launcher=0x7f020000;
|
public static final int app_icon=0x7f020000;
|
||||||
|
public static final int ic_launcher=0x7f020001;
|
||||||
|
public static final int ouya_icon=0x7f020002;
|
||||||
}
|
}
|
||||||
public static final class id {
|
public static final class id {
|
||||||
public static final int autoDec=0x7f080006;
|
public static final int autoDec=0x7f080006;
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -1,16 +1,14 @@
|
|||||||
package com.limelight;
|
package com.limelight;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
|
||||||
|
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.nvstream.http.PairingManager;
|
||||||
|
import com.limelight.utils.Dialog;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -52,6 +50,13 @@ public class Connection extends Activity {
|
|||||||
super.onPause();
|
super.onPause();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
|
||||||
|
Dialog.closeDialogs();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -267,29 +272,39 @@ public class Connection extends Activity {
|
|||||||
String message;
|
String message;
|
||||||
try {
|
try {
|
||||||
httpConn = new NvHTTP(InetAddress.getByName(hostText.getText().toString()),
|
httpConn = new NvHTTP(InetAddress.getByName(hostText.getText().toString()),
|
||||||
macAddress, PlatformBinding.getDeviceName());
|
macAddress, PlatformBinding.getDeviceName(), PlatformBinding.getCryptoProvider(Connection.this));
|
||||||
try {
|
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||||
if (httpConn.getPairState()) {
|
message = "Already paired";
|
||||||
message = "Already paired";
|
}
|
||||||
|
else {
|
||||||
|
final String pinStr = PairingManager.generatePinString();
|
||||||
|
|
||||||
|
// Spin the dialog off in a thread because it blocks
|
||||||
|
Dialog.displayDialog(Connection.this, "Pairing", "Please enter the following PIN on the target PC: "+pinStr, false);
|
||||||
|
|
||||||
|
PairingManager.PairState pairState = httpConn.pair(pinStr);
|
||||||
|
if (pairState == PairingManager.PairState.PIN_WRONG) {
|
||||||
|
message = "Incorrect PIN";
|
||||||
|
}
|
||||||
|
else if (pairState == PairingManager.PairState.FAILED) {
|
||||||
|
message = "Pairing failed";
|
||||||
|
}
|
||||||
|
else if (pairState == PairingManager.PairState.PAIRED) {
|
||||||
|
message = "Paired successfully";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
int session = httpConn.getSessionId();
|
// Should be no other values
|
||||||
if (session == 0) {
|
message = null;
|
||||||
message = "Pairing was declined by the target";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
message = "Pairing was successful";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
|
||||||
message = e.getMessage();
|
|
||||||
} catch (XmlPullParserException e) {
|
|
||||||
message = e.getMessage();
|
|
||||||
}
|
}
|
||||||
} catch (UnknownHostException e1) {
|
} catch (UnknownHostException e1) {
|
||||||
message = "Failed to resolve host";
|
message = "Failed to resolve host";
|
||||||
|
} catch (Exception e) {
|
||||||
|
message = e.getMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Dialog.closeDialogs();
|
||||||
|
|
||||||
final String toastMessage = message;
|
final String toastMessage = message;
|
||||||
runOnUiThread(new Runnable() {
|
runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
|
|
||||||
// Start the connection
|
// Start the connection
|
||||||
conn = new NvConnection(Game.this.getIntent().getStringExtra("host"), Game.this,
|
conn = new NvConnection(Game.this.getIntent().getStringExtra("host"), Game.this,
|
||||||
new StreamConfiguration(width, height, refreshRate, bitrate * 1000));
|
new StreamConfiguration(width, height, refreshRate, bitrate * 1000), PlatformBinding.getCryptoProvider(this));
|
||||||
keybTranslator = new KeyboardTranslator(conn);
|
keybTranslator = new KeyboardTranslator(conn);
|
||||||
controllerHandler = new ControllerHandler(conn);
|
controllerHandler = new ControllerHandler(conn);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package com.limelight.binding;
|
package com.limelight.binding;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
import com.limelight.binding.audio.AndroidAudioRenderer;
|
import com.limelight.binding.audio.AndroidAudioRenderer;
|
||||||
|
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
||||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||||
|
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||||
|
|
||||||
public class PlatformBinding {
|
public class PlatformBinding {
|
||||||
public static String getDeviceName() {
|
public static String getDeviceName() {
|
||||||
@@ -13,4 +17,8 @@ public class PlatformBinding {
|
|||||||
public static AudioRenderer getAudioRenderer() {
|
public static AudioRenderer getAudioRenderer() {
|
||||||
return new AndroidAudioRenderer();
|
return new AndroidAudioRenderer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static LimelightCryptoProvider getCryptoProvider(Context c) {
|
||||||
|
return new AndroidCryptoProvider(c);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,272 @@
|
|||||||
|
package com.limelight.binding.crypto;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.NoSuchProviderException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.Security;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.interfaces.RSAPrivateKey;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import javax.security.auth.x500.X500Principal;
|
||||||
|
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
import org.bouncycastle.openssl.PEMWriter;
|
||||||
|
import org.bouncycastle.x509.X509V3CertificateGenerator;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Base64;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||||
|
|
||||||
|
private File certFile;
|
||||||
|
private File keyFile;
|
||||||
|
|
||||||
|
private X509Certificate cert;
|
||||||
|
private RSAPrivateKey key;
|
||||||
|
private byte[] pemCertBytes;
|
||||||
|
|
||||||
|
static {
|
||||||
|
// Install the Bouncy Castle provider
|
||||||
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
public AndroidCryptoProvider(Context c) {
|
||||||
|
String dataPath = c.getFilesDir().getAbsolutePath();
|
||||||
|
|
||||||
|
certFile = new File(dataPath + File.separator + "client.crt");
|
||||||
|
keyFile = new File(dataPath + File.separator + "client.key");
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] loadFileToBytes(File f) {
|
||||||
|
if (!f.exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
FileInputStream fin = new FileInputStream(f);
|
||||||
|
byte[] fileData = new byte[(int) f.length()];
|
||||||
|
fin.read(fileData);
|
||||||
|
fin.close();
|
||||||
|
return fileData;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean loadCertKeyPair() {
|
||||||
|
byte[] certBytes = loadFileToBytes(certFile);
|
||||||
|
byte[] keyBytes = loadFileToBytes(keyFile);
|
||||||
|
|
||||||
|
// If either file was missing, we definitely can't succeed
|
||||||
|
if (certBytes == null || keyBytes == null) {
|
||||||
|
LimeLog.info("Missing cert or key; need to generate a new one");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
|
||||||
|
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||||
|
pemCertBytes = certBytes;
|
||||||
|
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
|
||||||
|
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
// May happen if the cert is corrupt
|
||||||
|
LimeLog.warning("Corrupted certificate");
|
||||||
|
return false;
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
// Should never happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
} catch (InvalidKeySpecException e) {
|
||||||
|
// May happen if the key is corrupt
|
||||||
|
LimeLog.warning("Corrupted key");
|
||||||
|
return false;
|
||||||
|
} catch (NoSuchProviderException e) {
|
||||||
|
// Should never happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Loaded key pair from disk");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("TrulyRandom")
|
||||||
|
private boolean generateCertKeyPair() {
|
||||||
|
X509V3CertificateGenerator certGenerator = new X509V3CertificateGenerator();
|
||||||
|
X500Principal principalName = new X500Principal("CN=NVIDIA GameStream Client");
|
||||||
|
|
||||||
|
byte[] snBytes = new byte[8];
|
||||||
|
new SecureRandom().nextBytes(snBytes);
|
||||||
|
|
||||||
|
KeyPair keyPair;
|
||||||
|
try {
|
||||||
|
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
||||||
|
keyPairGenerator.initialize(2048);
|
||||||
|
keyPair = keyPairGenerator.generateKeyPair();
|
||||||
|
} catch (NoSuchAlgorithmException e1) {
|
||||||
|
// Should never happen
|
||||||
|
e1.printStackTrace();
|
||||||
|
return false;
|
||||||
|
} catch (NoSuchProviderException e) {
|
||||||
|
// Should never happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Date now = new Date();
|
||||||
|
Date expirationDate = new Date();
|
||||||
|
|
||||||
|
// Expires in 20 years
|
||||||
|
expirationDate.setYear(expirationDate.getYear() + 20);
|
||||||
|
|
||||||
|
certGenerator.setSerialNumber(new BigInteger(snBytes).abs());
|
||||||
|
certGenerator.setIssuerDN(principalName);
|
||||||
|
certGenerator.setNotBefore(now);
|
||||||
|
certGenerator.setNotAfter(expirationDate);
|
||||||
|
certGenerator.setSubjectDN(principalName);
|
||||||
|
certGenerator.setPublicKey(keyPair.getPublic());
|
||||||
|
certGenerator.setSignatureAlgorithm("SHA1withRSA");
|
||||||
|
|
||||||
|
try {
|
||||||
|
cert = certGenerator.generate(keyPair.getPrivate(), "BC");
|
||||||
|
key = (RSAPrivateKey) keyPair.getPrivate();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Nothing should go wrong here
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Generated a new key pair");
|
||||||
|
|
||||||
|
// Save the resulting pair
|
||||||
|
saveCertKeyPair();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveCertKeyPair() {
|
||||||
|
try {
|
||||||
|
FileOutputStream certOut = new FileOutputStream(certFile);
|
||||||
|
FileOutputStream keyOut = new FileOutputStream(keyFile);
|
||||||
|
|
||||||
|
// Write the certificate in OpenSSL PEM format (important for the server)
|
||||||
|
StringWriter strWriter = new StringWriter();
|
||||||
|
PEMWriter pemWriter = new PEMWriter(strWriter);
|
||||||
|
pemWriter.writeObject(cert);
|
||||||
|
pemWriter.close();
|
||||||
|
|
||||||
|
// Line endings MUST be UNIX for the PC to accept the cert properly
|
||||||
|
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
|
||||||
|
String pemStr = strWriter.getBuffer().toString();
|
||||||
|
for (int i = 0; i < pemStr.length(); i++) {
|
||||||
|
char c = pemStr.charAt(i);
|
||||||
|
if (c != '\r')
|
||||||
|
certWriter.append(c);
|
||||||
|
}
|
||||||
|
certWriter.close();
|
||||||
|
|
||||||
|
// Write the private out in PKCS8 format
|
||||||
|
keyOut.write(key.getEncoded());
|
||||||
|
|
||||||
|
certOut.close();
|
||||||
|
keyOut.close();
|
||||||
|
|
||||||
|
LimeLog.info("Saved generated key pair to disk");
|
||||||
|
} catch (IOException e) {
|
||||||
|
// This isn't good because it means we'll have
|
||||||
|
// to re-pair next time
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate getClientCertificate() {
|
||||||
|
// Use a lock here to ensure only one guy will be generating or loading
|
||||||
|
// the certificate and key at a time
|
||||||
|
synchronized (this) {
|
||||||
|
// Return a loaded cert if we have one
|
||||||
|
if (cert != null) {
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No loaded cert yet, let's see if we have one on disk
|
||||||
|
if (loadCertKeyPair()) {
|
||||||
|
// Got one
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to generate a new key pair
|
||||||
|
if (!generateCertKeyPair()) {
|
||||||
|
// Failed
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the generated pair
|
||||||
|
loadCertKeyPair();
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RSAPrivateKey getClientPrivateKey() {
|
||||||
|
// Use a lock here to ensure only one guy will be generating or loading
|
||||||
|
// the certificate and key at a time
|
||||||
|
synchronized (this) {
|
||||||
|
// Return a loaded key if we have one
|
||||||
|
if (key != null) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No loaded key yet, let's see if we have one on disk
|
||||||
|
if (loadCertKeyPair()) {
|
||||||
|
// Got one
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to generate a new key pair
|
||||||
|
if (!generateCertKeyPair()) {
|
||||||
|
// Failed
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the generated pair
|
||||||
|
loadCertKeyPair();
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPemEncodedClientCertificate() {
|
||||||
|
synchronized (this) {
|
||||||
|
// Call our helper function to do the cert loading/generation for us
|
||||||
|
getClientCertificate();
|
||||||
|
|
||||||
|
// Return a cached value if we have it
|
||||||
|
return pemCertBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String encodeBase64String(byte[] data) {
|
||||||
|
return Base64.encodeToString(data, Base64.NO_WRAP);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.limelight.binding.input;
|
|||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
@@ -17,6 +18,24 @@ public class ControllerHandler {
|
|||||||
private short rightStickY = 0x0000;
|
private short rightStickY = 0x0000;
|
||||||
private short leftStickX = 0x0000;
|
private short leftStickX = 0x0000;
|
||||||
private short leftStickY = 0x0000;
|
private short leftStickY = 0x0000;
|
||||||
|
private int emulatingButtonFlags = 0;
|
||||||
|
|
||||||
|
// Used for OUYA bumper state tracking since they force all buttons
|
||||||
|
// up when the OUYA button goes down. We watch the last time we get
|
||||||
|
// a bumper up and compare that to our maximum delay when we receive
|
||||||
|
// a Start button press to see if we should activate one of our
|
||||||
|
// emulated button combos.
|
||||||
|
private long lastLbUpTime = 0;
|
||||||
|
private long lastRbUpTime = 0;
|
||||||
|
private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
|
||||||
|
|
||||||
|
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 10;
|
||||||
|
|
||||||
|
private static final int EMULATING_SPECIAL = 0x1;
|
||||||
|
private static final int EMULATING_SELECT = 0x2;
|
||||||
|
|
||||||
|
private static final int EMULATED_SPECIAL_UP_DELAY_MS = 100;
|
||||||
|
private static final int EMULATED_SELECT_UP_DELAY_MS = 30;
|
||||||
|
|
||||||
private HashMap<String, ControllerMapping> mappings = new HashMap<String, ControllerMapping>();
|
private HashMap<String, ControllerMapping> mappings = new HashMap<String, ControllerMapping>();
|
||||||
|
|
||||||
@@ -298,6 +317,18 @@ public class ControllerHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the button hasn't been down long enough, sleep for a bit before sending the up event
|
||||||
|
// This allows "instant" button presses (like OUYA's virtual menu button) to work. This
|
||||||
|
// path should not be triggered during normal usage.
|
||||||
|
if (SystemClock.uptimeMillis() - event.getDownTime() < ControllerHandler.MINIMUM_BUTTON_DOWN_TIME_MS)
|
||||||
|
{
|
||||||
|
// Since our sleep time is so short (10 ms), it shouldn't cause a problem doing this in the
|
||||||
|
// UI thread.
|
||||||
|
try {
|
||||||
|
Thread.sleep(ControllerHandler.MINIMUM_BUTTON_DOWN_TIME_MS);
|
||||||
|
} catch (InterruptedException e) {}
|
||||||
|
}
|
||||||
|
|
||||||
switch (keyCode) {
|
switch (keyCode) {
|
||||||
case KeyEvent.KEYCODE_BUTTON_MODE:
|
case KeyEvent.KEYCODE_BUTTON_MODE:
|
||||||
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
@@ -337,9 +368,11 @@ public class ControllerHandler {
|
|||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_L1:
|
case KeyEvent.KEYCODE_BUTTON_L1:
|
||||||
inputMap &= ~ControllerPacket.LB_FLAG;
|
inputMap &= ~ControllerPacket.LB_FLAG;
|
||||||
|
lastLbUpTime = SystemClock.uptimeMillis();
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_R1:
|
case KeyEvent.KEYCODE_BUTTON_R1:
|
||||||
inputMap &= ~ControllerPacket.RB_FLAG;
|
inputMap &= ~ControllerPacket.RB_FLAG;
|
||||||
|
lastRbUpTime = SystemClock.uptimeMillis();
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_THUMBL:
|
case KeyEvent.KEYCODE_BUTTON_THUMBL:
|
||||||
inputMap &= ~ControllerPacket.LS_CLK_FLAG;
|
inputMap &= ~ControllerPacket.LS_CLK_FLAG;
|
||||||
@@ -357,11 +390,39 @@ public class ControllerHandler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If one of the two is up, the special button comes up too
|
// Check if we're emulating the select button
|
||||||
if ((inputMap & ControllerPacket.BACK_FLAG) == 0 ||
|
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
|
||||||
(inputMap & ControllerPacket.PLAY_FLAG) == 0)
|
|
||||||
{
|
{
|
||||||
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
// If either start or LB is up, select comes up too
|
||||||
|
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
|
||||||
|
(inputMap & ControllerPacket.LB_FLAG) == 0)
|
||||||
|
{
|
||||||
|
inputMap &= ~ControllerPacket.BACK_FLAG;
|
||||||
|
|
||||||
|
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
|
||||||
|
} catch (InterruptedException e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're emulating the special button
|
||||||
|
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0)
|
||||||
|
{
|
||||||
|
// If either start or select and RB is up, the special button comes up too
|
||||||
|
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
|
||||||
|
((inputMap & ControllerPacket.BACK_FLAG) == 0 &&
|
||||||
|
(inputMap & ControllerPacket.RB_FLAG) == 0))
|
||||||
|
{
|
||||||
|
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
|
|
||||||
|
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
|
||||||
|
} catch (InterruptedException e) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendControllerInputPacket();
|
sendControllerInputPacket();
|
||||||
@@ -438,12 +499,27 @@ public class ControllerHandler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We detect back+start as the special button combo
|
// Start+LB acts like select for controllers with one button
|
||||||
if ((inputMap & ControllerPacket.BACK_FLAG) != 0 &&
|
if ((inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
|
||||||
|
((inputMap & ControllerPacket.LB_FLAG) != 0 ||
|
||||||
|
SystemClock.uptimeMillis() - lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
|
||||||
|
{
|
||||||
|
inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
|
||||||
|
inputMap |= ControllerPacket.BACK_FLAG;
|
||||||
|
|
||||||
|
emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We detect select+start or start+RB as the special button combo
|
||||||
|
if (((inputMap & ControllerPacket.RB_FLAG) != 0 ||
|
||||||
|
(SystemClock.uptimeMillis() - lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
|
||||||
|
(inputMap & ControllerPacket.BACK_FLAG) != 0) &&
|
||||||
(inputMap & ControllerPacket.PLAY_FLAG) != 0)
|
(inputMap & ControllerPacket.PLAY_FLAG) != 0)
|
||||||
{
|
{
|
||||||
inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG);
|
inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
|
||||||
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
|
|
||||||
|
emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendControllerInputPacket();
|
sendControllerInputPacket();
|
||||||
|
|||||||
Reference in New Issue
Block a user