From 39d7fc748faa3303ef6dbb274a37dcba9bb3fe58 Mon Sep 17 00:00:00 2001 From: Ansa89 Date: Mon, 2 Feb 2015 12:05:37 +0100 Subject: [PATCH 001/202] Italian translation: update --- app/src/main/res/values-it/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 21992302..3604f32d 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -95,8 +95,8 @@ Disabilita i messaggi di warning sullo schermo durante lo streaming Impostazioni Gamepad - Enable multiple controller support - When unchecked, all controllers appear as one + Supporto controller multipli + Quando disabilitato, tutti i controllers appaiono come uno solo Aggiusta deadzone degli stick analogici % From 07277e1a5b812be5b61be62b4721f12afef2039f Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Thu, 5 Feb 2015 13:01:35 -0500 Subject: [PATCH 002/202] Fix a few Lint warnings --- app/build.gradle | 16 ++++++++-------- .../java/com/limelight/grid/AppGridAdapter.java | 2 +- build.gradle | 2 +- limelight-android.iml | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 74780e17..4321f380 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -62,17 +62,17 @@ android { } dependencies { - compile group: 'org.jcodec', name: 'jcodec', version: '+' + compile group: 'org.jcodec', name: 'jcodec', version: '0.1.9' - compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '+' - compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '+' + compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51' + compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51' - 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.google.android', name: 'support-v4', version:'21.0.3' + compile group: 'com.koushikdutta.ion', name: 'ion', version:'2.0.5' + compile group: 'com.google.code.gson', name: 'gson', version:'2.3.1' - compile group: 'com.squareup.okhttp', name: 'okhttp', version:'+' - compile group: 'com.squareup.okio', name:'okio', version:'+' + compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.2.0' + compile group: 'com.squareup.okio', name:'okio', version:'1.2.0' compile files('libs/jmdns-fixed.jar') compile files('libs/limelight-common.jar') diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index 100e49e3..2de6399a 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -280,5 +280,5 @@ public class AppGridAdapter extends GenericGridAdapter { } } } - }; + } } diff --git a/build.gradle b/build.gradle index e26cdeef..c4477273 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.0.0' + classpath 'com.android.tools.build:gradle:1.0.1' } } diff --git a/limelight-android.iml b/limelight-android.iml index 55503402..42f4ed55 100644 --- a/limelight-android.iml +++ b/limelight-android.iml @@ -7,7 +7,7 @@ - + From d3986080a3c31cf9b2c84fe9668075665a8f1608 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Thu, 5 Feb 2015 13:21:04 -0500 Subject: [PATCH 003/202] Tighten up a bunch of declarations to make Lint happier --- app/build.gradle | 2 +- app/src/main/java/com/limelight/AppView.java | 6 +++--- app/src/main/java/com/limelight/Game.java | 8 ++++---- app/src/main/java/com/limelight/PcView.java | 2 +- .../binding/audio/AndroidAudioRenderer.java | 2 +- .../binding/crypto/AndroidCryptoProvider.java | 4 ++-- .../binding/input/ControllerHandler.java | 12 ++++++------ .../binding/input/KeyboardTranslator.java | 2 +- .../limelight/binding/input/TouchContext.java | 7 ++++--- .../binding/input/evdev/EvdevEvent.java | 6 +++--- .../binding/input/evdev/EvdevHandler.java | 6 +++--- .../binding/input/evdev/EvdevTranslator.java | 2 +- .../video/AndroidCpuDecoderRenderer.java | 2 +- .../video/MediaCodecDecoderRenderer.java | 4 ++-- .../binding/video/MediaCodecHelper.java | 14 +++++++------- .../computers/ComputerManagerService.java | 12 ++++++------ .../limelight/discovery/DiscoveryService.java | 2 +- .../com/limelight/grid/AppGridAdapter.java | 19 ++++++++++--------- .../limelight/grid/GenericGridAdapter.java | 10 +++++----- .../preferences/AddComputerManually.java | 4 ++-- .../preferences/SeekBarPreference.java | 17 +++++++---------- .../main/java/com/limelight/utils/Dialog.java | 9 +++++---- .../com/limelight/utils/SpinnerDialog.java | 9 +++++---- 23 files changed, 81 insertions(+), 80 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4321f380..fa4f859b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -67,7 +67,7 @@ dependencies { compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51' compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51' - compile group: 'com.google.android', name: 'support-v4', version:'21.0.3' + compile group: 'com.google.android', name: 'support-v4', version:'r7' compile group: 'com.koushikdutta.ion', name: 'ion', version:'2.0.5' compile group: 'com.google.code.gson', name: 'gson', version:'2.3.1' diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index a7db11d9..9218bf6d 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -74,7 +74,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { public final static String UUID_EXTRA = "UUID"; private ComputerManagerService.ComputerManagerBinder managerBinder; - private ServiceConnection serviceConnection = new ServiceConnection() { + private final ServiceConnection serviceConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder binder) { final ComputerManagerService.ComputerManagerBinder localBinder = ((ComputerManagerService.ComputerManagerBinder)binder); @@ -172,7 +172,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { blockingLoadSpinner.dismiss(); blockingLoadSpinner = null; } - } catch (Exception e) {} + } catch (Exception ignored) {} } }); @@ -505,7 +505,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { } public class AppObject { - public NvApp app; + public final NvApp app; public AppObject(NvApp app) { this.app = app; diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index 44e11471..a5bef3e1 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -60,7 +60,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, private int lastButtonState = 0; // Only 2 touches are supported - private TouchContext[] touchContextMap = new TouchContext[2]; + private final TouchContext[] touchContextMap = new TouchContext[2]; private long threeFingerDownTime = 0; private static final int THREE_FINGER_TAP_THRESHOLD = 300; @@ -69,7 +69,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, private KeyboardTranslator keybTranslator; private PreferenceConfiguration prefConfig; - private Point screenSize = new Point(0, 0); + private final Point screenSize = new Point(0, 0); private NvConnection conn; private SpinnerDialog spinner; @@ -246,7 +246,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, } @SuppressLint("InlinedApi") - private Runnable hideSystemUi = new Runnable() { + private final Runnable hideSystemUi = new Runnable() { @Override public void run() { // Use immersive mode on 4.4+ or standard low profile on previous builds @@ -315,7 +315,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, wifiLock.release(); } - private Runnable toggleGrab = new Runnable() { + private final Runnable toggleGrab = new Runnable() { @Override public void run() { diff --git a/app/src/main/java/com/limelight/PcView.java b/app/src/main/java/com/limelight/PcView.java index 76b44642..c2bcb3e0 100644 --- a/app/src/main/java/com/limelight/PcView.java +++ b/app/src/main/java/com/limelight/PcView.java @@ -53,7 +53,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { private PcGridAdapter pcGridAdapter; private ComputerManagerService.ComputerManagerBinder managerBinder; private boolean freezeUpdates, runningPolling; - private ServiceConnection serviceConnection = new ServiceConnection() { + private final ServiceConnection serviceConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder binder) { final ComputerManagerService.ComputerManagerBinder localBinder = ((ComputerManagerService.ComputerManagerBinder)binder); diff --git a/app/src/main/java/com/limelight/binding/audio/AndroidAudioRenderer.java b/app/src/main/java/com/limelight/binding/audio/AndroidAudioRenderer.java index c471f379..83c5fbb7 100644 --- a/app/src/main/java/com/limelight/binding/audio/AndroidAudioRenderer.java +++ b/app/src/main/java/com/limelight/binding/audio/AndroidAudioRenderer.java @@ -9,7 +9,7 @@ import com.limelight.nvstream.av.audio.AudioRenderer; public class AndroidAudioRenderer implements AudioRenderer { - public static final int FRAME_SIZE = 960; + private static final int FRAME_SIZE = 960; private AudioTrack track; diff --git a/app/src/main/java/com/limelight/binding/crypto/AndroidCryptoProvider.java b/app/src/main/java/com/limelight/binding/crypto/AndroidCryptoProvider.java index 64e9c1a6..879482fa 100644 --- a/app/src/main/java/com/limelight/binding/crypto/AndroidCryptoProvider.java +++ b/app/src/main/java/com/limelight/binding/crypto/AndroidCryptoProvider.java @@ -45,8 +45,8 @@ import com.limelight.nvstream.http.LimelightCryptoProvider; public class AndroidCryptoProvider implements LimelightCryptoProvider { - private File certFile; - private File keyFile; + private final File certFile; + private final File keyFile; private X509Certificate cert; private RSAPrivateKey key; diff --git a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java index fd882335..4680ee2c 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -30,17 +30,17 @@ 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 Vector2d inputVector = new Vector2d(); + private final Vector2d inputVector = new Vector2d(); - private HashMap contexts = new HashMap(); + private final HashMap contexts = new HashMap(); - private NvConnection conn; - private double stickDeadzone; + private final NvConnection conn; + private final double stickDeadzone; private final ControllerContext defaultContext = new ControllerContext(); - private GameGestures gestures; + private final GameGestures gestures; private boolean hasGameController; - private boolean multiControllerEnabled; + private final boolean multiControllerEnabled; private short currentControllers; public ControllerHandler(NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) { diff --git a/app/src/main/java/com/limelight/binding/input/KeyboardTranslator.java b/app/src/main/java/com/limelight/binding/input/KeyboardTranslator.java index ad8d92d7..bfaf2dbc 100644 --- a/app/src/main/java/com/limelight/binding/input/KeyboardTranslator.java +++ b/app/src/main/java/com/limelight/binding/input/KeyboardTranslator.java @@ -15,7 +15,7 @@ public class KeyboardTranslator extends KeycodeTranslator { /** * GFE's prefix for every key code */ - public static final short KEY_PREFIX = (short) 0x80; + private static final short KEY_PREFIX = (short) 0x80; public static final int VK_0 = 48; public static final int VK_9 = 57; diff --git a/app/src/main/java/com/limelight/binding/input/TouchContext.java b/app/src/main/java/com/limelight/binding/input/TouchContext.java index 87a38249..179e5034 100644 --- a/app/src/main/java/com/limelight/binding/input/TouchContext.java +++ b/app/src/main/java/com/limelight/binding/input/TouchContext.java @@ -11,9 +11,10 @@ public class TouchContext { private long originalTouchTime = 0; private boolean cancelled; - private NvConnection conn; - private int actionIndex; - private double xFactor, yFactor; + private final NvConnection conn; + private final int actionIndex; + private final double xFactor; + private final double yFactor; private static final int TAP_MOVEMENT_THRESHOLD = 10; private static final int TAP_TIME_THRESHOLD = 250; diff --git a/app/src/main/java/com/limelight/binding/input/evdev/EvdevEvent.java b/app/src/main/java/com/limelight/binding/input/evdev/EvdevEvent.java index cbe45005..0addf697 100644 --- a/app/src/main/java/com/limelight/binding/input/evdev/EvdevEvent.java +++ b/app/src/main/java/com/limelight/binding/input/evdev/EvdevEvent.java @@ -29,9 +29,9 @@ public class EvdevEvent { /* Keys */ public static final short KEY_Q = 16; - public short type; - public short code; - public int value; + public final short type; + public final short code; + public final int value; public EvdevEvent(short type, short code, int value) { this.type = type; diff --git a/app/src/main/java/com/limelight/binding/input/evdev/EvdevHandler.java b/app/src/main/java/com/limelight/binding/input/evdev/EvdevHandler.java index 5d78477a..f60d751e 100644 --- a/app/src/main/java/com/limelight/binding/input/evdev/EvdevHandler.java +++ b/app/src/main/java/com/limelight/binding/input/evdev/EvdevHandler.java @@ -7,12 +7,12 @@ import com.limelight.LimeLog; public class EvdevHandler { - private String absolutePath; - private EvdevListener listener; + private final String absolutePath; + private final EvdevListener listener; private boolean shutdown = false; private int fd = -1; - private Thread handlerThread = new Thread() { + private final Thread handlerThread = new Thread() { @Override public void run() { // All the finally blocks here make this code look like a mess diff --git a/app/src/main/java/com/limelight/binding/input/evdev/EvdevTranslator.java b/app/src/main/java/com/limelight/binding/input/evdev/EvdevTranslator.java index 00961e6e..e7e376c2 100644 --- a/app/src/main/java/com/limelight/binding/input/evdev/EvdevTranslator.java +++ b/app/src/main/java/com/limelight/binding/input/evdev/EvdevTranslator.java @@ -4,7 +4,7 @@ import android.view.KeyEvent; public class EvdevTranslator { - public static final short EVDEV_KEY_CODES[] = { + private static final short[] EVDEV_KEY_CODES = { 0, //KeyEvent.VK_RESERVED KeyEvent.KEYCODE_ESCAPE, KeyEvent.KEYCODE_1, diff --git a/app/src/main/java/com/limelight/binding/video/AndroidCpuDecoderRenderer.java b/app/src/main/java/com/limelight/binding/video/AndroidCpuDecoderRenderer.java index c43fd930..60ed4297 100644 --- a/app/src/main/java/com/limelight/binding/video/AndroidCpuDecoderRenderer.java +++ b/app/src/main/java/com/limelight/binding/video/AndroidCpuDecoderRenderer.java @@ -36,7 +36,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer { private int totalFrames; private long totalTimeMs; - private int cpuCount = Runtime.getRuntime().availableProcessors(); + private final int cpuCount = Runtime.getRuntime().availableProcessors(); @SuppressWarnings("unused") private int findOptimalPerformanceLevel() { diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java index 178f8662..5b73ed8a 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java @@ -558,8 +558,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { public class RendererException extends RuntimeException { private static final long serialVersionUID = 8985937536997012406L; - private Exception originalException; - private MediaCodecDecoderRenderer renderer; + private final Exception originalException; + private final MediaCodecDecoderRenderer renderer; private ByteBuffer currentBuffer; private int currentCodecFlags; diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java index e106271e..76f96104 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java @@ -20,12 +20,12 @@ import com.limelight.LimeLog; public class MediaCodecHelper { - public static final List preferredDecoders; + private static final List preferredDecoders; - public static final List blacklistedDecoderPrefixes; - public static final List spsFixupBitstreamFixupDecoderPrefixes; - public static final List whitelistedAdaptiveResolutionPrefixes; - public static final List baselineProfileHackPrefixes; + private static final List blacklistedDecoderPrefixes; + private static final List spsFixupBitstreamFixupDecoderPrefixes; + private static final List whitelistedAdaptiveResolutionPrefixes; + private static final List baselineProfileHackPrefixes; static { preferredDecoders = new LinkedList(); @@ -146,7 +146,7 @@ public class MediaCodecHelper { return str; } - public static MediaCodecInfo findPreferredDecoder() { + private 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") - public static MediaCodecInfo findKnownSafeDecoder() throws Exception { + private static MediaCodecInfo findKnownSafeDecoder() throws Exception { for (MediaCodecInfo codecInfo : getMediaCodecList()) { // Skip encoders if (codecInfo.isEncoder()) { diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index 3112399b..a35451be 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -33,15 +33,15 @@ public class ComputerManagerService extends Service { private static final int POLLING_PERIOD_MS = 3000; private static final int MDNS_QUERY_PERIOD_MS = 1000; - private ComputerManagerBinder binder = new ComputerManagerBinder(); + private final ComputerManagerBinder binder = new ComputerManagerBinder(); private ComputerDatabaseManager dbManager; - private AtomicInteger dbRefCount = new AtomicInteger(0); + private final AtomicInteger dbRefCount = new AtomicInteger(0); private IdentityManager idManager; private final LinkedList pollingTuples = new LinkedList(); private ComputerManagerListener listener = null; - private AtomicInteger activePolls = new AtomicInteger(0); + private final AtomicInteger activePolls = new AtomicInteger(0); private boolean pollingActive = false; private DiscoveryService.DiscoveryBinder discoveryBinder; @@ -491,8 +491,8 @@ public class ComputerManagerService extends Service { public class ApplistPoller { private Thread thread; - private ComputerDetails computer; - private Object pollEvent = new Object(); + private final ComputerDetails computer; + private final Object pollEvent = new Object(); public ApplistPoller(ComputerDetails computer) { this.computer = computer; @@ -593,7 +593,7 @@ public class ComputerManagerService extends Service { class PollingTuple { public Thread thread; - public ComputerDetails computer; + public final ComputerDetails computer; public PollingTuple(ComputerDetails computer, Thread thread) { this.computer = computer; diff --git a/app/src/main/java/com/limelight/discovery/DiscoveryService.java b/app/src/main/java/com/limelight/discovery/DiscoveryService.java index 8bf9cb3a..7e6497fa 100644 --- a/app/src/main/java/com/limelight/discovery/DiscoveryService.java +++ b/app/src/main/java/com/limelight/discovery/DiscoveryService.java @@ -70,7 +70,7 @@ public class DiscoveryService extends Service { }); } - private DiscoveryBinder binder = new DiscoveryBinder(); + private final DiscoveryBinder binder = new DiscoveryBinder(); @Override public IBinder onBind(Intent intent) { diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index 2de6399a..d30f75e7 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -51,10 +51,10 @@ import java.security.cert.X509Certificate; public class AppGridAdapter extends GenericGridAdapter { - private ComputerDetails computer; - private String uniqueId; - private LimelightCryptoProvider cryptoProvider; - private SSLContext sslContext; + private final ComputerDetails computer; + private final String uniqueId; + private final LimelightCryptoProvider cryptoProvider; + private final SSLContext sslContext; private final HashMap pendingRequests = new HashMap(); public AppGridAdapter(Context context, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException { @@ -69,7 +69,7 @@ public class AppGridAdapter extends GenericGridAdapter { sslContext.init(ourKeyman, trustAllCerts, new SecureRandom()); } - TrustManager[] trustAllCerts = new TrustManager[] { + private final TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; @@ -78,7 +78,7 @@ public class AppGridAdapter extends GenericGridAdapter { public void checkServerTrusted(X509Certificate[] certs, String authType) {} }}; - KeyManager[] ourKeyman = new KeyManager[] { + private final KeyManager[] ourKeyman = new KeyManager[] { new X509KeyManager() { public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) { @@ -203,8 +203,8 @@ public class AppGridAdapter extends GenericGridAdapter { } private class ImageCacheRequest extends AsyncTask { - private ImageView view; - private int appId; + private final ImageView view; + private final int appId; public ImageCacheRequest(ImageView view, int appId) { this.view = view; @@ -223,7 +223,7 @@ public class AppGridAdapter extends GenericGridAdapter { if (in != null) { try { in.close(); - } catch (IOException e) {} + } catch (IOException ignored) {} } } return null; @@ -249,6 +249,7 @@ public class AppGridAdapter extends GenericGridAdapter { // Set SSL contexts correctly to allow us to authenticate Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts); Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext); + Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setHostnameVerifier(hv); // Kick off the deferred image load synchronized (pendingRequests) { diff --git a/app/src/main/java/com/limelight/grid/GenericGridAdapter.java b/app/src/main/java/com/limelight/grid/GenericGridAdapter.java index f0a17618..439a22b3 100644 --- a/app/src/main/java/com/limelight/grid/GenericGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/GenericGridAdapter.java @@ -13,11 +13,11 @@ import com.limelight.R; import java.util.ArrayList; public abstract class GenericGridAdapter extends BaseAdapter { - protected Context context; - protected int defaultImageRes; - protected int layoutId; - protected ArrayList itemList = new ArrayList(); - protected LayoutInflater inflater; + protected final Context context; + protected final int defaultImageRes; + protected final int layoutId; + protected final ArrayList itemList = new ArrayList(); + protected final LayoutInflater inflater; public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) { this.context = context; diff --git a/app/src/main/java/com/limelight/preferences/AddComputerManually.java b/app/src/main/java/com/limelight/preferences/AddComputerManually.java index 723b2330..71f84746 100644 --- a/app/src/main/java/com/limelight/preferences/AddComputerManually.java +++ b/app/src/main/java/com/limelight/preferences/AddComputerManually.java @@ -27,9 +27,9 @@ import android.widget.Toast; public class AddComputerManually extends Activity { private TextView hostText; private ComputerManagerService.ComputerManagerBinder managerBinder; - private LinkedBlockingQueue computersToAdd = new LinkedBlockingQueue(); + private final LinkedBlockingQueue computersToAdd = new LinkedBlockingQueue(); private Thread addThread; - private ServiceConnection serviceConnection = new ServiceConnection() { + private final ServiceConnection serviceConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, final IBinder binder) { managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder); startAddThread(); diff --git a/app/src/main/java/com/limelight/preferences/SeekBarPreference.java b/app/src/main/java/com/limelight/preferences/SeekBarPreference.java index 9e86d1bd..617e8afb 100644 --- a/app/src/main/java/com/limelight/preferences/SeekBarPreference.java +++ b/app/src/main/java/com/limelight/preferences/SeekBarPreference.java @@ -20,10 +20,14 @@ public class SeekBarPreference extends DialogPreference private SeekBar seekBar; private TextView valueText; - private Context context; + private final Context context; - private String dialogMessage, suffix; - private int defaultValue, maxValue, minValue, currentValue; + private final String dialogMessage; + private final String suffix; + private final int defaultValue; + private final int maxValue; + private final int minValue; + private int currentValue; public SeekBarPreference(Context context, AttributeSet attrs) { super(context, attrs); @@ -127,13 +131,6 @@ public class SeekBarPreference extends DialogPreference } } - public void setMax(int max) { - this.maxValue = max; - } - public int getMax() { - return this.maxValue; - } - public void setProgress(int progress) { this.currentValue = progress; if (seekBar != null) { diff --git a/app/src/main/java/com/limelight/utils/Dialog.java b/app/src/main/java/com/limelight/utils/Dialog.java index eb7cd81b..720f687a 100644 --- a/app/src/main/java/com/limelight/utils/Dialog.java +++ b/app/src/main/java/com/limelight/utils/Dialog.java @@ -7,15 +7,16 @@ import android.app.AlertDialog; import android.content.DialogInterface; public class Dialog implements Runnable { - private String title, message; - private Activity activity; - private boolean endAfterDismiss; + private final String title; + private final String message; + private final Activity activity; + private final boolean endAfterDismiss; private AlertDialog alert; private static final ArrayList rundownDialogs = new ArrayList(); - public Dialog(Activity activity, String title, String message, boolean endAfterDismiss) + private Dialog(Activity activity, String title, String message, boolean endAfterDismiss) { this.activity = activity; this.title = title; diff --git a/app/src/main/java/com/limelight/utils/SpinnerDialog.java b/app/src/main/java/com/limelight/utils/SpinnerDialog.java index 4424a48c..df97b2c3 100644 --- a/app/src/main/java/com/limelight/utils/SpinnerDialog.java +++ b/app/src/main/java/com/limelight/utils/SpinnerDialog.java @@ -9,14 +9,15 @@ import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; public class SpinnerDialog implements Runnable,OnCancelListener { - private String title, message; - private Activity activity; + private final String title; + private final String message; + private final Activity activity; private ProgressDialog progress; - private boolean finish; + private final boolean finish; private static final ArrayList rundownDialogs = new ArrayList(); - public SpinnerDialog(Activity activity, String title, String message, boolean finish) + private SpinnerDialog(Activity activity, String title, String message, boolean finish) { this.activity = activity; this.title = title; From 2247e43a48413e5149ab8a071402a335cc07dd28 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Thu, 5 Feb 2015 13:23:01 -0500 Subject: [PATCH 004/202] Remove unused imports --- app/src/main/java/com/limelight/AppView.java | 8 -------- app/src/main/java/com/limelight/PcView.java | 1 - .../com/limelight/binding/input/ControllerHandler.java | 1 - .../com/limelight/computers/ComputerManagerService.java | 1 - app/src/main/java/com/limelight/grid/AppGridAdapter.java | 6 ------ app/src/main/java/com/limelight/utils/CacheHelper.java | 2 -- 6 files changed, 19 deletions(-) diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index 9218bf6d..6beee6dd 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -1,9 +1,6 @@ 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; @@ -11,15 +8,11 @@ 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; import com.limelight.nvstream.http.ComputerDetails; -import com.limelight.nvstream.http.GfeHttpResponseException; import com.limelight.nvstream.http.NvApp; import com.limelight.nvstream.http.NvHTTP; import com.limelight.preferences.PreferenceConfiguration; @@ -48,7 +41,6 @@ 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; diff --git a/app/src/main/java/com/limelight/PcView.java b/app/src/main/java/com/limelight/PcView.java index c2bcb3e0..e9511dcd 100644 --- a/app/src/main/java/com/limelight/PcView.java +++ b/app/src/main/java/com/limelight/PcView.java @@ -25,7 +25,6 @@ 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; diff --git a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java index 4680ee2c..7bbea5b2 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -3,7 +3,6 @@ package com.limelight.binding.input; import java.util.HashMap; import java.util.Map; -import android.content.Context; import android.hardware.input.InputManager; import android.os.SystemClock; import android.view.InputDevice; diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index a35451be..e9a5481d 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -1,6 +1,5 @@ package com.limelight.computers; -import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.StringReader; diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index d30f75e7..f2fc6665 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -9,7 +9,6 @@ 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; @@ -19,10 +18,6 @@ import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.LimelightCryptoProvider; import com.limelight.utils.CacheHelper; -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -36,7 +31,6 @@ import java.security.SecureRandom; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; -import java.util.Map; import java.util.UUID; import java.util.concurrent.Future; diff --git a/app/src/main/java/com/limelight/utils/CacheHelper.java b/app/src/main/java/com/limelight/utils/CacheHelper.java index d7cee523..1da135a4 100644 --- a/app/src/main/java/com/limelight/utils/CacheHelper.java +++ b/app/src/main/java/com/limelight/utils/CacheHelper.java @@ -9,8 +9,6 @@ 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) { From 47265d0d10eed7d577f05109b267a99258a2932e Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Thu, 5 Feb 2015 16:06:22 -0500 Subject: [PATCH 005/202] Add another SELinux policy change needed on Nexus 9 --- .../java/com/limelight/binding/input/evdev/EvdevReader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/limelight/binding/input/evdev/EvdevReader.java b/app/src/main/java/com/limelight/binding/input/evdev/EvdevReader.java index ddb3a7ba..3ee2ddcd 100644 --- a/app/src/main/java/com/limelight/binding/input/evdev/EvdevReader.java +++ b/app/src/main/java/com/limelight/binding/input/evdev/EvdevReader.java @@ -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 { getattr read search }\" " + + shell.runCommand("supolicy --live \"allow untrusted_app input_device dir { open getattr read search }\" " + "\"allow untrusted_app input_device chr_file { open read write ioctl }\""); } } From b1ea487e2213f3f3a6fdc7b0ed2e9c06d5db722c Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Thu, 5 Feb 2015 16:06:55 -0500 Subject: [PATCH 006/202] Use the mode (power) button on the Asus Nexus Player Gamepad as a select button --- .../com/limelight/binding/input/ControllerHandler.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java index 7bbea5b2..aa84d530 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -262,6 +262,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener { boolean[] hasStartKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_MENU, 0); if (!hasStartKey[0] && !hasStartKey[1]) { context.backIsStart = true; + context.modeIsSelect = true; } } @@ -426,10 +427,18 @@ public class ControllerHandler implements InputManager.InputDeviceListener { // Ensure that we never use back as start if we have a real start context.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) { // 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; } @@ -789,6 +798,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; From a095c10a2549cf759ec0d5145305c32cbf7a5174 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Thu, 5 Feb 2015 16:15:16 -0500 Subject: [PATCH 007/202] Increment version to 3.1 and update build files --- app/app.iml | 2 ++ app/build.gradle | 4 ++-- limelight-android.iml | 2 -- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/app.iml b/app/app.iml index b0810f46..87814d7a 100644 --- a/app/app.iml +++ b/app/app.iml @@ -9,6 +9,7 @@ + diff --git a/app/build.gradle b/app/build.gradle index fa4f859b..6f5d10e7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { minSdkVersion 16 targetSdkVersion 21 - versionName "3.1-beta2" - versionCode = 51 + versionName "3.1" + versionCode = 53 } productFlavors { diff --git a/limelight-android.iml b/limelight-android.iml index 42f4ed55..0bb6048a 100644 --- a/limelight-android.iml +++ b/limelight-android.iml @@ -8,8 +8,6 @@ - - From e1a1a6344d1d27c74261771f9d17dd89086b1f91 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 6 Feb 2015 13:38:32 -0500 Subject: [PATCH 008/202] Fill the whole height with the list view --- app/src/main/res/layout/list_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/list_view.xml b/app/src/main/res/layout/list_view.xml index c41508ee..e303b15c 100644 --- a/app/src/main/res/layout/list_view.xml +++ b/app/src/main/res/layout/list_view.xml @@ -6,7 +6,7 @@ Date: Sat, 7 Feb 2015 05:57:30 -0500 Subject: [PATCH 009/202] Replace unpair option with delete PC --- app/src/main/java/com/limelight/PcView.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/limelight/PcView.java b/app/src/main/java/com/limelight/PcView.java index e9511dcd..9de1185e 100644 --- a/app/src/main/java/com/limelight/PcView.java +++ b/app/src/main/java/com/limelight/PcView.java @@ -254,7 +254,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { } else { menu.add(Menu.NONE, APP_LIST_ID, 1, getResources().getString(R.string.pcview_menu_app_list)); - menu.add(Menu.NONE, UNPAIR_ID, 2, getResources().getString(R.string.pcview_menu_unpair_pc)); + + // FIXME: We used to be able to unpair here but it's been broken since GFE 2.1.x, so I've replaced + // it with delete which actually work + menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc)); } } From a8bf2cd1cf58e5a68f94c1a7a4287c690ae66b8c Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 7 Feb 2015 06:08:00 -0500 Subject: [PATCH 010/202] Fix UI dropped frames when loading images --- .../java/com/limelight/grid/AppGridAdapter.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index f2fc6665..e2da702a 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -253,7 +253,7 @@ public class AppGridAdapter extends GenericGridAdapter { .asBitmap() .setCallback(new FutureCallback() { @Override - public void onCompleted(Exception e, Bitmap result) { + public void onCompleted(Exception e, final Bitmap result) { synchronized (pendingRequests) { pendingRequests.remove(view); } @@ -263,8 +263,15 @@ public class AppGridAdapter extends GenericGridAdapter { view.setImageBitmap(result); view.setVisibility(View.VISIBLE); - // Populate the disk cache if we got an image back - populateBitmapCache(computer.uuid, appId, result); + // Populate the disk cache if we got an image back. + // We do it in a new thread because it can be very expensive, especially + // when we do the initial load where lots of disk I/O is happening at once. + new Thread() { + @Override + public void run() { + populateBitmapCache(computer.uuid, appId, result); + } + }.start(); } else { // Leave the loading icon as is (probably should change this eventually...) From 265b3f9963306bb2e55b505fda60e35f9bb5242f Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 7 Feb 2015 06:23:35 -0500 Subject: [PATCH 011/202] Use image alpha to make images transparent while loading --- .../main/java/com/limelight/grid/AppGridAdapter.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index e2da702a..a817d694 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -4,7 +4,6 @@ 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; @@ -166,8 +165,8 @@ public class AppGridAdapter extends GenericGridAdapter { @Override public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) { - // Hide the image view while we're loading the image from disk cache - imgView.setVisibility(View.INVISIBLE); + // Clear existing contents of the image view + imgView.setImageAlpha(0); // Check the on-disk cache new ImageCacheRequest(imgView, obj.app.getAppId()).execute(); @@ -229,7 +228,7 @@ public class AppGridAdapter extends GenericGridAdapter { // Disk cache was read successfully LimeLog.info("Image disk cache hit for ("+computer.uuid+", "+appId+")"); view.setImageBitmap(result); - view.setVisibility(View.VISIBLE); + view.setImageAlpha(255); } else { LimeLog.info("Image disk cache miss for ("+computer.uuid+", "+appId+")"); @@ -238,7 +237,7 @@ public class AppGridAdapter extends GenericGridAdapter { // Load the placeholder image view.setImageResource(defaultImageRes); - view.setVisibility(View.VISIBLE); + view.setImageAlpha(255); // Set SSL contexts correctly to allow us to authenticate Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts); @@ -261,7 +260,7 @@ public class AppGridAdapter extends GenericGridAdapter { if (result != null) { // Make the view visible now view.setImageBitmap(result); - view.setVisibility(View.VISIBLE); + view.setImageAlpha(255); // Populate the disk cache if we got an image back. // We do it in a new thread because it can be very expensive, especially From 55c800c2a5d60b4e54751f5d02db35875cdfbb93 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 7 Feb 2015 06:52:28 -0500 Subject: [PATCH 012/202] Fade in box art when scrolling --- .../java/com/limelight/grid/AppGridAdapter.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index a817d694..b2ca369e 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -166,7 +166,7 @@ public class AppGridAdapter extends GenericGridAdapter { @Override public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) { // Clear existing contents of the image view - imgView.setImageAlpha(0); + imgView.setAlpha(0.0f); // Check the on-disk cache new ImageCacheRequest(imgView, obj.app.getAppId()).execute(); @@ -222,13 +222,17 @@ public class AppGridAdapter extends GenericGridAdapter { return null; } + private void fadeInImage(ImageView view) { + view.animate().alpha(1.0f).setDuration(250).start(); + } + @Override protected void onPostExecute(Bitmap result) { if (result != null) { // Disk cache was read successfully - LimeLog.info("Image disk cache hit for ("+computer.uuid+", "+appId+")"); + LimeLog.info("Image disk cache hit for (" + computer.uuid + ", " + appId + ")"); view.setImageBitmap(result); - view.setImageAlpha(255); + fadeInImage(view); } else { LimeLog.info("Image disk cache miss for ("+computer.uuid+", "+appId+")"); @@ -237,7 +241,7 @@ public class AppGridAdapter extends GenericGridAdapter { // Load the placeholder image view.setImageResource(defaultImageRes); - view.setImageAlpha(255); + fadeInImage(view); // Set SSL contexts correctly to allow us to authenticate Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts); @@ -260,7 +264,7 @@ public class AppGridAdapter extends GenericGridAdapter { if (result != null) { // Make the view visible now view.setImageBitmap(result); - view.setImageAlpha(255); + fadeInImage(view); // Populate the disk cache if we got an image back. // We do it in a new thread because it can be very expensive, especially From 10204afdb4f6db1875c245481356d01ed4de5b3c Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 7 Feb 2015 11:44:56 -0500 Subject: [PATCH 013/202] Only add PCs to the computer list when they have been polled once to get a UUID for equality comparison. Fix equality comparison in PcView to avoid duplicate PCs enumerated over mDNS. --- app/src/main/java/com/limelight/PcView.java | 2 +- .../computers/ComputerManagerService.java | 38 ++++--------------- 2 files changed, 9 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/limelight/PcView.java b/app/src/main/java/com/limelight/PcView.java index 9de1185e..74c74474 100644 --- a/app/src/main/java/com/limelight/PcView.java +++ b/app/src/main/java/com/limelight/PcView.java @@ -530,7 +530,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i); // Check if this is the same computer - if (details.equals(computer.details)) { + if (details.uuid.equals(computer.details.uuid)) { existingEntry = computer; break; } diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index e9a5481d..b85b0bf6 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -64,10 +64,8 @@ public class ComputerManagerService extends Service { }; // Returns true if the details object was modified - private boolean runPoll(ComputerDetails details) + private boolean runPoll(ComputerDetails details, boolean newPc) { - boolean newPc = details.name.isEmpty(); - if (!getLocalDatabaseReference()) { return false; } @@ -112,7 +110,7 @@ public class ComputerManagerService extends Service { public void run() { while (!isInterrupted() && pollingActive) { // Check if this poll has modified the details - runPoll(details); + runPoll(details, false); // Wait until the next polling interval try { @@ -176,10 +174,6 @@ 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); } @@ -238,7 +232,7 @@ public class ComputerManagerService extends Service { @Override public void notifyComputerAdded(MdnsComputer computer) { // Kick off a serverinfo poll on this machine - addComputer(computer.getAddress()); + addComputerBlocking(computer.getAddress()); } @Override @@ -254,28 +248,11 @@ public class ComputerManagerService extends Service { }; } - public void addComputer(InetAddress addr) { - // Setup a placeholder - ComputerDetails fakeDetails = new ComputerDetails(); - fakeDetails.localIp = addr; - fakeDetails.remoteIp = addr; - fakeDetails.name = ""; - - addTuple(fakeDetails); - } - private void addTuple(ComputerDetails details) { synchronized (pollingTuples) { for (PollingTuple tuple : pollingTuples) { // Check if this is the same computer - 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))) { - + if (tuple.computer.uuid.equals(details.uuid)) { // 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; @@ -306,13 +283,14 @@ 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 - runPoll(fakeDetails); + runPoll(fakeDetails, true); // 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; From 2fdecc551a5ab30223af762844f9b0a9b6a24581 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 7 Feb 2015 11:54:46 -0500 Subject: [PATCH 014/202] Tabs -> Spaces --- app/src/main/java/com/limelight/AppView.java | 222 +-- app/src/main/java/com/limelight/Game.java | 1416 ++++++++--------- app/src/main/java/com/limelight/PcView.java | 804 +++++----- .../limelight/binding/PlatformBinding.java | 22 +- .../binding/audio/AndroidAudioRenderer.java | 74 +- .../binding/crypto/AndroidCryptoProvider.java | 460 +++--- .../binding/input/ControllerHandler.java | 790 ++++----- .../binding/input/KeyboardTranslator.java | 4 +- .../limelight/binding/input/TouchContext.java | 158 +- .../binding/input/evdev/EvdevEvent.java | 74 +- .../binding/input/evdev/EvdevHandler.java | 296 ++-- .../binding/input/evdev/EvdevListener.java | 16 +- .../binding/input/evdev/EvdevReader.java | 140 +- .../binding/input/evdev/EvdevTranslator.java | 262 +-- .../binding/input/evdev/EvdevWatcher.java | 318 ++-- .../video/AndroidCpuDecoderRenderer.java | 428 ++--- .../video/ConfigurableDecoderRenderer.java | 126 +- .../video/MediaCodecDecoderRenderer.java | 992 ++++++------ .../binding/video/MediaCodecHelper.java | 4 +- .../computers/ComputerDatabaseManager.java | 280 ++-- .../computers/ComputerManagerListener.java | 2 +- .../computers/ComputerManagerService.java | 460 +++--- .../limelight/computers/IdentityManager.java | 142 +- .../limelight/discovery/DiscoveryService.java | 140 +- .../nvstream/av/video/cpu/AvcDecoder.java | 78 +- .../preferences/AddComputerManually.java | 226 +-- .../main/java/com/limelight/utils/Dialog.java | 126 +- .../com/limelight/utils/SpinnerDialog.java | 210 +-- 28 files changed, 4135 insertions(+), 4135 deletions(-) diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index 6beee6dd..baefa505 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -57,13 +57,13 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { 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; + private final static int START_OR_RESUME_ID = 1; + private final static int QUIT_ID = 2; + private final static int CANCEL_ID = 3; private final static int START_WTIH_QUIT = 4; public final static String NAME_EXTRA = "Name"; - public final static String UUID_EXTRA = "UUID"; + public final static String UUID_EXTRA = "UUID"; private ComputerManagerService.ComputerManagerBinder managerBinder; private final ServiceConnection serviceConnection = new ServiceConnection() { @@ -183,33 +183,33 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { managerBinder.stopPolling(); } } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); String locale = PreferenceConfiguration.readPreferences(this).language; - if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) { - Configuration config = new Configuration(getResources().getConfiguration()); - config.locale = new Locale(locale); - getResources().updateConfiguration(config, getResources().getDisplayMetrics()); - } + if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) { + Configuration config = new Configuration(getResources().getConfiguration()); + config.locale = new Locale(locale); + getResources().updateConfiguration(config, getResources().getDisplayMetrics()); + } - setContentView(R.layout.activity_app_view); + setContentView(R.layout.activity_app_view); UiHelper.notifyNewRootView(this); uuidString = getIntent().getStringExtra(UUID_EXTRA); - - String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA); - TextView label = (TextView) findViewById(R.id.appListText); - setTitle(labelText); - label.setText(labelText); + + String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA); + TextView label = (TextView) findViewById(R.id.appListText); + setTitle(labelText); + label.setText(labelText); // Bind to the computer manager service bindService(new Intent(this, ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE); - } + } private void populateAppGridWithCache() { try { @@ -233,25 +233,25 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title), getResources().getString(R.string.applist_refresh_msg), true); } - - @Override - protected void onDestroy() { - super.onDestroy(); - - SpinnerDialog.closeDialogs(this); - Dialog.closeDialogs(); + + @Override + protected void onDestroy() { + super.onDestroy(); + + SpinnerDialog.closeDialogs(this); + Dialog.closeDialogs(); if (managerBinder != null) { unbindService(serviceConnection); } - } - - @Override - protected void onResume() { - super.onResume(); + } + + @Override + protected void onResume() { + super.onResume(); startComputerUpdates(); - } + } @Override protected void onPause() { @@ -259,49 +259,49 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { stopComputerUpdates(); } - - private int getRunningAppId() { + + private int getRunningAppId() { 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; - } + AppObject app = (AppObject) appGridAdapter.getItem(i); + if (app.app == null) { + continue; + } + + if (app.app.getIsRunning()) { + runningAppId = app.app.getAppId(); + break; + } } return runningAppId; - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position); if (selectedApp == null || selectedApp.app == null) { - return; + return; } int runningAppId = getRunningAppId(); if (runningAppId != -1) { - if (runningAppId == selectedApp.app.getAppId()) { + if (runningAppId == selectedApp.app.getAppId()) { menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume)); menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit)); - } - else { + } + else { menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start)); menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel)); - } + } } } - - @Override - public void onContextMenuClosed(Menu menu) { - } + + @Override + public void onContextMenuClosed(Menu menu) { + } private void displayQuitConfirmationDialog(final Runnable onYes, final Runnable onNo) { DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { @@ -414,57 +414,57 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { }); } - private void doStart(NvApp app) { - Intent intent = new Intent(this, Game.class); - intent.putExtra(Game.EXTRA_HOST, + private void doStart(NvApp app) { + Intent intent = new Intent(this, Game.class); + intent.putExtra(Game.EXTRA_HOST, computer.reachability == ComputerDetails.Reachability.LOCAL ? computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress()); - intent.putExtra(Game.EXTRA_APP, app.getAppName()); - intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId()); - intent.putExtra(Game.EXTRA_STREAMING_REMOTE, + intent.putExtra(Game.EXTRA_APP, app.getAppName()); + intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId()); + intent.putExtra(Game.EXTRA_STREAMING_REMOTE, computer.reachability != ComputerDetails.Reachability.LOCAL); - startActivity(intent); - } - - private void doQuit(final NvApp app) { - Toast.makeText(AppView.this, getResources().getString(R.string.applist_quit_app)+" "+app.getAppName()+"...", Toast.LENGTH_SHORT).show(); - new Thread(new Runnable() { - @Override - public void run() { - NvHTTP httpConn; - String message; - try { - httpConn = new NvHTTP(getAddress(), + startActivity(intent); + } + + private void doQuit(final NvApp app) { + Toast.makeText(AppView.this, getResources().getString(R.string.applist_quit_app)+" "+app.getAppName()+"...", Toast.LENGTH_SHORT).show(); + new Thread(new Runnable() { + @Override + public void run() { + NvHTTP httpConn; + String message; + try { + httpConn = new NvHTTP(getAddress(), managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(AppView.this)); - if (httpConn.quitApp()) { - message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName(); - } - else { - message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName(); - } - } catch (UnknownHostException e) { - message = getResources().getString(R.string.error_unknown_host); - } catch (FileNotFoundException e) { - message = getResources().getString(R.string.error_404); - } catch (Exception e) { - message = e.getMessage(); - } finally { + if (httpConn.quitApp()) { + message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName(); + } + else { + message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName(); + } + } catch (UnknownHostException e) { + message = getResources().getString(R.string.error_unknown_host); + } catch (FileNotFoundException e) { + message = getResources().getString(R.string.error_404); + } catch (Exception e) { + message = e.getMessage(); + } finally { // Trigger a poll immediately if (poller != null) { poller.pollNow(); } } - - final String toastMessage = message; - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show(); - } - }); - } - }).start(); - } + + final String toastMessage = message; + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show(); + } + }); + } + }).start(); + } @Override public int getAdapterFragmentLayoutId() { @@ -497,15 +497,15 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { } public class AppObject { - public final NvApp app; - - public AppObject(NvApp app) { - this.app = app; - } - - @Override - public String toString() { - return app.getAppName(); - } - } + public final NvApp app; + + public AppObject(NvApp app) { + this.app = app; + } + + @Override + public String toString() { + return app.getAppName(); + } + } } diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index a5bef3e1..0e327ae1 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -52,133 +52,133 @@ import java.util.Locale; public class Game extends Activity implements SurfaceHolder.Callback, - OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener, - OnSystemUiVisibilityChangeListener, GameGestures + OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener, + OnSystemUiVisibilityChangeListener, GameGestures { - private int lastMouseX = Integer.MIN_VALUE; - private int lastMouseY = Integer.MIN_VALUE; - private int lastButtonState = 0; - - // Only 2 touches are supported - private final TouchContext[] touchContextMap = new TouchContext[2]; + private int lastMouseX = Integer.MIN_VALUE; + private int lastMouseY = Integer.MIN_VALUE; + private int lastButtonState = 0; + + // Only 2 touches are supported + private final TouchContext[] touchContextMap = new TouchContext[2]; private long threeFingerDownTime = 0; private static final int THREE_FINGER_TAP_THRESHOLD = 300; - - private ControllerHandler controllerHandler; - private KeyboardTranslator keybTranslator; - - private PreferenceConfiguration prefConfig; - private final Point screenSize = new Point(0, 0); - - private NvConnection conn; - private SpinnerDialog spinner; - private boolean displayedFailureDialog = false; - private boolean connecting = false; - private boolean connected = false; - - private EvdevWatcher evdevWatcher; - private int modifierFlags = 0; - private boolean grabbedInput = true; - private boolean grabComboDown = false; - - private ConfigurableDecoderRenderer decoderRenderer; - - private WifiManager.WifiLock wifiLock; - - private int drFlags = 0; - - public static final String EXTRA_HOST = "Host"; - public static final String EXTRA_APP = "App"; - public static final String EXTRA_UNIQUEID = "UniqueId"; - public static final String EXTRA_STREAMING_REMOTE = "Remote"; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + + private ControllerHandler controllerHandler; + private KeyboardTranslator keybTranslator; + + private PreferenceConfiguration prefConfig; + private final Point screenSize = new Point(0, 0); + + private NvConnection conn; + private SpinnerDialog spinner; + private boolean displayedFailureDialog = false; + private boolean connecting = false; + private boolean connected = false; + + private EvdevWatcher evdevWatcher; + private int modifierFlags = 0; + private boolean grabbedInput = true; + private boolean grabComboDown = false; + + private ConfigurableDecoderRenderer decoderRenderer; + + private WifiManager.WifiLock wifiLock; + + private int drFlags = 0; + + public static final String EXTRA_HOST = "Host"; + public static final String EXTRA_APP = "App"; + public static final String EXTRA_UNIQUEID = "UniqueId"; + public static final String EXTRA_STREAMING_REMOTE = "Remote"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); String locale = PreferenceConfiguration.readPreferences(this).language; if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) { - Configuration config = new Configuration(getResources().getConfiguration()); - config.locale = new Locale(locale); - getResources().updateConfiguration(config, getResources().getDisplayMetrics()); - } - - // We don't want a title bar - requestWindowFeature(Window.FEATURE_NO_TITLE); - - // Full-screen and don't let the display go off - getWindow().addFlags( - WindowManager.LayoutParams.FLAG_FULLSCREEN | - WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - - // If we're going to use immersive mode, we want to have - // the entire screen - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { - getWindow().getDecorView().setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); - - getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN); - } - - // Listen for UI visibility events - getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this); - - // Change volume button behavior - setVolumeControlStream(AudioManager.STREAM_MUSIC); - - // Inflate the content - setContentView(R.layout.activity_game); - - // Start the spinner - spinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title), - getResources().getString(R.string.conn_establishing_msg), true); - - // Read the stream preferences - prefConfig = PreferenceConfiguration.readPreferences(this); - switch (prefConfig.decoder) { - case PreferenceConfiguration.FORCE_SOFTWARE_DECODER: - drFlags |= VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING; - break; - case PreferenceConfiguration.AUTOSELECT_DECODER: - break; - case PreferenceConfiguration.FORCE_HARDWARE_DECODER: - drFlags |= VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING; - break; - } - - if (prefConfig.stretchVideo) { - drFlags |= VideoDecoderRenderer.FLAG_FILL_SCREEN; - } - - Display display = getWindowManager().getDefaultDisplay(); - display.getSize(screenSize); - - // Listen for events on the game surface - SurfaceView sv = (SurfaceView) findViewById(R.id.surfaceView); - sv.setOnGenericMotionListener(this); - sv.setOnTouchListener(this); - - // Warn the user if they're on a metered connection - checkDataConnection(); - - // Make sure Wi-Fi is fully powered up - WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE); - wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Limelight"); - wifiLock.setReferenceCounted(false); - wifiLock.acquire(); - - String host = Game.this.getIntent().getStringExtra(EXTRA_HOST); - String app = Game.this.getIntent().getStringExtra(EXTRA_APP); - String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID); + Configuration config = new Configuration(getResources().getConfiguration()); + config.locale = new Locale(locale); + getResources().updateConfiguration(config, getResources().getDisplayMetrics()); + } + + // We don't want a title bar + requestWindowFeature(Window.FEATURE_NO_TITLE); + + // Full-screen and don't let the display go off + getWindow().addFlags( + WindowManager.LayoutParams.FLAG_FULLSCREEN | + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + + // If we're going to use immersive mode, we want to have + // the entire screen + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { + getWindow().getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + + getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN); + } + + // Listen for UI visibility events + getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this); + + // Change volume button behavior + setVolumeControlStream(AudioManager.STREAM_MUSIC); + + // Inflate the content + setContentView(R.layout.activity_game); + + // Start the spinner + spinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title), + getResources().getString(R.string.conn_establishing_msg), true); + + // Read the stream preferences + prefConfig = PreferenceConfiguration.readPreferences(this); + switch (prefConfig.decoder) { + case PreferenceConfiguration.FORCE_SOFTWARE_DECODER: + drFlags |= VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING; + break; + case PreferenceConfiguration.AUTOSELECT_DECODER: + break; + case PreferenceConfiguration.FORCE_HARDWARE_DECODER: + drFlags |= VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING; + break; + } + + if (prefConfig.stretchVideo) { + drFlags |= VideoDecoderRenderer.FLAG_FILL_SCREEN; + } + + Display display = getWindowManager().getDefaultDisplay(); + display.getSize(screenSize); + + // Listen for events on the game surface + SurfaceView sv = (SurfaceView) findViewById(R.id.surfaceView); + sv.setOnGenericMotionListener(this); + sv.setOnTouchListener(this); + + // Warn the user if they're on a metered connection + checkDataConnection(); + + // Make sure Wi-Fi is fully powered up + WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE); + wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Limelight"); + wifiLock.setReferenceCounted(false); + wifiLock.acquire(); + + String host = Game.this.getIntent().getStringExtra(EXTRA_HOST); + 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); - - decoderRenderer = new ConfigurableDecoderRenderer(); - decoderRenderer.initializeWithFlags(drFlags); + + decoderRenderer = new ConfigurableDecoderRenderer(); + decoderRenderer.initializeWithFlags(drFlags); - StreamConfiguration config = new StreamConfiguration.Builder() + StreamConfiguration config = new StreamConfiguration.Builder() .setResolution(prefConfig.width, prefConfig.height) .setRefreshRate(prefConfig.fps) .setApp(app) @@ -191,305 +191,305 @@ public class Game extends Activity implements SurfaceHolder.Callback, .setRemote(remote) .build(); - // 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); + // 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); - - SurfaceHolder sh = sv.getHolder(); - if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) { - // Set the surface to the size of the video - sh.setFixedSize(prefConfig.width, prefConfig.height); - } - - // Initialize touch contexts - for (int i = 0; i < touchContextMap.length; i++) { - touchContextMap[i] = new TouchContext(conn, i, + + SurfaceHolder sh = sv.getHolder(); + if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) { + // Set the surface to the size of the video + sh.setFixedSize(prefConfig.width, prefConfig.height); + } + + // Initialize touch contexts + for (int i = 0; i < touchContextMap.length; i++) { + touchContextMap[i] = new TouchContext(conn, i, ((double)prefConfig.width / (double)screenSize.x), ((double)prefConfig.height / (double)screenSize.y)); - } - - if (LimelightBuildProps.ROOT_BUILD) { - // Start watching for raw input - evdevWatcher = new EvdevWatcher(this); - evdevWatcher.start(); - } - - // The connection will be started when the surface gets created - sh.addCallback(this); - } - - private void resizeSurfaceWithAspectRatio(SurfaceView sv, double vidWidth, double vidHeight) - { - // Get the visible width of the activity - double visibleWidth = getWindow().getDecorView().getWidth(); - - ViewGroup.LayoutParams lp = sv.getLayoutParams(); - - // Calculate the new size of the SurfaceView - lp.width = (int) visibleWidth; - lp.height = (int) ((vidHeight / vidWidth) * visibleWidth); + } - // Apply the size change - sv.setLayoutParams(lp); - } - - private void checkDataConnection() - { - ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - if (mgr.isActiveNetworkMetered()) { - displayTransientMessage(getResources().getString(R.string.conn_metered)); - } - } + if (LimelightBuildProps.ROOT_BUILD) { + // Start watching for raw input + evdevWatcher = new EvdevWatcher(this); + evdevWatcher.start(); + } - @SuppressLint("InlinedApi") - private final Runnable hideSystemUi = new Runnable() { - @Override - public void run() { - // Use immersive mode on 4.4+ or standard low profile on previous builds - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { - Game.this.getWindow().getDecorView().setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_FULLSCREEN | - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - } - else { - Game.this.getWindow().getDecorView().setSystemUiVisibility( - View.SYSTEM_UI_FLAG_FULLSCREEN | - View.SYSTEM_UI_FLAG_LOW_PROFILE); - } - } - }; + // The connection will be started when the surface gets created + sh.addCallback(this); + } - private void hideSystemUi(int delay) { - Handler h = getWindow().getDecorView().getHandler(); - if (h != null) { - h.removeCallbacks(hideSystemUi); - h.postDelayed(hideSystemUi, delay); - } - } - - @Override - protected void onStop() { - super.onStop(); - - SpinnerDialog.closeDialogs(this); - Dialog.closeDialogs(); + private void resizeSurfaceWithAspectRatio(SurfaceView sv, double vidWidth, double vidHeight) + { + // Get the visible width of the activity + double visibleWidth = getWindow().getDecorView().getWidth(); + + ViewGroup.LayoutParams lp = sv.getLayoutParams(); + + // Calculate the new size of the SurfaceView + lp.width = (int) visibleWidth; + lp.height = (int) ((vidHeight / vidWidth) * visibleWidth); + + // Apply the size change + sv.setLayoutParams(lp); + } + + private void checkDataConnection() + { + ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + if (mgr.isActiveNetworkMetered()) { + displayTransientMessage(getResources().getString(R.string.conn_metered)); + } + } + + @SuppressLint("InlinedApi") + private final Runnable hideSystemUi = new Runnable() { + @Override + public void run() { + // Use immersive mode on 4.4+ or standard low profile on previous builds + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { + Game.this.getWindow().getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } + else { + Game.this.getWindow().getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_LOW_PROFILE); + } + } + }; + + private void hideSystemUi(int delay) { + Handler h = getWindow().getDecorView().getHandler(); + if (h != null) { + h.removeCallbacks(hideSystemUi); + h.postDelayed(hideSystemUi, delay); + } + } + + @Override + protected void onStop() { + super.onStop(); + + SpinnerDialog.closeDialogs(this); + Dialog.closeDialogs(); InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE); inputManager.unregisterInputDeviceListener(controllerHandler); - - displayedFailureDialog = true; - stopConnection(); - - int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency(); - int averageDecoderLat = decoderRenderer.getAverageDecoderLatency(); - String message = null; - if (averageEndToEndLat > 0) { - message = getResources().getString(R.string.conn_client_latency)+" "+averageEndToEndLat+" ms"; - if (averageDecoderLat > 0) { - message += " ("+getResources().getString(R.string.conn_client_latency_hw)+" "+averageDecoderLat+" ms)"; - } - } - else if (averageDecoderLat > 0) { - message = getResources().getString(R.string.conn_hardware_latency)+" "+averageDecoderLat+" ms"; - } - - if (message != null) { - Toast.makeText(this, message, Toast.LENGTH_LONG).show(); - } - finish(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - - wifiLock.release(); - } - - private final Runnable toggleGrab = new Runnable() { - @Override - public void run() { - - if (evdevWatcher != null) { - if (grabbedInput) { - evdevWatcher.ungrabAll(); - } - else { - evdevWatcher.regrabAll(); - } - } - - grabbedInput = !grabbedInput; - } - }; - - // Returns true if the key stroke was consumed - private boolean handleSpecialKeys(short translatedKey, boolean down) { - int modifierMask = 0; - - // Mask off the high byte - translatedKey &= 0xff; - - if (translatedKey == KeyboardTranslator.VK_CONTROL) { - modifierMask = KeyboardPacket.MODIFIER_CTRL; - } - else if (translatedKey == KeyboardTranslator.VK_SHIFT) { - modifierMask = KeyboardPacket.MODIFIER_SHIFT; - } - else if (translatedKey == KeyboardTranslator.VK_ALT) { - modifierMask = KeyboardPacket.MODIFIER_ALT; - } - - if (down) { - this.modifierFlags |= modifierMask; - } - else { - this.modifierFlags &= ~modifierMask; - } - - // Check if Ctrl+Shift+Z is pressed - if (translatedKey == KeyboardTranslator.VK_Z && - (modifierFlags & (KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_SHIFT)) == - (KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_SHIFT)) - { - if (down) { - // Now that we've pressed the magic combo - // we'll wait for one of the keys to come up - grabComboDown = true; - } - else { - // Toggle the grab if Z comes up - Handler h = getWindow().getDecorView().getHandler(); - if (h != null) { - h.postDelayed(toggleGrab, 250); - } - - grabComboDown = false; - } - - return true; - } - // Toggle the grab if control or shift comes up - else if (grabComboDown) { - Handler h = getWindow().getDecorView().getHandler(); - if (h != null) { - h.postDelayed(toggleGrab, 250); - } - - grabComboDown = false; - return true; - } - - // Not a special combo - return false; - } - - private static byte getModifierState(KeyEvent event) { - byte modifier = 0; - if (event.isShiftPressed()) { - modifier |= KeyboardPacket.MODIFIER_SHIFT; - } - if (event.isCtrlPressed()) { - modifier |= KeyboardPacket.MODIFIER_CTRL; - } - if (event.isAltPressed()) { - modifier |= KeyboardPacket.MODIFIER_ALT; - } - return modifier; - } - - private byte getModifierState() { - return (byte) modifierFlags; - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - // Pass-through virtual navigation keys - if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) { - return super.onKeyDown(keyCode, event); - } - - // Try the controller handler first - boolean handled = controllerHandler.handleButtonDown(event); - if (!handled) { - // Try the keyboard handler - short translated = keybTranslator.translate(event.getKeyCode()); - if (translated == 0) { - return super.onKeyDown(keyCode, event); - } - - // Let this method take duplicate key down events - if (handleSpecialKeys(translated, true)) { - return true; - } - - // Eat repeat down events - if (event.getRepeatCount() > 0) { - return true; - } - - // Pass through keyboard input if we're not grabbing - if (!grabbedInput) { - return super.onKeyDown(keyCode, event); - } - - keybTranslator.sendKeyDown(translated, - getModifierState(event)); - } - - return true; - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - // Pass-through virtual navigation keys - if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) { - return super.onKeyUp(keyCode, event); - } - - // Try the controller handler first - boolean handled = controllerHandler.handleButtonUp(event); - if (!handled) { - // Try the keyboard handler - short translated = keybTranslator.translate(event.getKeyCode()); - if (translated == 0) { - return super.onKeyUp(keyCode, event); - } - - if (handleSpecialKeys(translated, false)) { - return true; - } - - // Pass through keyboard input if we're not grabbing - if (!grabbedInput) { - return super.onKeyUp(keyCode, event); - } - - keybTranslator.sendKeyUp(translated, - getModifierState(event)); - } - - return true; - } - - private TouchContext getTouchContext(int actionIndex) - { - if (actionIndex < touchContextMap.length) { - return touchContextMap[actionIndex]; - } - else { - return null; - } - } + displayedFailureDialog = true; + stopConnection(); + + int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency(); + int averageDecoderLat = decoderRenderer.getAverageDecoderLatency(); + String message = null; + if (averageEndToEndLat > 0) { + message = getResources().getString(R.string.conn_client_latency)+" "+averageEndToEndLat+" ms"; + if (averageDecoderLat > 0) { + message += " ("+getResources().getString(R.string.conn_client_latency_hw)+" "+averageDecoderLat+" ms)"; + } + } + else if (averageDecoderLat > 0) { + message = getResources().getString(R.string.conn_hardware_latency)+" "+averageDecoderLat+" ms"; + } + + if (message != null) { + Toast.makeText(this, message, Toast.LENGTH_LONG).show(); + } + + finish(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + wifiLock.release(); + } + + private final Runnable toggleGrab = new Runnable() { + @Override + public void run() { + + if (evdevWatcher != null) { + if (grabbedInput) { + evdevWatcher.ungrabAll(); + } + else { + evdevWatcher.regrabAll(); + } + } + + grabbedInput = !grabbedInput; + } + }; + + // Returns true if the key stroke was consumed + private boolean handleSpecialKeys(short translatedKey, boolean down) { + int modifierMask = 0; + + // Mask off the high byte + translatedKey &= 0xff; + + if (translatedKey == KeyboardTranslator.VK_CONTROL) { + modifierMask = KeyboardPacket.MODIFIER_CTRL; + } + else if (translatedKey == KeyboardTranslator.VK_SHIFT) { + modifierMask = KeyboardPacket.MODIFIER_SHIFT; + } + else if (translatedKey == KeyboardTranslator.VK_ALT) { + modifierMask = KeyboardPacket.MODIFIER_ALT; + } + + if (down) { + this.modifierFlags |= modifierMask; + } + else { + this.modifierFlags &= ~modifierMask; + } + + // Check if Ctrl+Shift+Z is pressed + if (translatedKey == KeyboardTranslator.VK_Z && + (modifierFlags & (KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_SHIFT)) == + (KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_SHIFT)) + { + if (down) { + // Now that we've pressed the magic combo + // we'll wait for one of the keys to come up + grabComboDown = true; + } + else { + // Toggle the grab if Z comes up + Handler h = getWindow().getDecorView().getHandler(); + if (h != null) { + h.postDelayed(toggleGrab, 250); + } + + grabComboDown = false; + } + + return true; + } + // Toggle the grab if control or shift comes up + else if (grabComboDown) { + Handler h = getWindow().getDecorView().getHandler(); + if (h != null) { + h.postDelayed(toggleGrab, 250); + } + + grabComboDown = false; + return true; + } + + // Not a special combo + return false; + } + + private static byte getModifierState(KeyEvent event) { + byte modifier = 0; + if (event.isShiftPressed()) { + modifier |= KeyboardPacket.MODIFIER_SHIFT; + } + if (event.isCtrlPressed()) { + modifier |= KeyboardPacket.MODIFIER_CTRL; + } + if (event.isAltPressed()) { + modifier |= KeyboardPacket.MODIFIER_ALT; + } + return modifier; + } + + private byte getModifierState() { + return (byte) modifierFlags; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + // Pass-through virtual navigation keys + if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) { + return super.onKeyDown(keyCode, event); + } + + // Try the controller handler first + boolean handled = controllerHandler.handleButtonDown(event); + if (!handled) { + // Try the keyboard handler + short translated = keybTranslator.translate(event.getKeyCode()); + if (translated == 0) { + return super.onKeyDown(keyCode, event); + } + + // Let this method take duplicate key down events + if (handleSpecialKeys(translated, true)) { + return true; + } + + // Eat repeat down events + if (event.getRepeatCount() > 0) { + return true; + } + + // Pass through keyboard input if we're not grabbing + if (!grabbedInput) { + return super.onKeyDown(keyCode, event); + } + + keybTranslator.sendKeyDown(translated, + getModifierState(event)); + } + + return true; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + // Pass-through virtual navigation keys + if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) { + return super.onKeyUp(keyCode, event); + } + + // Try the controller handler first + boolean handled = controllerHandler.handleButtonUp(event); + if (!handled) { + // Try the keyboard handler + short translated = keybTranslator.translate(event.getKeyCode()); + if (translated == 0) { + return super.onKeyUp(keyCode, event); + } + + if (handleSpecialKeys(translated, false)) { + return true; + } + + // Pass through keyboard input if we're not grabbing + if (!grabbedInput) { + return super.onKeyUp(keyCode, event); + } + + keybTranslator.sendKeyUp(translated, + getModifierState(event)); + } + + return true; + } + + private TouchContext getTouchContext(int actionIndex) + { + if (actionIndex < touchContextMap.length) { + return touchContextMap[actionIndex]; + } + else { + return null; + } + } @Override public void showKeyboard() { @@ -498,27 +498,27 @@ public class Game extends Activity implements SurfaceHolder.Callback, inputManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY); } - // Returns true if the event was consumed - private boolean handleMotionEvent(MotionEvent event) { - // Pass through keyboard input if we're not grabbing - if (!grabbedInput) { - return false; - } + // Returns true if the event was consumed + private boolean handleMotionEvent(MotionEvent event) { + // Pass through keyboard input if we're not grabbing + if (!grabbedInput) { + return false; + } - if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { - if (controllerHandler.handleMotionEvent(event)) { - return true; - } - } - else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) - { - // This case is for touch-based input devices - if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN || - event.getSource() == InputDevice.SOURCE_STYLUS) - { - int actionIndex = event.getActionIndex(); + if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { + if (controllerHandler.handleMotionEvent(event)) { + return true; + } + } + else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) + { + // This case is for touch-based input devices + if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN || + event.getSource() == InputDevice.SOURCE_STYLUS) + { + int actionIndex = event.getActionIndex(); - int eventX = (int)event.getX(actionIndex); + int eventX = (int)event.getX(actionIndex); int eventY = (int)event.getY(actionIndex); // Special handling for 3 finger gesture @@ -536,19 +536,19 @@ public class Game extends Activity implements SurfaceHolder.Callback, return true; } - TouchContext context = getTouchContext(actionIndex); - if (context == null) { - return false; - } + TouchContext context = getTouchContext(actionIndex); + if (context == null) { + return false; + } - switch (event.getActionMasked()) - { - case MotionEvent.ACTION_POINTER_DOWN: - case MotionEvent.ACTION_DOWN: - context.touchDownEvent(eventX, eventY); - break; - case MotionEvent.ACTION_POINTER_UP: - case MotionEvent.ACTION_UP: + switch (event.getActionMasked()) + { + case MotionEvent.ACTION_POINTER_DOWN: + case MotionEvent.ACTION_DOWN: + context.touchDownEvent(eventX, eventY); + break; + case MotionEvent.ACTION_POINTER_UP: + case MotionEvent.ACTION_UP: if (event.getPointerCount() == 1) { // All fingers up if (SystemClock.uptimeMillis() - threeFingerDownTime < THREE_FINGER_TAP_THRESHOLD) { @@ -557,15 +557,15 @@ public class Game extends Activity implements SurfaceHolder.Callback, return true; } } - context.touchUpEvent(eventX, eventY); - if (actionIndex == 0 && event.getPointerCount() > 1 && !context.isCancelled()) { - // The original secondary touch now becomes primary - context.touchDownEvent((int)event.getX(1), (int)event.getY(1)); - } - break; - case MotionEvent.ACTION_MOVE: - // ACTION_MOVE is special because it always has actionIndex == 0 - // We'll call the move handlers for all indexes manually + context.touchUpEvent(eventX, eventY); + if (actionIndex == 0 && event.getPointerCount() > 1 && !context.isCancelled()) { + // The original secondary touch now becomes primary + context.touchDownEvent((int)event.getX(1), (int)event.getY(1)); + } + break; + case MotionEvent.ACTION_MOVE: + // ACTION_MOVE is special because it always has actionIndex == 0 + // We'll call the move handlers for all indexes manually // First process the historical events for (int i = 0; i < event.getHistorySize(); i++) { @@ -588,48 +588,48 @@ public class Game extends Activity implements SurfaceHolder.Callback, (int)event.getY(aTouchContextMap.getActionIndex())); } } - break; - default: - return false; - } - } - // This case is for mice - else if (event.getSource() == InputDevice.SOURCE_MOUSE) - { - int changedButtons = event.getButtonState() ^ lastButtonState; - - if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) { - // Send the vertical scroll packet - byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL); - conn.sendMouseScroll(vScrollClicks); - } + break; + default: + return false; + } + } + // This case is for mice + else if (event.getSource() == InputDevice.SOURCE_MOUSE) + { + int changedButtons = event.getButtonState() ^ lastButtonState; - if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) { - if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) { - conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT); - } - else { - conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT); - } - } + if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) { + // Send the vertical scroll packet + byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL); + conn.sendMouseScroll(vScrollClicks); + } - if ((changedButtons & MotionEvent.BUTTON_SECONDARY) != 0) { - if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) { - conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT); - } - else { - conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT); - } - } + if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) { + if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) { + conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT); + } + else { + conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT); + } + } - if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) { - if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) { - conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_MIDDLE); - } - else { - conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_MIDDLE); - } - } + if ((changedButtons & MotionEvent.BUTTON_SECONDARY) != 0) { + if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) { + conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT); + } + else { + conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT); + } + } + + if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) { + if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) { + conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_MIDDLE); + } + else { + conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_MIDDLE); + } + } // First process the history for (int i = 0; i < event.getHistorySize(); i++) { @@ -640,256 +640,256 @@ public class Game extends Activity implements SurfaceHolder.Callback, updateMousePosition((int)event.getX(), (int)event.getY()); lastButtonState = event.getButtonState(); - } - else - { - // Unknown source - return false; - } + } + else + { + // Unknown source + return false; + } - // Handled a known source - return true; - } + // Handled a known source + return true; + } - // Unknown class - return false; - } - - @Override - public boolean onTouchEvent(MotionEvent event) { + // Unknown class + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { return handleMotionEvent(event) || super.onTouchEvent(event); } - @Override - public boolean onGenericMotionEvent(MotionEvent event) { + @Override + public boolean onGenericMotionEvent(MotionEvent event) { return handleMotionEvent(event) || super.onGenericMotionEvent(event); } - - private void updateMousePosition(int eventX, int eventY) { - // Send a mouse move if we already have a mouse location - // and the mouse coordinates change - if (lastMouseX != Integer.MIN_VALUE && - lastMouseY != Integer.MIN_VALUE && - !(lastMouseX == eventX && lastMouseY == eventY)) - { - int deltaX = eventX - lastMouseX; - int deltaY = eventY - lastMouseY; - - // Scale the deltas if the device resolution is different - // than the stream resolution - deltaX = (int)Math.round((double)deltaX * ((double)prefConfig.width / (double)screenSize.x)); - deltaY = (int)Math.round((double)deltaY * ((double)prefConfig.height / (double)screenSize.y)); - - conn.sendMouseMove((short)deltaX, (short)deltaY); - } - - // Update pointer location for delta calculation next time - lastMouseX = eventX; - lastMouseY = eventY; - } - @Override - public boolean onGenericMotion(View v, MotionEvent event) { - return handleMotionEvent(event); - } + private void updateMousePosition(int eventX, int eventY) { + // Send a mouse move if we already have a mouse location + // and the mouse coordinates change + if (lastMouseX != Integer.MIN_VALUE && + lastMouseY != Integer.MIN_VALUE && + !(lastMouseX == eventX && lastMouseY == eventY)) + { + int deltaX = eventX - lastMouseX; + int deltaY = eventY - lastMouseY; - @SuppressLint("ClickableViewAccessibility") - @Override - public boolean onTouch(View v, MotionEvent event) { - return handleMotionEvent(event); - } + // Scale the deltas if the device resolution is different + // than the stream resolution + deltaX = (int)Math.round((double)deltaX * ((double)prefConfig.width / (double)screenSize.x)); + deltaY = (int)Math.round((double)deltaY * ((double)prefConfig.height / (double)screenSize.y)); - @Override - public void stageStarting(Stage stage) { - if (spinner != null) { - spinner.setMessage(getResources().getString(R.string.conn_starting)+" "+stage.getName()); - } - } + conn.sendMouseMove((short)deltaX, (short)deltaY); + } - @Override - public void stageComplete(Stage stage) { - } - - private void stopConnection() { - if (connecting || connected) { - connecting = connected = false; - conn.stop(); - } - - // Close the Evdev watcher to allow use of captured input devices - if (evdevWatcher != null) { - evdevWatcher.shutdown(); - evdevWatcher = null; - } - } + // Update pointer location for delta calculation next time + lastMouseX = eventX; + lastMouseY = eventY; + } - @Override - public void stageFailed(Stage stage) { - if (spinner != null) { - spinner.dismiss(); - spinner = null; - } + @Override + public boolean onGenericMotion(View v, MotionEvent event) { + return handleMotionEvent(event); + } - if (!displayedFailureDialog) { - displayedFailureDialog = true; - stopConnection(); - Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), - getResources().getString(R.string.conn_error_msg)+" "+stage.getName(), true); - } - } + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouch(View v, MotionEvent event) { + return handleMotionEvent(event); + } - @Override - public void connectionTerminated(Exception e) { - if (!displayedFailureDialog) { - displayedFailureDialog = true; - e.printStackTrace(); - - stopConnection(); - Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title), - getResources().getString(R.string.conn_terminated_msg), true); - } - } + @Override + public void stageStarting(Stage stage) { + if (spinner != null) { + spinner.setMessage(getResources().getString(R.string.conn_starting)+" "+stage.getName()); + } + } - @Override - public void connectionStarted() { - if (spinner != null) { - spinner.dismiss(); - spinner = null; - } - - connecting = false; - connected = true; - - hideSystemUi(1000); - } + @Override + public void stageComplete(Stage stage) { + } - @Override - public void displayMessage(final String message) { - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(Game.this, message, Toast.LENGTH_LONG).show(); - } - }); - } + private void stopConnection() { + if (connecting || connected) { + connecting = connected = false; + conn.stop(); + } - @Override - public void displayTransientMessage(final String message) { - if (!prefConfig.disableWarnings) { - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(Game.this, message, Toast.LENGTH_LONG).show(); - } - }); - } - } + // Close the Evdev watcher to allow use of captured input devices + if (evdevWatcher != null) { + evdevWatcher.shutdown(); + evdevWatcher = null; + } + } - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - } + @Override + public void stageFailed(Stage stage) { + if (spinner != null) { + spinner.dismiss(); + spinner = null; + } - @Override - public void surfaceCreated(SurfaceHolder holder) { - if (!connected && !connecting) { - connecting = true; - - // Resize the surface to match the aspect ratio of the video - // This must be done after the surface is created. - if (!prefConfig.stretchVideo && decoderRenderer.isHardwareAccelerated()) { - resizeSurfaceWithAspectRatio((SurfaceView) findViewById(R.id.surfaceView), + if (!displayedFailureDialog) { + displayedFailureDialog = true; + stopConnection(); + Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), + getResources().getString(R.string.conn_error_msg)+" "+stage.getName(), true); + } + } + + @Override + public void connectionTerminated(Exception e) { + if (!displayedFailureDialog) { + displayedFailureDialog = true; + e.printStackTrace(); + + stopConnection(); + Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title), + getResources().getString(R.string.conn_terminated_msg), true); + } + } + + @Override + public void connectionStarted() { + if (spinner != null) { + spinner.dismiss(); + spinner = null; + } + + connecting = false; + connected = true; + + hideSystemUi(1000); + } + + @Override + public void displayMessage(final String message) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(Game.this, message, Toast.LENGTH_LONG).show(); + } + }); + } + + @Override + public void displayTransientMessage(final String message) { + if (!prefConfig.disableWarnings) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(Game.this, message, Toast.LENGTH_LONG).show(); + } + }); + } + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + if (!connected && !connecting) { + connecting = true; + + // Resize the surface to match the aspect ratio of the video + // This must be done after the surface is created. + if (!prefConfig.stretchVideo && decoderRenderer.isHardwareAccelerated()) { + resizeSurfaceWithAspectRatio((SurfaceView) findViewById(R.id.surfaceView), prefConfig.width, prefConfig.height); - } - - conn.start(PlatformBinding.getDeviceName(), holder, drFlags, - PlatformBinding.getAudioRenderer(), decoderRenderer); - } - } + } - @Override - public void surfaceDestroyed(SurfaceHolder holder) { - if (connected) { - stopConnection(); - } - } + conn.start(PlatformBinding.getDeviceName(), holder, drFlags, + PlatformBinding.getAudioRenderer(), decoderRenderer); + } + } - @Override - public void mouseMove(int deltaX, int deltaY) { - conn.sendMouseMove((short) deltaX, (short) deltaY); - } + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + if (connected) { + stopConnection(); + } + } - @Override - public void mouseButtonEvent(int buttonId, boolean down) { - byte buttonIndex; - - switch (buttonId) - { - case EvdevListener.BUTTON_LEFT: - buttonIndex = MouseButtonPacket.BUTTON_LEFT; - break; - case EvdevListener.BUTTON_MIDDLE: - buttonIndex = MouseButtonPacket.BUTTON_MIDDLE; - break; - case EvdevListener.BUTTON_RIGHT: - buttonIndex = MouseButtonPacket.BUTTON_RIGHT; - break; - default: - LimeLog.warning("Unhandled button: "+buttonId); - return; - } - - if (down) { - conn.sendMouseButtonDown(buttonIndex); - } - else { - conn.sendMouseButtonUp(buttonIndex); - } - } + @Override + public void mouseMove(int deltaX, int deltaY) { + conn.sendMouseMove((short) deltaX, (short) deltaY); + } - @Override - public void mouseScroll(byte amount) { - conn.sendMouseScroll(amount); - } + @Override + public void mouseButtonEvent(int buttonId, boolean down) { + byte buttonIndex; - @Override - public void keyboardEvent(boolean buttonDown, short keyCode) { - short keyMap = keybTranslator.translate(keyCode); - if (keyMap != 0) { - if (handleSpecialKeys(keyMap, buttonDown)) { - return; - } - - if (buttonDown) { - keybTranslator.sendKeyDown(keyMap, getModifierState()); - } - else { - keybTranslator.sendKeyUp(keyMap, getModifierState()); - } - } - } + switch (buttonId) + { + case EvdevListener.BUTTON_LEFT: + buttonIndex = MouseButtonPacket.BUTTON_LEFT; + break; + case EvdevListener.BUTTON_MIDDLE: + buttonIndex = MouseButtonPacket.BUTTON_MIDDLE; + break; + case EvdevListener.BUTTON_RIGHT: + buttonIndex = MouseButtonPacket.BUTTON_RIGHT; + break; + default: + LimeLog.warning("Unhandled button: "+buttonId); + return; + } - @Override - public void onSystemUiVisibilityChange(int visibility) { - // Don't do anything if we're not connected - if (!connected) { - return; - } - - // This flag is set for all devices - if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { - hideSystemUi(2000); - } - // This flag is only set on 4.4+ - else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT && - (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) { - hideSystemUi(2000); - } - // This flag is only set before 4.4+ - else if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.KITKAT && - (visibility & View.SYSTEM_UI_FLAG_LOW_PROFILE) == 0) { - hideSystemUi(2000); - } - } + if (down) { + conn.sendMouseButtonDown(buttonIndex); + } + else { + conn.sendMouseButtonUp(buttonIndex); + } + } + + @Override + public void mouseScroll(byte amount) { + conn.sendMouseScroll(amount); + } + + @Override + public void keyboardEvent(boolean buttonDown, short keyCode) { + short keyMap = keybTranslator.translate(keyCode); + if (keyMap != 0) { + if (handleSpecialKeys(keyMap, buttonDown)) { + return; + } + + if (buttonDown) { + keybTranslator.sendKeyDown(keyMap, getModifierState()); + } + else { + keybTranslator.sendKeyUp(keyMap, getModifierState()); + } + } + } + + @Override + public void onSystemUiVisibilityChange(int visibility) { + // Don't do anything if we're not connected + if (!connected) { + return; + } + + // This flag is set for all devices + if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { + hideSystemUi(2000); + } + // This flag is only set on 4.4+ + else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT && + (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) { + hideSystemUi(2000); + } + // This flag is only set before 4.4+ + else if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.KITKAT && + (visibility & View.SYSTEM_UI_FLAG_LOW_PROFILE) == 0) { + hideSystemUi(2000); + } + } } diff --git a/app/src/main/java/com/limelight/PcView.java b/app/src/main/java/com/limelight/PcView.java index 74c74474..c6a44a9e 100644 --- a/app/src/main/java/com/limelight/PcView.java +++ b/app/src/main/java/com/limelight/PcView.java @@ -50,69 +50,69 @@ import android.widget.AdapterView.AdapterContextMenuInfo; public class PcView extends Activity implements AdapterFragmentCallbacks { private RelativeLayout noPcFoundLayout; private PcGridAdapter pcGridAdapter; - private ComputerManagerService.ComputerManagerBinder managerBinder; - private boolean freezeUpdates, runningPolling; - private final ServiceConnection serviceConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName className, IBinder binder) { - final ComputerManagerService.ComputerManagerBinder localBinder = - ((ComputerManagerService.ComputerManagerBinder)binder); - - // Wait in a separate thread to avoid stalling the UI - new Thread() { - @Override - public void run() { - // Wait for the binder to be ready - localBinder.waitForReady(); - - // Now make the binder visible - managerBinder = localBinder; - - // Start updates - startComputerUpdates(); - - // Force a keypair to be generated early to avoid discovery delays - new AndroidCryptoProvider(PcView.this).getClientCertificate(); - } - }.start(); - } + private ComputerManagerService.ComputerManagerBinder managerBinder; + private boolean freezeUpdates, runningPolling; + private final ServiceConnection serviceConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder binder) { + final ComputerManagerService.ComputerManagerBinder localBinder = + ((ComputerManagerService.ComputerManagerBinder)binder); - public void onServiceDisconnected(ComponentName className) { - managerBinder = null; - } - }; - - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - - // Reinitialize views just in case orientation changed - initializeViews(); - } - - private final static int APP_LIST_ID = 1; - private final static int PAIR_ID = 2; - private final static int UNPAIR_ID = 3; - private final static int WOL_ID = 4; - private final static int DELETE_ID = 5; - - private void initializeViews() { - setContentView(R.layout.activity_pc_view); + // Wait in a separate thread to avoid stalling the UI + new Thread() { + @Override + public void run() { + // Wait for the binder to be ready + localBinder.waitForReady(); + + // Now make the binder visible + managerBinder = localBinder; + + // Start updates + startComputerUpdates(); + + // Force a keypair to be generated early to avoid discovery delays + new AndroidCryptoProvider(PcView.this).getClientCertificate(); + } + }.start(); + } + + public void onServiceDisconnected(ComponentName className) { + managerBinder = null; + } + }; + + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + // Reinitialize views just in case orientation changed + initializeViews(); + } + + private final static int APP_LIST_ID = 1; + private final static int PAIR_ID = 2; + private final static int UNPAIR_ID = 3; + private final static int WOL_ID = 4; + private final static int DELETE_ID = 5; + + private void initializeViews() { + setContentView(R.layout.activity_pc_view); UiHelper.notifyNewRootView(this); // Set default preferences if we've never been run PreferenceManager.setDefaultValues(this, R.xml.preferences, false); - // Setup the list view + // Setup the list view ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton); ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc); - settingsButton.setOnClickListener(new OnClickListener() { + settingsButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { startActivity(new Intent(PcView.this, StreamSettings.class)); } }); - addComputerButton.setOnClickListener(new OnClickListener() { + addComputerButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Intent i = new Intent(PcView.this, AddComputerManually.class); @@ -133,114 +133,114 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { } pcGridAdapter.notifyDataSetChanged(); } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); String locale = PreferenceConfiguration.readPreferences(this).language; - if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) { - Configuration config = new Configuration(getResources().getConfiguration()); - config.locale = new Locale(locale); - getResources().updateConfiguration(config, getResources().getDisplayMetrics()); - } - - // Bind to the computer manager service - bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection, - Service.BIND_AUTO_CREATE); + if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) { + Configuration config = new Configuration(getResources().getConfiguration()); + config.locale = new Locale(locale); + getResources().updateConfiguration(config, getResources().getDisplayMetrics()); + } + + // Bind to the computer manager service + bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection, + Service.BIND_AUTO_CREATE); pcGridAdapter = new PcGridAdapter(this, PreferenceConfiguration.readPreferences(this).listMode, PreferenceConfiguration.readPreferences(this).smallIconMode); - - initializeViews(); - } - - private void startComputerUpdates() { - if (managerBinder != null) { - if (runningPolling) { - return; - } - - freezeUpdates = false; - managerBinder.startPolling(new ComputerManagerListener() { - @Override - public void notifyComputerUpdated(final ComputerDetails details) { - if (!freezeUpdates) { - PcView.this.runOnUiThread(new Runnable() { - @Override - public void run() { - updateComputer(details); - } - }); - } - } - }); - runningPolling = true; - } - } - - private void stopComputerUpdates(boolean wait) { - if (managerBinder != null) { - if (!runningPolling) { - return; - } - - freezeUpdates = true; - - managerBinder.stopPolling(); - - if (wait) { - managerBinder.waitForPollingStopped(); - } - - runningPolling = false; - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - - if (managerBinder != null) { - unbindService(serviceConnection); - } - } - - @Override - protected void onResume() { - super.onResume(); - - startComputerUpdates(); - } - - @Override - protected void onPause() { - super.onPause(); - - stopComputerUpdates(false); - } - - @Override - protected void onStop() { - super.onStop(); - - Dialog.closeDialogs(); - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { - stopComputerUpdates(false); - - // Call superclass - super.onCreateContextMenu(menu, v, menuInfo); + + initializeViews(); + } + + private void startComputerUpdates() { + if (managerBinder != null) { + if (runningPolling) { + return; + } + + freezeUpdates = false; + managerBinder.startPolling(new ComputerManagerListener() { + @Override + public void notifyComputerUpdated(final ComputerDetails details) { + if (!freezeUpdates) { + PcView.this.runOnUiThread(new Runnable() { + @Override + public void run() { + updateComputer(details); + } + }); + } + } + }); + runningPolling = true; + } + } + + private void stopComputerUpdates(boolean wait) { + if (managerBinder != null) { + if (!runningPolling) { + return; + } + + freezeUpdates = true; + + managerBinder.stopPolling(); + + if (wait) { + managerBinder.waitForPollingStopped(); + } + + runningPolling = false; + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + + if (managerBinder != null) { + unbindService(serviceConnection); + } + } + + @Override + protected void onResume() { + super.onResume(); + + startComputerUpdates(); + } + + @Override + protected void onPause() { + super.onPause(); + + stopComputerUpdates(false); + } + + @Override + protected void onStop() { + super.onStop(); + + Dialog.closeDialogs(); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + stopComputerUpdates(false); + + // Call superclass + super.onCreateContextMenu(menu, v, menuInfo); AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position); if (computer == null || computer.details == null || computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) { - startComputerUpdates(); - return; + startComputerUpdates(); + return; } // Inflate the context menu @@ -260,94 +260,94 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc)); } } - - @Override - public void onContextMenuClosed(Menu menu) { - startComputerUpdates(); - } - - private void doPair(final ComputerDetails computer) { - if (computer.reachability == ComputerDetails.Reachability.OFFLINE) { - Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show(); - return; - } - if (computer.runningGameId != 0) { - Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_ingame), Toast.LENGTH_LONG).show(); - return; - } - if (managerBinder == null) { - Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); - return; - } - - Toast.makeText(PcView.this, getResources().getString(R.string.pairing), Toast.LENGTH_SHORT).show(); - new Thread(new Runnable() { - @Override - public void run() { - NvHTTP httpConn; - String message; + + @Override + public void onContextMenuClosed(Menu menu) { + startComputerUpdates(); + } + + private void doPair(final ComputerDetails computer) { + if (computer.reachability == ComputerDetails.Reachability.OFFLINE) { + Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show(); + return; + } + if (computer.runningGameId != 0) { + Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_ingame), Toast.LENGTH_LONG).show(); + return; + } + if (managerBinder == null) { + Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); + return; + } + + Toast.makeText(PcView.this, getResources().getString(R.string.pairing), Toast.LENGTH_SHORT).show(); + new Thread(new Runnable() { + @Override + public void run() { + NvHTTP httpConn; + String message; boolean success = false; - try { - // Stop updates and wait while pairing - stopComputerUpdates(true); - - InetAddress addr = null; - if (computer.reachability == ComputerDetails.Reachability.LOCAL) { - addr = computer.localIp; - } - else if (computer.reachability == ComputerDetails.Reachability.REMOTE) { - addr = computer.remoteIp; - } - - httpConn = new NvHTTP(addr, - managerBinder.getUniqueId(), - PlatformBinding.getDeviceName(), - PlatformBinding.getCryptoProvider(PcView.this)); - if (httpConn.getPairState() == PairingManager.PairState.PAIRED) { + try { + // Stop updates and wait while pairing + stopComputerUpdates(true); + + InetAddress addr = null; + if (computer.reachability == ComputerDetails.Reachability.LOCAL) { + addr = computer.localIp; + } + else if (computer.reachability == ComputerDetails.Reachability.REMOTE) { + addr = computer.remoteIp; + } + + httpConn = new NvHTTP(addr, + managerBinder.getUniqueId(), + PlatformBinding.getDeviceName(), + PlatformBinding.getCryptoProvider(PcView.this)); + if (httpConn.getPairState() == PairingManager.PairState.PAIRED) { // Don't display any toast, but open the app list - message = null; + message = null; success = true; - } - else { - final String pinStr = PairingManager.generatePinString(); - - // Spin the dialog off in a thread because it blocks - Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title), - getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false); - - PairingManager.PairState pairState = httpConn.pair(pinStr); - if (pairState == PairingManager.PairState.PIN_WRONG) { - message = getResources().getString(R.string.pair_incorrect_pin); - } - else if (pairState == PairingManager.PairState.FAILED) { - message = getResources().getString(R.string.pair_fail); - } - else if (pairState == PairingManager.PairState.PAIRED) { + } + else { + final String pinStr = PairingManager.generatePinString(); + + // Spin the dialog off in a thread because it blocks + Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title), + getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false); + + PairingManager.PairState pairState = httpConn.pair(pinStr); + if (pairState == PairingManager.PairState.PIN_WRONG) { + message = getResources().getString(R.string.pair_incorrect_pin); + } + else if (pairState == PairingManager.PairState.FAILED) { + message = getResources().getString(R.string.pair_fail); + } + else if (pairState == PairingManager.PairState.PAIRED) { // Just navigate to the app view without displaying a toast message = null; success = true; - } - else { - // Should be no other values - message = null; - } - } - } catch (UnknownHostException e) { - message = getResources().getString(R.string.error_unknown_host); - } catch (FileNotFoundException e) { - message = getResources().getString(R.string.error_404); - } catch (Exception e) { + } + else { + // Should be no other values + message = null; + } + } + } catch (UnknownHostException e) { + message = getResources().getString(R.string.error_unknown_host); + } catch (FileNotFoundException e) { + message = getResources().getString(R.string.error_404); + } catch (Exception e) { e.printStackTrace(); - message = e.getMessage(); - } - - Dialog.closeDialogs(); - - final String toastMessage = message; + message = e.getMessage(); + } + + Dialog.closeDialogs(); + + final String toastMessage = message; final boolean toastSuccess = success; - runOnUiThread(new Runnable() { - @Override - public void run() { + runOnUiThread(new Runnable() { + @Override + public void run() { if (toastMessage != null) { Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show(); } @@ -356,124 +356,124 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { // Open the app list after a successful pairing attemp doAppList(computer); } - } - }); - - // Start polling again - startComputerUpdates(); - } - }).start(); - } - - private void doWakeOnLan(final ComputerDetails computer) { - if (computer.reachability != ComputerDetails.Reachability.OFFLINE) { - Toast.makeText(PcView.this, getResources().getString(R.string.wol_pc_online), Toast.LENGTH_SHORT).show(); - return; - } - - if (computer.macAddress == null) { - Toast.makeText(PcView.this, getResources().getString(R.string.wol_no_mac), Toast.LENGTH_SHORT).show(); - return; - } - - Toast.makeText(PcView.this, getResources().getString(R.string.wol_waking_pc), Toast.LENGTH_SHORT).show(); - new Thread(new Runnable() { - @Override - public void run() { - String message; - try { - WakeOnLanSender.sendWolPacket(computer); - message = getResources().getString(R.string.wol_waking_msg); - } catch (IOException e) { - message = getResources().getString(R.string.wol_fail); - } - - final String toastMessage = message; - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show(); - } - }); - } - }).start(); - } - - private void doUnpair(final ComputerDetails computer) { - if (computer.reachability == ComputerDetails.Reachability.OFFLINE) { - Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show(); - return; - } - if (managerBinder == null) { - Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); - return; - } - - Toast.makeText(PcView.this, getResources().getString(R.string.unpairing), Toast.LENGTH_SHORT).show(); - new Thread(new Runnable() { - @Override - public void run() { - NvHTTP httpConn; - String message; - try { - InetAddress addr = null; - if (computer.reachability == ComputerDetails.Reachability.LOCAL) { - addr = computer.localIp; - } - else if (computer.reachability == ComputerDetails.Reachability.REMOTE) { - addr = computer.remoteIp; - } - - httpConn = new NvHTTP(addr, - managerBinder.getUniqueId(), - PlatformBinding.getDeviceName(), - PlatformBinding.getCryptoProvider(PcView.this)); - if (httpConn.getPairState() == PairingManager.PairState.PAIRED) { - httpConn.unpair(); - if (httpConn.getPairState() == PairingManager.PairState.NOT_PAIRED) { - message = getResources().getString(R.string.unpair_success); - } - else { - message = getResources().getString(R.string.unpair_fail); - } - } - else { - message = getResources().getString(R.string.unpair_error); - } - } catch (UnknownHostException e) { - message = getResources().getString(R.string.error_unknown_host); - } catch (FileNotFoundException e) { - message = getResources().getString(R.string.error_404); - } catch (Exception e) { - message = e.getMessage(); - } - - final String toastMessage = message; - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show(); - } - }); - } - }).start(); - } - - private void doAppList(ComputerDetails computer) { - if (computer.reachability == ComputerDetails.Reachability.OFFLINE) { - Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show(); - return; - } - if (managerBinder == null) { - Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); - return; - } - - Intent i = new Intent(this, AppView.class); - i.putExtra(AppView.NAME_EXTRA, computer.name); - i.putExtra(AppView.UUID_EXTRA, computer.uuid.toString()); - startActivity(i); - } + } + }); + + // Start polling again + startComputerUpdates(); + } + }).start(); + } + + private void doWakeOnLan(final ComputerDetails computer) { + if (computer.reachability != ComputerDetails.Reachability.OFFLINE) { + Toast.makeText(PcView.this, getResources().getString(R.string.wol_pc_online), Toast.LENGTH_SHORT).show(); + return; + } + + if (computer.macAddress == null) { + Toast.makeText(PcView.this, getResources().getString(R.string.wol_no_mac), Toast.LENGTH_SHORT).show(); + return; + } + + Toast.makeText(PcView.this, getResources().getString(R.string.wol_waking_pc), Toast.LENGTH_SHORT).show(); + new Thread(new Runnable() { + @Override + public void run() { + String message; + try { + WakeOnLanSender.sendWolPacket(computer); + message = getResources().getString(R.string.wol_waking_msg); + } catch (IOException e) { + message = getResources().getString(R.string.wol_fail); + } + + final String toastMessage = message; + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show(); + } + }); + } + }).start(); + } + + private void doUnpair(final ComputerDetails computer) { + if (computer.reachability == ComputerDetails.Reachability.OFFLINE) { + Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show(); + return; + } + if (managerBinder == null) { + Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); + return; + } + + Toast.makeText(PcView.this, getResources().getString(R.string.unpairing), Toast.LENGTH_SHORT).show(); + new Thread(new Runnable() { + @Override + public void run() { + NvHTTP httpConn; + String message; + try { + InetAddress addr = null; + if (computer.reachability == ComputerDetails.Reachability.LOCAL) { + addr = computer.localIp; + } + else if (computer.reachability == ComputerDetails.Reachability.REMOTE) { + addr = computer.remoteIp; + } + + httpConn = new NvHTTP(addr, + managerBinder.getUniqueId(), + PlatformBinding.getDeviceName(), + PlatformBinding.getCryptoProvider(PcView.this)); + if (httpConn.getPairState() == PairingManager.PairState.PAIRED) { + httpConn.unpair(); + if (httpConn.getPairState() == PairingManager.PairState.NOT_PAIRED) { + message = getResources().getString(R.string.unpair_success); + } + else { + message = getResources().getString(R.string.unpair_fail); + } + } + else { + message = getResources().getString(R.string.unpair_error); + } + } catch (UnknownHostException e) { + message = getResources().getString(R.string.error_unknown_host); + } catch (FileNotFoundException e) { + message = getResources().getString(R.string.error_404); + } catch (Exception e) { + message = e.getMessage(); + } + + final String toastMessage = message; + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show(); + } + }); + } + }).start(); + } + + private void doAppList(ComputerDetails computer) { + if (computer.reachability == ComputerDetails.Reachability.OFFLINE) { + Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show(); + return; + } + if (managerBinder == null) { + Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); + return; + } + + Intent i = new Intent(this, AppView.class); + i.putExtra(AppView.NAME_EXTRA, computer.name); + i.putExtra(AppView.UUID_EXTRA, computer.uuid.toString()); + startActivity(i); + } @Override public boolean onContextItemSelected(MenuItem item) { @@ -482,75 +482,75 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { switch (item.getItemId()) { case PAIR_ID: - doPair(computer.details); - return true; - + doPair(computer.details); + return true; + case UNPAIR_ID: - doUnpair(computer.details); - return true; - + doUnpair(computer.details); + return true; + case WOL_ID: - doWakeOnLan(computer.details); - return true; - + doWakeOnLan(computer.details); + return true; + case DELETE_ID: - if (managerBinder == null) { - Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); - return true; - } - managerBinder.removeComputer(computer.details.name); - removeComputer(computer.details); - return true; - + if (managerBinder == null) { + Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); + return true; + } + managerBinder.removeComputer(computer.details.name); + removeComputer(computer.details); + return true; + case APP_LIST_ID: - doAppList(computer.details); - return true; - + doAppList(computer.details); + return true; + default: return super.onContextItemSelected(item); } } private void removeComputer(ComputerDetails details) { - for (int i = 0; i < pcGridAdapter.getCount(); i++) { - ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i); - - if (details.equals(computer.details)) { + for (int i = 0; i < pcGridAdapter.getCount(); i++) { + ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i); + + if (details.equals(computer.details)) { pcGridAdapter.removeComputer(computer); pcGridAdapter.notifyDataSetChanged(); - break; - } - } + break; + } + } } - private void updateComputer(ComputerDetails details) { - ComputerObject existingEntry = null; - - for (int i = 0; i < pcGridAdapter.getCount(); i++) { - ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i); - - // Check if this is the same computer - if (details.uuid.equals(computer.details.uuid)) { - existingEntry = computer; - break; - } - } - - if (existingEntry != null) { - // Replace the information in the existing entry - existingEntry.details = details; - } - else { - // Add a new entry + private void updateComputer(ComputerDetails details) { + ComputerObject existingEntry = null; + + for (int i = 0; i < pcGridAdapter.getCount(); i++) { + ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i); + + // Check if this is the same computer + if (details.uuid.equals(computer.details.uuid)) { + existingEntry = computer; + break; + } + } + + if (existingEntry != null) { + // Replace the information in the existing entry + existingEntry.details = details; + } + else { + // Add a new entry pcGridAdapter.addComputer(new ComputerObject(details)); // Remove the "Discovery in progress" view noPcFoundLayout.setVisibility(View.INVISIBLE); - } + } // Notify the view that the data has changed pcGridAdapter.notifyDataSetChanged(); - } + } @Override public int getAdapterFragmentLayoutId() { @@ -584,15 +584,15 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { } public class ComputerObject { - public ComputerDetails details; - - public ComputerObject(ComputerDetails details) { - this.details = details; - } - - @Override - public String toString() { - return details.name; - } - } + public ComputerDetails details; + + public ComputerObject(ComputerDetails details) { + this.details = details; + } + + @Override + public String toString() { + return details.name; + } + } } diff --git a/app/src/main/java/com/limelight/binding/PlatformBinding.java b/app/src/main/java/com/limelight/binding/PlatformBinding.java index e159dd4e..b37c7645 100644 --- a/app/src/main/java/com/limelight/binding/PlatformBinding.java +++ b/app/src/main/java/com/limelight/binding/PlatformBinding.java @@ -8,17 +8,17 @@ import com.limelight.nvstream.av.audio.AudioRenderer; import com.limelight.nvstream.http.LimelightCryptoProvider; public class PlatformBinding { - public static String getDeviceName() { - String deviceName = android.os.Build.MODEL; + public static String getDeviceName() { + String deviceName = android.os.Build.MODEL; deviceName = deviceName.replace(" ", ""); return deviceName; - } - - public static AudioRenderer getAudioRenderer() { - return new AndroidAudioRenderer(); - } - - public static LimelightCryptoProvider getCryptoProvider(Context c) { - return new AndroidCryptoProvider(c); - } + } + + public static AudioRenderer getAudioRenderer() { + return new AndroidAudioRenderer(); + } + + public static LimelightCryptoProvider getCryptoProvider(Context c) { + return new AndroidCryptoProvider(c); + } } diff --git a/app/src/main/java/com/limelight/binding/audio/AndroidAudioRenderer.java b/app/src/main/java/com/limelight/binding/audio/AndroidAudioRenderer.java index 83c5fbb7..9f849a9e 100644 --- a/app/src/main/java/com/limelight/binding/audio/AndroidAudioRenderer.java +++ b/app/src/main/java/com/limelight/binding/audio/AndroidAudioRenderer.java @@ -9,27 +9,27 @@ import com.limelight.nvstream.av.audio.AudioRenderer; public class AndroidAudioRenderer implements AudioRenderer { - private static final int FRAME_SIZE = 960; - - private AudioTrack track; + private static final int FRAME_SIZE = 960; - @Override - public boolean streamInitialized(int channelCount, int sampleRate) { - int channelConfig; - int bufferSize; + private AudioTrack track; - switch (channelCount) - { - case 1: - channelConfig = AudioFormat.CHANNEL_OUT_MONO; - break; - case 2: - channelConfig = AudioFormat.CHANNEL_OUT_STEREO; - break; - default: - LimeLog.severe("Decoder returned unhandled channel count"); - return false; - } + @Override + public boolean streamInitialized(int channelCount, int sampleRate) { + int channelConfig; + int bufferSize; + + switch (channelCount) + { + case 1: + channelConfig = AudioFormat.CHANNEL_OUT_MONO; + break; + case 2: + channelConfig = AudioFormat.CHANNEL_OUT_STEREO; + break; + default: + LimeLog.severe("Decoder returned unhandled channel count"); + return false; + } // We're not supposed to request less than the minimum // buffer size for our buffer, but it appears that we can @@ -72,26 +72,26 @@ public class AndroidAudioRenderer implements AudioRenderer { AudioTrack.MODE_STREAM); track.play(); } - - LimeLog.info("Audio track buffer size: "+bufferSize); - return true; - } + LimeLog.info("Audio track buffer size: "+bufferSize); - @Override - public void playDecodedAudio(byte[] audioData, int offset, int length) { - track.write(audioData, offset, length); - } + return true; + } - @Override - public void streamClosing() { - if (track != null) { - track.release(); - } - } + @Override + public void playDecodedAudio(byte[] audioData, int offset, int length) { + track.write(audioData, offset, length); + } - @Override - public int getCapabilities() { - return 0; - } + @Override + public void streamClosing() { + if (track != null) { + track.release(); + } + } + + @Override + public int getCapabilities() { + return 0; + } } diff --git a/app/src/main/java/com/limelight/binding/crypto/AndroidCryptoProvider.java b/app/src/main/java/com/limelight/binding/crypto/AndroidCryptoProvider.java index 879482fa..a210ba61 100644 --- a/app/src/main/java/com/limelight/binding/crypto/AndroidCryptoProvider.java +++ b/app/src/main/java/com/limelight/binding/crypto/AndroidCryptoProvider.java @@ -45,239 +45,239 @@ import com.limelight.nvstream.http.LimelightCryptoProvider; public class AndroidCryptoProvider implements LimelightCryptoProvider { - private final File certFile; - private final File keyFile; - - private X509Certificate cert; - private RSAPrivateKey key; - private byte[] pemCertBytes; - - private static final Object globalCryptoLock = new Object(); - - 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()]; - if (fin.read(fileData) != f.length()) { + private final File certFile; + private final File keyFile; + + private X509Certificate cert; + private RSAPrivateKey key; + private byte[] pemCertBytes; + + private static final Object globalCryptoLock = new Object(); + + 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()]; + if (fin.read(fileData) != f.length()) { // Failed to read fileData = null; } - 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; - } - - return true; - } - - @SuppressLint("TrulyRandom") - private boolean generateCertKeyPair() { - 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(); - - // Expires in 20 years - Calendar calendar = Calendar.getInstance(); - calendar.setTime(now); - calendar.add(Calendar.YEAR, 20); - Date expirationDate = calendar.getTime(); - - BigInteger serial = new BigInteger(snBytes).abs(); - - X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE); - nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client"); - X500Name name = nameBuilder.build(); - - X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name, - SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded())); - - try { - ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate()); - cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen)); - 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(); - JcaPEMWriter pemWriter = new JcaPEMWriter(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 (globalCryptoLock) { - // 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; - } - } + fin.close(); + return fileData; + } catch (IOException e) { + return null; + } + } - 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 (globalCryptoLock) { - // 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 (globalCryptoLock) { - // Call our helper function to do the cert loading/generation for us - getClientCertificate(); - - // Return a cached value if we have it - return pemCertBytes; - } - } + private boolean loadCertKeyPair() { + byte[] certBytes = loadFileToBytes(certFile); + byte[] keyBytes = loadFileToBytes(keyFile); - @Override - public String encodeBase64String(byte[] data) { - return Base64.encodeToString(data, Base64.NO_WRAP); - } + // 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; + } + + return true; + } + + @SuppressLint("TrulyRandom") + private boolean generateCertKeyPair() { + 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(); + + // Expires in 20 years + Calendar calendar = Calendar.getInstance(); + calendar.setTime(now); + calendar.add(Calendar.YEAR, 20); + Date expirationDate = calendar.getTime(); + + BigInteger serial = new BigInteger(snBytes).abs(); + + X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE); + nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client"); + X500Name name = nameBuilder.build(); + + X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name, + SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded())); + + try { + ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate()); + cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen)); + 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(); + JcaPEMWriter pemWriter = new JcaPEMWriter(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 (globalCryptoLock) { + // 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 (globalCryptoLock) { + // 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 (globalCryptoLock) { + // 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); + } } diff --git a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java index aa84d530..24551bad 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -17,23 +17,23 @@ import com.limelight.utils.Vector2d; public class ControllerHandler implements InputManager.InputDeviceListener { - private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100; + private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100; private static final int START_DOWN_TIME_KEYB_MS = 750; - - private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25; - - private static final int 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 final Vector2d inputVector = new Vector2d(); - - private final HashMap contexts = new HashMap(); - - private final NvConnection conn; + + private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25; + + 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 final Vector2d inputVector = new Vector2d(); + + private final HashMap contexts = new HashMap(); + + private final NvConnection conn; private final double stickDeadzone; private final ControllerContext defaultContext = new ControllerContext(); private final GameGestures gestures; @@ -41,9 +41,9 @@ public class ControllerHandler implements InputManager.InputDeviceListener { private final boolean multiControllerEnabled; private short currentControllers; - - public ControllerHandler(NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) { - this.conn = conn; + + public ControllerHandler(NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) { + this.conn = conn; this.gestures = gestures; this.multiControllerEnabled = multiControllerEnabled; @@ -82,20 +82,20 @@ public class ControllerHandler implements InputManager.InputDeviceListener { defaultContext.leftTriggerAxis = MotionEvent.AXIS_BRAKE; defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS; defaultContext.controllerNumber = (short) 0; - } - - private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) { - InputDevice.MotionRange range; - - // First get the axis for SOURCE_JOYSTICK - range = dev.getMotionRange(axis, InputDevice.SOURCE_JOYSTICK); - if (range == null) { - // Now try the axis for SOURCE_GAMEPAD - range = dev.getMotionRange(axis, InputDevice.SOURCE_GAMEPAD); - } - - return range; - } + } + + private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) { + InputDevice.MotionRange range; + + // First get the axis for SOURCE_JOYSTICK + range = dev.getMotionRange(axis, InputDevice.SOURCE_JOYSTICK); + if (range == null) { + // Now try the axis for SOURCE_GAMEPAD + range = dev.getMotionRange(axis, InputDevice.SOURCE_GAMEPAD); + } + + return range; + } private short assignNewControllerNumber() { for (short i = 0; i < 4; i++) { @@ -137,9 +137,9 @@ public class ControllerHandler implements InputManager.InputDeviceListener { LimeLog.info("Controller number "+controllerNumber+" is now available"); currentControllers &= ~(1 << controllerNumber); } - - private ControllerContext createContextForDevice(InputDevice dev) { - ControllerContext context = new ControllerContext(); + + private ControllerContext createContextForDevice(InputDevice dev) { + ControllerContext context = new ControllerContext(); String devName = dev.getName(); LimeLog.info("Creating controller context for device: "+devName); @@ -148,91 +148,91 @@ public class ControllerHandler implements InputManager.InputDeviceListener { context.id = dev.getId(); context.leftStickXAxis = MotionEvent.AXIS_X; - context.leftStickYAxis = MotionEvent.AXIS_Y; + context.leftStickYAxis = MotionEvent.AXIS_Y; if (getMotionRangeForJoystickAxis(dev, context.leftStickXAxis) != null && getMotionRangeForJoystickAxis(dev, context.leftStickYAxis) != null) { // This is a gamepad hasGameController = true; context.hasJoystickAxes = true; } - - InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER); - InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER); - InputDevice.MotionRange brakeRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_BRAKE); - InputDevice.MotionRange gasRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_GAS); - if (leftTriggerRange != null && rightTriggerRange != null) - { - // Some controllers use LTRIGGER and RTRIGGER (like Ouya) - context.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER; - context.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; - } - else - { - InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX); - InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY); - 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; - - // Xbox controllers use Z and RZ for triggers - context.leftTriggerAxis = MotionEvent.AXIS_Z; - context.rightTriggerAxis = MotionEvent.AXIS_RZ; - context.triggersIdleNegative = true; - context.isXboxController = true; - } - else { - // DS4 controller uses RX and RY for triggers - context.leftTriggerAxis = MotionEvent.AXIS_RX; - context.rightTriggerAxis = MotionEvent.AXIS_RY; - context.triggersIdleNegative = true; - - context.isDualShock4 = true; - } - } - } - - if (context.rightStickXAxis == -1 && context.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; - } - else { - InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX); - InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY); - - // Try RX and RY now - if (rxRange != null && ryRange != null) { - context.rightStickXAxis = MotionEvent.AXIS_RX; - context.rightStickYAxis = MotionEvent.AXIS_RY; - } - } - } - - // Some devices have "hats" for d-pads - 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; - } - - if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) { + + InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER); + InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER); + InputDevice.MotionRange brakeRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_BRAKE); + InputDevice.MotionRange gasRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_GAS); + if (leftTriggerRange != null && rightTriggerRange != null) + { + // Some controllers use LTRIGGER and RTRIGGER (like Ouya) + context.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER; + context.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; + } + else + { + InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX); + InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY); + 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; + + // Xbox controllers use Z and RZ for triggers + context.leftTriggerAxis = MotionEvent.AXIS_Z; + context.rightTriggerAxis = MotionEvent.AXIS_RZ; + context.triggersIdleNegative = true; + context.isXboxController = true; + } + else { + // DS4 controller uses RX and RY for triggers + context.leftTriggerAxis = MotionEvent.AXIS_RX; + context.rightTriggerAxis = MotionEvent.AXIS_RY; + context.triggersIdleNegative = true; + + context.isDualShock4 = true; + } + } + } + + if (context.rightStickXAxis == -1 && context.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; + } + else { + InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX); + InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY); + + // Try RX and RY now + if (rxRange != null && ryRange != null) { + context.rightStickXAxis = MotionEvent.AXIS_RX; + context.rightStickYAxis = MotionEvent.AXIS_RY; + } + } + } + + // Some devices have "hats" for d-pads + 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; + } + + if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) { context.leftStickDeadzoneRadius = (float) stickDeadzone; - } - - if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) { + } + + if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) { context.rightStickDeadzoneRadius = (float) stickDeadzone; } @@ -300,114 +300,114 @@ public class ControllerHandler implements InputManager.InputDeviceListener { LimeLog.info("Assigned as controller "+context.controllerNumber); return context; - } - - private ControllerContext getContextForDevice(InputDevice dev) { - // Unknown devices use the default context - if (dev == null) { - return defaultContext; - } - - String descriptor = dev.getDescriptor(); - - // Return the existing context if it exists - ControllerContext context = contexts.get(descriptor); - if (context != null) { - return context; - } - - // Otherwise create a new context + } + + private ControllerContext getContextForDevice(InputDevice dev) { + // Unknown devices use the default context + if (dev == null) { + return defaultContext; + } + + String descriptor = dev.getDescriptor(); + + // Return the existing context if it exists + ControllerContext context = contexts.get(descriptor); + if (context != null) { + return context; + } + + // Otherwise create a new context context = createContextForDevice(dev); - contexts.put(descriptor, context); - - return context; - } - - private void sendControllerInputPacket(ControllerContext context) { - conn.sendControllerInput(context.controllerNumber, context.inputMap, + contexts.put(descriptor, context); + + return context; + } + + private void sendControllerInputPacket(ControllerContext context) { + conn.sendControllerInput(context.controllerNumber, context.inputMap, context.leftTrigger, context.rightTrigger, context.leftStickX, context.leftStickY, context.rightStickX, context.rightStickY); - } + } // Return a valid keycode, 0 to consume, or -1 to not consume the event // Device MAY BE NULL - private int handleRemapping(ControllerContext context, KeyEvent event) { + private int handleRemapping(ControllerContext context, KeyEvent event) { // For remotes, don't capture the back button - if (context.isRemote) { + if (context.isRemote) { if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { return -1; } } if (context.isDualShock4) { - switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_BUTTON_Y: - return KeyEvent.KEYCODE_BUTTON_L1; - - case KeyEvent.KEYCODE_BUTTON_Z: - return KeyEvent.KEYCODE_BUTTON_R1; - - case KeyEvent.KEYCODE_BUTTON_C: - return KeyEvent.KEYCODE_BUTTON_B; - - case KeyEvent.KEYCODE_BUTTON_X: - return KeyEvent.KEYCODE_BUTTON_Y; - - case KeyEvent.KEYCODE_BUTTON_B: - return KeyEvent.KEYCODE_BUTTON_A; - - case KeyEvent.KEYCODE_BUTTON_A: - return KeyEvent.KEYCODE_BUTTON_X; - - case KeyEvent.KEYCODE_BUTTON_SELECT: - return KeyEvent.KEYCODE_BUTTON_THUMBL; - - case KeyEvent.KEYCODE_BUTTON_START: - return KeyEvent.KEYCODE_BUTTON_THUMBR; - - case KeyEvent.KEYCODE_BUTTON_L2: - return KeyEvent.KEYCODE_BUTTON_SELECT; - - case KeyEvent.KEYCODE_BUTTON_R2: - return KeyEvent.KEYCODE_BUTTON_START; - - // These are duplicate trigger events - case KeyEvent.KEYCODE_BUTTON_R1: - case KeyEvent.KEYCODE_BUTTON_L1: - return 0; - } - } - - if (context.hatXAxis != -1 && context.hatYAxis != -1) { - switch (event.getKeyCode()) { - // These are duplicate dpad events for hat input - case KeyEvent.KEYCODE_DPAD_LEFT: - case KeyEvent.KEYCODE_DPAD_RIGHT: - case KeyEvent.KEYCODE_DPAD_CENTER: - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_DOWN: - return 0; - } - } - else if (context.hatXAxis == -1 && - context.hatYAxis == -1 && - context.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 - switch (event.getScanCode()) - { - case 704: - return KeyEvent.KEYCODE_DPAD_LEFT; - case 705: - return KeyEvent.KEYCODE_DPAD_RIGHT; - case 706: - return KeyEvent.KEYCODE_DPAD_UP; - case 707: - return KeyEvent.KEYCODE_DPAD_DOWN; - } - } + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_BUTTON_Y: + return KeyEvent.KEYCODE_BUTTON_L1; + + case KeyEvent.KEYCODE_BUTTON_Z: + return KeyEvent.KEYCODE_BUTTON_R1; + + case KeyEvent.KEYCODE_BUTTON_C: + return KeyEvent.KEYCODE_BUTTON_B; + + case KeyEvent.KEYCODE_BUTTON_X: + return KeyEvent.KEYCODE_BUTTON_Y; + + case KeyEvent.KEYCODE_BUTTON_B: + return KeyEvent.KEYCODE_BUTTON_A; + + case KeyEvent.KEYCODE_BUTTON_A: + return KeyEvent.KEYCODE_BUTTON_X; + + case KeyEvent.KEYCODE_BUTTON_SELECT: + return KeyEvent.KEYCODE_BUTTON_THUMBL; + + case KeyEvent.KEYCODE_BUTTON_START: + return KeyEvent.KEYCODE_BUTTON_THUMBR; + + case KeyEvent.KEYCODE_BUTTON_L2: + return KeyEvent.KEYCODE_BUTTON_SELECT; + + case KeyEvent.KEYCODE_BUTTON_R2: + return KeyEvent.KEYCODE_BUTTON_START; + + // These are duplicate trigger events + case KeyEvent.KEYCODE_BUTTON_R1: + case KeyEvent.KEYCODE_BUTTON_L1: + return 0; + } + } + + if (context.hatXAxis != -1 && context.hatYAxis != -1) { + switch (event.getKeyCode()) { + // These are duplicate dpad events for hat input + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + return 0; + } + } + else if (context.hatXAxis == -1 && + context.hatYAxis == -1 && + context.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 + switch (event.getScanCode()) + { + case 704: + return KeyEvent.KEYCODE_DPAD_LEFT; + case 705: + return KeyEvent.KEYCODE_DPAD_RIGHT; + case 706: + return KeyEvent.KEYCODE_DPAD_UP; + case 707: + return KeyEvent.KEYCODE_DPAD_DOWN; + } + } // Past here we can fixup the keycode and potentially trigger // another special case so we need to remember what keycode we're using @@ -439,21 +439,21 @@ public class ControllerHandler implements InputManager.InputDeviceListener { // Emulate the select button with mode return KeyEvent.KEYCODE_BUTTON_SELECT; } - - return keyCode; - } + + return keyCode; + } private Vector2d populateCachedVector(float x, float y) { // Reinitialize our cached Vector2d object inputVector.initialize(x, y); return inputVector; } - - private void handleDeadZone(Vector2d stickVector, float deadzoneRadius) { - if (stickVector.getMagnitude() <= deadzoneRadius) { - // Deadzone + + private void handleDeadZone(Vector2d stickVector, float deadzoneRadius) { + if (stickVector.getMagnitude() <= deadzoneRadius) { + // Deadzone stickVector.initialize(0, 0); - } + } // We're not normalizing here because we let the computer handle the deadzones. // Normalizing can make the deadzones larger than they should be after the computer also @@ -518,9 +518,9 @@ public class ControllerHandler implements InputManager.InputDeviceListener { sendControllerInputPacket(context); } - - public boolean handleMotionEvent(MotionEvent event) { - ControllerContext context = getContextForDevice(event.getDevice()); + + public boolean handleMotionEvent(MotionEvent event) { + ControllerContext context = getContextForDevice(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 @@ -548,255 +548,255 @@ public class ControllerHandler implements InputManager.InputDeviceListener { handleAxisSet(context, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY); - return true; - } - - public boolean handleButtonUp(KeyEvent event) { - ControllerContext context = getContextForDevice(event.getDevice()); + return true; + } + + public boolean handleButtonUp(KeyEvent event) { + ControllerContext context = getContextForDevice(event.getDevice()); int keyCode = handleRemapping(context, event); - if (keyCode == 0) { - 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 ignored) {} - } - - switch (keyCode) { - case KeyEvent.KEYCODE_BUTTON_MODE: + if (keyCode == 0) { + 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 ignored) {} + } + + switch (keyCode) { + case KeyEvent.KEYCODE_BUTTON_MODE: context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG; - break; - case KeyEvent.KEYCODE_BUTTON_START: - case KeyEvent.KEYCODE_MENU: + break; + case KeyEvent.KEYCODE_BUTTON_START: + case KeyEvent.KEYCODE_MENU: if (SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) { gestures.showKeyboard(); } context.inputMap &= ~ControllerPacket.PLAY_FLAG; - break; - case KeyEvent.KEYCODE_BACK: - case KeyEvent.KEYCODE_BUTTON_SELECT: + break; + case KeyEvent.KEYCODE_BACK: + case KeyEvent.KEYCODE_BUTTON_SELECT: context.inputMap &= ~ControllerPacket.BACK_FLAG; - break; - case KeyEvent.KEYCODE_DPAD_LEFT: + break; + case KeyEvent.KEYCODE_DPAD_LEFT: context.inputMap &= ~ControllerPacket.LEFT_FLAG; - break; - case KeyEvent.KEYCODE_DPAD_RIGHT: + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: context.inputMap &= ~ControllerPacket.RIGHT_FLAG; - break; - case KeyEvent.KEYCODE_DPAD_UP: + break; + case KeyEvent.KEYCODE_DPAD_UP: context.inputMap &= ~ControllerPacket.UP_FLAG; - break; - case KeyEvent.KEYCODE_DPAD_DOWN: + break; + case KeyEvent.KEYCODE_DPAD_DOWN: context.inputMap &= ~ControllerPacket.DOWN_FLAG; - break; - case KeyEvent.KEYCODE_BUTTON_B: + break; + case KeyEvent.KEYCODE_BUTTON_B: context.inputMap &= ~ControllerPacket.B_FLAG; - break; - case KeyEvent.KEYCODE_DPAD_CENTER: - case KeyEvent.KEYCODE_BUTTON_A: + break; + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_BUTTON_A: context.inputMap &= ~ControllerPacket.A_FLAG; - break; - case KeyEvent.KEYCODE_BUTTON_X: + break; + case KeyEvent.KEYCODE_BUTTON_X: context.inputMap &= ~ControllerPacket.X_FLAG; - break; - case KeyEvent.KEYCODE_BUTTON_Y: + break; + case KeyEvent.KEYCODE_BUTTON_Y: context.inputMap &= ~ControllerPacket.Y_FLAG; - break; - case KeyEvent.KEYCODE_BUTTON_L1: + break; + case KeyEvent.KEYCODE_BUTTON_L1: context.inputMap &= ~ControllerPacket.LB_FLAG; context.lastLbUpTime = SystemClock.uptimeMillis(); - break; - case KeyEvent.KEYCODE_BUTTON_R1: + break; + case KeyEvent.KEYCODE_BUTTON_R1: context.inputMap &= ~ControllerPacket.RB_FLAG; context.lastRbUpTime = SystemClock.uptimeMillis(); - break; - case KeyEvent.KEYCODE_BUTTON_THUMBL: + break; + case KeyEvent.KEYCODE_BUTTON_THUMBL: context.inputMap &= ~ControllerPacket.LS_CLK_FLAG; - break; - case KeyEvent.KEYCODE_BUTTON_THUMBR: + break; + case KeyEvent.KEYCODE_BUTTON_THUMBR: context.inputMap &= ~ControllerPacket.RS_CLK_FLAG; - break; - case KeyEvent.KEYCODE_BUTTON_L2: + break; + case KeyEvent.KEYCODE_BUTTON_L2: context.leftTrigger = 0; - break; - case KeyEvent.KEYCODE_BUTTON_R2: + break; + case KeyEvent.KEYCODE_BUTTON_R2: context.rightTrigger = 0; - break; - default: - return false; - } - - // Check if we're emulating the select button - if ((context.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) - { + break; + default: + return false; + } + + // Check if we're emulating the select button + if ((context.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) + { context.inputMap &= ~ControllerPacket.BACK_FLAG; context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT; - - try { - Thread.sleep(EMULATED_SELECT_UP_DELAY_MS); - } catch (InterruptedException ignored) {} - } - } - - // Check if we're emulating the special button - if ((context.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)) - { + + try { + Thread.sleep(EMULATED_SELECT_UP_DELAY_MS); + } catch (InterruptedException ignored) {} + } + } + + // Check if we're emulating the special button + if ((context.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)) + { context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG; context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL; - - try { - Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS); - } catch (InterruptedException ignored) {} - } - } - - sendControllerInputPacket(context); - return true; - } - - public boolean handleButtonDown(KeyEvent event) { - ControllerContext context = getContextForDevice(event.getDevice()); + + try { + Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS); + } catch (InterruptedException ignored) {} + } + } + + sendControllerInputPacket(context); + return true; + } + + public boolean handleButtonDown(KeyEvent event) { + ControllerContext context = getContextForDevice(event.getDevice()); int keyCode = handleRemapping(context, event); - if (keyCode == 0) { - return true; - } - - switch (keyCode) { - case KeyEvent.KEYCODE_BUTTON_MODE: + if (keyCode == 0) { + return true; + } + + switch (keyCode) { + case KeyEvent.KEYCODE_BUTTON_MODE: context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG; - break; - case KeyEvent.KEYCODE_BUTTON_START: - case KeyEvent.KEYCODE_MENU: + break; + case KeyEvent.KEYCODE_BUTTON_START: + case KeyEvent.KEYCODE_MENU: if (event.getRepeatCount() == 0) { context.startDownTime = SystemClock.uptimeMillis(); } context.inputMap |= ControllerPacket.PLAY_FLAG; - break; - case KeyEvent.KEYCODE_BACK: - case KeyEvent.KEYCODE_BUTTON_SELECT: + break; + case KeyEvent.KEYCODE_BACK: + case KeyEvent.KEYCODE_BUTTON_SELECT: context.inputMap |= ControllerPacket.BACK_FLAG; - break; - case KeyEvent.KEYCODE_DPAD_LEFT: + break; + case KeyEvent.KEYCODE_DPAD_LEFT: context.inputMap |= ControllerPacket.LEFT_FLAG; - break; - case KeyEvent.KEYCODE_DPAD_RIGHT: + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: context.inputMap |= ControllerPacket.RIGHT_FLAG; - break; - case KeyEvent.KEYCODE_DPAD_UP: + break; + case KeyEvent.KEYCODE_DPAD_UP: context.inputMap |= ControllerPacket.UP_FLAG; - break; - case KeyEvent.KEYCODE_DPAD_DOWN: + break; + case KeyEvent.KEYCODE_DPAD_DOWN: context.inputMap |= ControllerPacket.DOWN_FLAG; - break; - case KeyEvent.KEYCODE_BUTTON_B: + break; + case KeyEvent.KEYCODE_BUTTON_B: context.inputMap |= ControllerPacket.B_FLAG; - break; - case KeyEvent.KEYCODE_DPAD_CENTER: - case KeyEvent.KEYCODE_BUTTON_A: + break; + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_BUTTON_A: context.inputMap |= ControllerPacket.A_FLAG; - break; - case KeyEvent.KEYCODE_BUTTON_X: + break; + case KeyEvent.KEYCODE_BUTTON_X: context.inputMap |= ControllerPacket.X_FLAG; - break; - case KeyEvent.KEYCODE_BUTTON_Y: + break; + case KeyEvent.KEYCODE_BUTTON_Y: context.inputMap |= ControllerPacket.Y_FLAG; - break; - case KeyEvent.KEYCODE_BUTTON_L1: + break; + case KeyEvent.KEYCODE_BUTTON_L1: context.inputMap |= ControllerPacket.LB_FLAG; - break; - case KeyEvent.KEYCODE_BUTTON_R1: + break; + case KeyEvent.KEYCODE_BUTTON_R1: context.inputMap |= ControllerPacket.RB_FLAG; - break; - case KeyEvent.KEYCODE_BUTTON_THUMBL: + break; + case KeyEvent.KEYCODE_BUTTON_THUMBL: context.inputMap |= ControllerPacket.LS_CLK_FLAG; - break; - case KeyEvent.KEYCODE_BUTTON_THUMBR: + break; + case KeyEvent.KEYCODE_BUTTON_THUMBR: context.inputMap |= ControllerPacket.RS_CLK_FLAG; - break; - case KeyEvent.KEYCODE_BUTTON_L2: + break; + case KeyEvent.KEYCODE_BUTTON_L2: context.leftTrigger = (byte)0xFF; - break; - case KeyEvent.KEYCODE_BUTTON_R2: + break; + case KeyEvent.KEYCODE_BUTTON_R2: context.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)) - { + 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)) + { context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG); context.inputMap |= ControllerPacket.BACK_FLAG; context.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) - { + } + + // 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) + { context.inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG); context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG; context.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); } - return true; - } + return true; + } class ControllerContext { public String name; public int id; - public int leftStickXAxis = -1; - public int leftStickYAxis = -1; - public float leftStickDeadzoneRadius; + public int leftStickXAxis = -1; + public int leftStickYAxis = -1; + public float leftStickDeadzoneRadius; - public int rightStickXAxis = -1; - public int rightStickYAxis = -1; - public float rightStickDeadzoneRadius; - - public int leftTriggerAxis = -1; - public int rightTriggerAxis = -1; - public boolean triggersIdleNegative; + public int rightStickXAxis = -1; + public int rightStickYAxis = -1; + public float rightStickDeadzoneRadius; + + public int leftTriggerAxis = -1; + public int rightTriggerAxis = -1; + public boolean triggersIdleNegative; public float triggerDeadzone; - - public int hatXAxis = -1; - public int hatYAxis = -1; - - public boolean isDualShock4; - public boolean isXboxController; + + public int hatXAxis = -1; + public int hatYAxis = -1; + + public boolean isDualShock4; + public boolean isXboxController; public boolean backIsStart; public boolean modeIsSelect; public boolean isRemote; @@ -822,5 +822,5 @@ public class ControllerHandler implements InputManager.InputDeviceListener { public long lastRbUpTime = 0; public long startDownTime = 0; - } + } } diff --git a/app/src/main/java/com/limelight/binding/input/KeyboardTranslator.java b/app/src/main/java/com/limelight/binding/input/KeyboardTranslator.java index bfaf2dbc..f087215f 100644 --- a/app/src/main/java/com/limelight/binding/input/KeyboardTranslator.java +++ b/app/src/main/java/com/limelight/binding/input/KeyboardTranslator.java @@ -23,8 +23,8 @@ public class KeyboardTranslator extends KeycodeTranslator { public static final int VK_Z = 90; public static final int VK_ALT = 18; public static final int VK_NUMPAD0 = 96; - public static final int VK_BACK_SLASH = 92; - public static final int VK_CAPS_LOCK = 20; + public static final int VK_BACK_SLASH = 92; + public static final int VK_CAPS_LOCK = 20; public static final int VK_CLEAR = 12; public static final int VK_COMMA = 44; public static final int VK_CONTROL = 17; diff --git a/app/src/main/java/com/limelight/binding/input/TouchContext.java b/app/src/main/java/com/limelight/binding/input/TouchContext.java index 179e5034..113771c6 100644 --- a/app/src/main/java/com/limelight/binding/input/TouchContext.java +++ b/app/src/main/java/com/limelight/binding/input/TouchContext.java @@ -4,95 +4,95 @@ import com.limelight.nvstream.NvConnection; import com.limelight.nvstream.input.MouseButtonPacket; public class TouchContext { - private int lastTouchX = 0; - private int lastTouchY = 0; - private int originalTouchX = 0; - private int originalTouchY = 0; - private long originalTouchTime = 0; + private int lastTouchX = 0; + private int lastTouchY = 0; + private int originalTouchX = 0; + private int originalTouchY = 0; + private long originalTouchTime = 0; private boolean cancelled; - - private final NvConnection conn; - private final int actionIndex; + + private final NvConnection conn; + private final int actionIndex; private final double xFactor; private final double yFactor; - - private static final int TAP_MOVEMENT_THRESHOLD = 10; - private static final int TAP_TIME_THRESHOLD = 250; - - public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor) - { - this.conn = conn; - this.actionIndex = actionIndex; + + private static final int TAP_MOVEMENT_THRESHOLD = 10; + private static final int TAP_TIME_THRESHOLD = 250; + + public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor) + { + this.conn = conn; + this.actionIndex = actionIndex; this.xFactor = xFactor; this.yFactor = yFactor; - } + } public int getActionIndex() { return actionIndex; } - - private boolean isTap() - { - int xDelta = Math.abs(lastTouchX - originalTouchX); - int yDelta = Math.abs(lastTouchY - originalTouchY); - long timeDelta = System.currentTimeMillis() - originalTouchTime; - - return xDelta <= TAP_MOVEMENT_THRESHOLD && - yDelta <= TAP_MOVEMENT_THRESHOLD && - timeDelta <= TAP_TIME_THRESHOLD; - } - - private byte getMouseButtonIndex() - { - if (actionIndex == 1) { - return MouseButtonPacket.BUTTON_RIGHT; - } - else { - return MouseButtonPacket.BUTTON_LEFT; - } - } - - public boolean touchDownEvent(int eventX, int eventY) - { - originalTouchX = lastTouchX = eventX; - originalTouchY = lastTouchY = eventY; - originalTouchTime = System.currentTimeMillis(); + + private boolean isTap() + { + int xDelta = Math.abs(lastTouchX - originalTouchX); + int yDelta = Math.abs(lastTouchY - originalTouchY); + long timeDelta = System.currentTimeMillis() - originalTouchTime; + + return xDelta <= TAP_MOVEMENT_THRESHOLD && + yDelta <= TAP_MOVEMENT_THRESHOLD && + timeDelta <= TAP_TIME_THRESHOLD; + } + + private byte getMouseButtonIndex() + { + if (actionIndex == 1) { + return MouseButtonPacket.BUTTON_RIGHT; + } + else { + return MouseButtonPacket.BUTTON_LEFT; + } + } + + public boolean touchDownEvent(int eventX, int eventY) + { + originalTouchX = lastTouchX = eventX; + originalTouchY = lastTouchY = eventY; + originalTouchTime = System.currentTimeMillis(); cancelled = false; return true; - } - - public void touchUpEvent(int eventX, int eventY) - { + } + + public void touchUpEvent(int eventX, int eventY) + { if (cancelled) { return; } - if (isTap()) - { - byte buttonIndex = getMouseButtonIndex(); - - // Lower the mouse button - conn.sendMouseButtonDown(buttonIndex); - - // We need to sleep a bit here because some games - // do input detection by polling - try { - Thread.sleep(100); - } catch (InterruptedException ignored) {} - - // Raise the mouse button - conn.sendMouseButtonUp(buttonIndex); - } - } - - public boolean touchMoveEvent(int eventX, int eventY) + if (isTap()) + { + byte buttonIndex = getMouseButtonIndex(); + + // Lower the mouse button + conn.sendMouseButtonDown(buttonIndex); + + // We need to sleep a bit here because some games + // do input detection by polling + try { + Thread.sleep(100); + } catch (InterruptedException ignored) {} + + // Raise the mouse button + conn.sendMouseButtonUp(buttonIndex); + } + } + + public boolean touchMoveEvent(int eventX, int eventY) { if (eventX != lastTouchX || eventY != lastTouchY) - { - // We only send moves for the primary touch point - if (actionIndex == 0) { + { + // We only send moves for the primary touch point + if (actionIndex == 0) { int deltaX = eventX - lastTouchX; int deltaY = eventY - lastTouchY; @@ -100,15 +100,15 @@ public class TouchContext { deltaX = (int)Math.round((double)deltaX * xFactor); deltaY = (int)Math.round((double)deltaY * yFactor); - conn.sendMouseMove((short)deltaX, (short)deltaY); - } - - lastTouchX = eventX; - lastTouchY = eventY; - } - - return true; - } + conn.sendMouseMove((short)deltaX, (short)deltaY); + } + + lastTouchX = eventX; + lastTouchY = eventY; + } + + return true; + } public void cancelTouch() { cancelled = true; diff --git a/app/src/main/java/com/limelight/binding/input/evdev/EvdevEvent.java b/app/src/main/java/com/limelight/binding/input/evdev/EvdevEvent.java index 0addf697..5e04f168 100644 --- a/app/src/main/java/com/limelight/binding/input/evdev/EvdevEvent.java +++ b/app/src/main/java/com/limelight/binding/input/evdev/EvdevEvent.java @@ -1,41 +1,41 @@ package com.limelight.binding.input.evdev; public class EvdevEvent { - public static final int EVDEV_MIN_EVENT_SIZE = 16; - public static final int EVDEV_MAX_EVENT_SIZE = 24; - - /* Event types */ - public static final short EV_SYN = 0x00; - public static final short EV_KEY = 0x01; - public static final short EV_REL = 0x02; - public static final short EV_MSC = 0x04; - - /* Relative axes */ - public static final short REL_X = 0x00; - public static final short REL_Y = 0x01; - public static final short REL_WHEEL = 0x08; - - /* Buttons */ - public static final short BTN_LEFT = 0x110; - public static final short BTN_RIGHT = 0x111; - public static final short BTN_MIDDLE = 0x112; - public static final short BTN_SIDE = 0x113; - public static final short BTN_EXTRA = 0x114; - public static final short BTN_FORWARD = 0x115; - public static final short BTN_BACK = 0x116; - public static final short BTN_TASK = 0x117; - public static final short BTN_GAMEPAD = 0x130; - - /* Keys */ - public static final short KEY_Q = 16; - - public final short type; - public final short code; - public final int value; - - public EvdevEvent(short type, short code, int value) { - this.type = type; - this.code = code; - this.value = value; - } + public static final int EVDEV_MIN_EVENT_SIZE = 16; + public static final int EVDEV_MAX_EVENT_SIZE = 24; + + /* Event types */ + public static final short EV_SYN = 0x00; + public static final short EV_KEY = 0x01; + public static final short EV_REL = 0x02; + public static final short EV_MSC = 0x04; + + /* Relative axes */ + public static final short REL_X = 0x00; + public static final short REL_Y = 0x01; + public static final short REL_WHEEL = 0x08; + + /* Buttons */ + public static final short BTN_LEFT = 0x110; + public static final short BTN_RIGHT = 0x111; + public static final short BTN_MIDDLE = 0x112; + public static final short BTN_SIDE = 0x113; + public static final short BTN_EXTRA = 0x114; + public static final short BTN_FORWARD = 0x115; + public static final short BTN_BACK = 0x116; + public static final short BTN_TASK = 0x117; + public static final short BTN_GAMEPAD = 0x130; + + /* Keys */ + public static final short KEY_Q = 16; + + public final short type; + public final short code; + public final int value; + + public EvdevEvent(short type, short code, int value) { + this.type = type; + this.code = code; + this.value = value; + } } diff --git a/app/src/main/java/com/limelight/binding/input/evdev/EvdevHandler.java b/app/src/main/java/com/limelight/binding/input/evdev/EvdevHandler.java index f60d751e..0a164ff9 100644 --- a/app/src/main/java/com/limelight/binding/input/evdev/EvdevHandler.java +++ b/app/src/main/java/com/limelight/binding/input/evdev/EvdevHandler.java @@ -7,161 +7,161 @@ import com.limelight.LimeLog; public class EvdevHandler { - private final String absolutePath; - private final EvdevListener listener; - private boolean shutdown = false; - private int fd = -1; - - private final Thread handlerThread = new Thread() { - @Override - public void run() { - // All the finally blocks here make this code look like a mess - // but it's important that we get this right to avoid causing - // system-wide input problems. - - // Open the /dev/input/eventX file - fd = EvdevReader.open(absolutePath); - if (fd == -1) { - LimeLog.warning("Unable to open "+absolutePath); - return; - } + private final String absolutePath; + private final EvdevListener listener; + private boolean shutdown = false; + private int fd = -1; - try { - // Check if it's a mouse or keyboard, but not a gamepad - if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) || - EvdevReader.isGamepad(fd)) { - // We only handle keyboards and mice - return; - } + private final Thread handlerThread = new Thread() { + @Override + public void run() { + // All the finally blocks here make this code look like a mess + // but it's important that we get this right to avoid causing + // system-wide input problems. - // Grab it for ourselves - if (!EvdevReader.grab(fd)) { - LimeLog.warning("Unable to grab "+absolutePath); - return; - } + // Open the /dev/input/eventX file + fd = EvdevReader.open(absolutePath); + if (fd == -1) { + LimeLog.warning("Unable to open "+absolutePath); + return; + } - LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath); + try { + // Check if it's a mouse or keyboard, but not a gamepad + if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) || + EvdevReader.isGamepad(fd)) { + // We only handle keyboards and mice + return; + } - ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder()); + // Grab it for ourselves + if (!EvdevReader.grab(fd)) { + LimeLog.warning("Unable to grab "+absolutePath); + return; + } - try { - int deltaX = 0; - int deltaY = 0; - byte deltaScroll = 0; + LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath); - while (!isInterrupted() && !shutdown) { - EvdevEvent event = EvdevReader.read(fd, buffer); - if (event == null) { - return; - } + ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder()); - switch (event.type) - { - case EvdevEvent.EV_SYN: - if (deltaX != 0 || deltaY != 0) { - listener.mouseMove(deltaX, deltaY); - deltaX = deltaY = 0; - } - if (deltaScroll != 0) { - listener.mouseScroll(deltaScroll); - deltaScroll = 0; - } - break; + try { + int deltaX = 0; + int deltaY = 0; + byte deltaScroll = 0; - case EvdevEvent.EV_REL: - switch (event.code) - { - case EvdevEvent.REL_X: - deltaX = event.value; - break; - case EvdevEvent.REL_Y: - deltaY = event.value; - break; - case EvdevEvent.REL_WHEEL: - deltaScroll = (byte) event.value; - break; - } - break; + while (!isInterrupted() && !shutdown) { + EvdevEvent event = EvdevReader.read(fd, buffer); + if (event == null) { + return; + } - case EvdevEvent.EV_KEY: - switch (event.code) - { - case EvdevEvent.BTN_LEFT: - listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT, - event.value != 0); - break; - case EvdevEvent.BTN_MIDDLE: - listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE, - event.value != 0); - break; - case EvdevEvent.BTN_RIGHT: - listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT, - event.value != 0); - break; - - case EvdevEvent.BTN_SIDE: - case EvdevEvent.BTN_EXTRA: - case EvdevEvent.BTN_FORWARD: - case EvdevEvent.BTN_BACK: - case EvdevEvent.BTN_TASK: - // Other unhandled mouse buttons - break; - - default: - // We got some unrecognized button. This means - // someone is trying to use the other device in this - // "combination" input device. We'll try to handle - // it via keyboard, but we're not going to disconnect - // if we can't - short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code); - if (keyCode != 0) { - listener.keyboardEvent(event.value != 0, keyCode); - } - break; - } - break; - - case EvdevEvent.EV_MSC: - break; - } - } - } finally { - // Release our grab - EvdevReader.ungrab(fd); - } - } finally { - // Close the file - EvdevReader.close(fd); - } - } - }; - - public EvdevHandler(String absolutePath, EvdevListener listener) { - this.absolutePath = absolutePath; - this.listener = listener; - } - - public void start() { - handlerThread.start(); - } - - public void stop() { - // Close the fd. It doesn't matter if this races - // with the handler thread. We'll close this out from - // under the thread to wake it up - if (fd != -1) { - EvdevReader.close(fd); - } - - shutdown = true; - handlerThread.interrupt(); - - try { - handlerThread.join(); - } catch (InterruptedException ignored) {} - } - - public void notifyDeleted() { - stop(); - } + switch (event.type) + { + case EvdevEvent.EV_SYN: + if (deltaX != 0 || deltaY != 0) { + listener.mouseMove(deltaX, deltaY); + deltaX = deltaY = 0; + } + if (deltaScroll != 0) { + listener.mouseScroll(deltaScroll); + deltaScroll = 0; + } + break; + + case EvdevEvent.EV_REL: + switch (event.code) + { + case EvdevEvent.REL_X: + deltaX = event.value; + break; + case EvdevEvent.REL_Y: + deltaY = event.value; + break; + case EvdevEvent.REL_WHEEL: + deltaScroll = (byte) event.value; + break; + } + break; + + case EvdevEvent.EV_KEY: + switch (event.code) + { + case EvdevEvent.BTN_LEFT: + listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT, + event.value != 0); + break; + case EvdevEvent.BTN_MIDDLE: + listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE, + event.value != 0); + break; + case EvdevEvent.BTN_RIGHT: + listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT, + event.value != 0); + break; + + case EvdevEvent.BTN_SIDE: + case EvdevEvent.BTN_EXTRA: + case EvdevEvent.BTN_FORWARD: + case EvdevEvent.BTN_BACK: + case EvdevEvent.BTN_TASK: + // Other unhandled mouse buttons + break; + + default: + // We got some unrecognized button. This means + // someone is trying to use the other device in this + // "combination" input device. We'll try to handle + // it via keyboard, but we're not going to disconnect + // if we can't + short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code); + if (keyCode != 0) { + listener.keyboardEvent(event.value != 0, keyCode); + } + break; + } + break; + + case EvdevEvent.EV_MSC: + break; + } + } + } finally { + // Release our grab + EvdevReader.ungrab(fd); + } + } finally { + // Close the file + EvdevReader.close(fd); + } + } + }; + + public EvdevHandler(String absolutePath, EvdevListener listener) { + this.absolutePath = absolutePath; + this.listener = listener; + } + + public void start() { + handlerThread.start(); + } + + public void stop() { + // Close the fd. It doesn't matter if this races + // with the handler thread. We'll close this out from + // under the thread to wake it up + if (fd != -1) { + EvdevReader.close(fd); + } + + shutdown = true; + handlerThread.interrupt(); + + try { + handlerThread.join(); + } catch (InterruptedException ignored) {} + } + + public void notifyDeleted() { + stop(); + } } diff --git a/app/src/main/java/com/limelight/binding/input/evdev/EvdevListener.java b/app/src/main/java/com/limelight/binding/input/evdev/EvdevListener.java index 909627a5..926a637d 100644 --- a/app/src/main/java/com/limelight/binding/input/evdev/EvdevListener.java +++ b/app/src/main/java/com/limelight/binding/input/evdev/EvdevListener.java @@ -1,12 +1,12 @@ package com.limelight.binding.input.evdev; public interface EvdevListener { - public static final int BUTTON_LEFT = 1; - public static final int BUTTON_MIDDLE = 2; - public static final int BUTTON_RIGHT = 3; - - public void mouseMove(int deltaX, int deltaY); - public void mouseButtonEvent(int buttonId, boolean down); - public void mouseScroll(byte amount); - public void keyboardEvent(boolean buttonDown, short keyCode); + public static final int BUTTON_LEFT = 1; + public static final int BUTTON_MIDDLE = 2; + public static final int BUTTON_RIGHT = 3; + + public void mouseMove(int deltaX, int deltaY); + public void mouseButtonEvent(int buttonId, boolean down); + public void mouseScroll(byte amount); + public void keyboardEvent(boolean buttonDown, short keyCode); } diff --git a/app/src/main/java/com/limelight/binding/input/evdev/EvdevReader.java b/app/src/main/java/com/limelight/binding/input/evdev/EvdevReader.java index 3ee2ddcd..f6c4b0e5 100644 --- a/app/src/main/java/com/limelight/binding/input/evdev/EvdevReader.java +++ b/app/src/main/java/com/limelight/binding/input/evdev/EvdevReader.java @@ -8,9 +8,9 @@ import java.util.Locale; import com.limelight.LimeLog; public class EvdevReader { - static { - System.loadLibrary("evdev_reader"); - } + static { + System.loadLibrary("evdev_reader"); + } public static void patchSeLinuxPolicies() { // @@ -30,76 +30,76 @@ public class EvdevReader { "\"allow untrusted_app input_device chr_file { open read write ioctl }\""); } } - - // Requires root to chmod /dev/input/eventX - public static void setPermissions(String[] files, int octalPermissions) { + + // Requires root to chmod /dev/input/eventX + public static void setPermissions(String[] files, int octalPermissions) { EvdevShell shell = EvdevShell.getInstance(); for (String file : files) { shell.runCommand(String.format((Locale)null, "chmod %o %s", octalPermissions, file)); } - } - - // Returns the fd to be passed to other function or -1 on error - public static native int open(String fileName); - - // Prevent other apps (including Android itself) from using the device while "grabbed" - public static native boolean grab(int fd); - public static native boolean ungrab(int fd); - - // Used for checking device capabilities - public static native boolean hasRelAxis(int fd, short axis); - public static native boolean hasAbsAxis(int fd, short axis); - public static native boolean hasKey(int fd, short key); - - public static boolean isMouse(int fd) { - // This is the same check that Android does in EventHub.cpp - return hasRelAxis(fd, EvdevEvent.REL_X) && - hasRelAxis(fd, EvdevEvent.REL_Y) && - hasKey(fd, EvdevEvent.BTN_LEFT); - } - - public static boolean isAlphaKeyboard(int fd) { - // This is the same check that Android does in EventHub.cpp - return hasKey(fd, EvdevEvent.KEY_Q); - } - - public static boolean isGamepad(int fd) { - return hasKey(fd, EvdevEvent.BTN_GAMEPAD); - } - - // Returns the bytes read or -1 on error - private static native int read(int fd, byte[] buffer); - - // Takes a byte buffer to use to read the output into. - // This buffer MUST be in native byte order and at least - // EVDEV_MAX_EVENT_SIZE bytes long. - public static EvdevEvent read(int fd, ByteBuffer buffer) { - int bytesRead = read(fd, buffer.array()); - if (bytesRead < 0) { - LimeLog.warning("Failed to read: "+bytesRead); - return null; - } - else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) { - LimeLog.warning("Short read: "+bytesRead); - return null; - } - - buffer.limit(bytesRead); - buffer.rewind(); - - // Throw away the time stamp - if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) { - buffer.getLong(); - buffer.getLong(); - } else { - buffer.getInt(); - buffer.getInt(); - } - - return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt()); - } - - // Closes the fd from open() - public static native int close(int fd); + } + + // Returns the fd to be passed to other function or -1 on error + public static native int open(String fileName); + + // Prevent other apps (including Android itself) from using the device while "grabbed" + public static native boolean grab(int fd); + public static native boolean ungrab(int fd); + + // Used for checking device capabilities + public static native boolean hasRelAxis(int fd, short axis); + public static native boolean hasAbsAxis(int fd, short axis); + public static native boolean hasKey(int fd, short key); + + public static boolean isMouse(int fd) { + // This is the same check that Android does in EventHub.cpp + return hasRelAxis(fd, EvdevEvent.REL_X) && + hasRelAxis(fd, EvdevEvent.REL_Y) && + hasKey(fd, EvdevEvent.BTN_LEFT); + } + + public static boolean isAlphaKeyboard(int fd) { + // This is the same check that Android does in EventHub.cpp + return hasKey(fd, EvdevEvent.KEY_Q); + } + + public static boolean isGamepad(int fd) { + return hasKey(fd, EvdevEvent.BTN_GAMEPAD); + } + + // Returns the bytes read or -1 on error + private static native int read(int fd, byte[] buffer); + + // Takes a byte buffer to use to read the output into. + // This buffer MUST be in native byte order and at least + // EVDEV_MAX_EVENT_SIZE bytes long. + public static EvdevEvent read(int fd, ByteBuffer buffer) { + int bytesRead = read(fd, buffer.array()); + if (bytesRead < 0) { + LimeLog.warning("Failed to read: "+bytesRead); + return null; + } + else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) { + LimeLog.warning("Short read: "+bytesRead); + return null; + } + + buffer.limit(bytesRead); + buffer.rewind(); + + // Throw away the time stamp + if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) { + buffer.getLong(); + buffer.getLong(); + } else { + buffer.getInt(); + buffer.getInt(); + } + + return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt()); + } + + // Closes the fd from open() + public static native int close(int fd); } diff --git a/app/src/main/java/com/limelight/binding/input/evdev/EvdevTranslator.java b/app/src/main/java/com/limelight/binding/input/evdev/EvdevTranslator.java index e7e376c2..303799dd 100644 --- a/app/src/main/java/com/limelight/binding/input/evdev/EvdevTranslator.java +++ b/app/src/main/java/com/limelight/binding/input/evdev/EvdevTranslator.java @@ -4,136 +4,136 @@ import android.view.KeyEvent; public class EvdevTranslator { - private static final short[] EVDEV_KEY_CODES = { - 0, //KeyEvent.VK_RESERVED - KeyEvent.KEYCODE_ESCAPE, - KeyEvent.KEYCODE_1, - KeyEvent.KEYCODE_2, - KeyEvent.KEYCODE_3, - KeyEvent.KEYCODE_4, - KeyEvent.KEYCODE_5, - KeyEvent.KEYCODE_6, - KeyEvent.KEYCODE_7, - KeyEvent.KEYCODE_8, - KeyEvent.KEYCODE_9, - KeyEvent.KEYCODE_0, - KeyEvent.KEYCODE_MINUS, - KeyEvent.KEYCODE_EQUALS, - KeyEvent.KEYCODE_DEL, - KeyEvent.KEYCODE_TAB, - KeyEvent.KEYCODE_Q, - KeyEvent.KEYCODE_W, - KeyEvent.KEYCODE_E, - KeyEvent.KEYCODE_R, - KeyEvent.KEYCODE_T, - KeyEvent.KEYCODE_Y, - KeyEvent.KEYCODE_U, - KeyEvent.KEYCODE_I, - KeyEvent.KEYCODE_O, - KeyEvent.KEYCODE_P, - KeyEvent.KEYCODE_LEFT_BRACKET, - KeyEvent.KEYCODE_RIGHT_BRACKET, - KeyEvent.KEYCODE_ENTER, - KeyEvent.KEYCODE_CTRL_LEFT, - KeyEvent.KEYCODE_A, - KeyEvent.KEYCODE_S, - KeyEvent.KEYCODE_D, - KeyEvent.KEYCODE_F, - KeyEvent.KEYCODE_G, - KeyEvent.KEYCODE_H, - KeyEvent.KEYCODE_J, - KeyEvent.KEYCODE_K, - KeyEvent.KEYCODE_L, - KeyEvent.KEYCODE_SEMICOLON, - KeyEvent.KEYCODE_APOSTROPHE, - KeyEvent.KEYCODE_GRAVE, - KeyEvent.KEYCODE_SHIFT_LEFT, - KeyEvent.KEYCODE_BACKSLASH, - KeyEvent.KEYCODE_Z, - KeyEvent.KEYCODE_X, - KeyEvent.KEYCODE_C, - KeyEvent.KEYCODE_V, - KeyEvent.KEYCODE_B, - KeyEvent.KEYCODE_N, - KeyEvent.KEYCODE_M, - KeyEvent.KEYCODE_COMMA, - KeyEvent.KEYCODE_PERIOD, - KeyEvent.KEYCODE_SLASH, - KeyEvent.KEYCODE_SHIFT_RIGHT, - KeyEvent.KEYCODE_NUMPAD_MULTIPLY, - KeyEvent.KEYCODE_ALT_LEFT, - KeyEvent.KEYCODE_SPACE, - KeyEvent.KEYCODE_CAPS_LOCK, - KeyEvent.KEYCODE_F1, - KeyEvent.KEYCODE_F2, - KeyEvent.KEYCODE_F3, - KeyEvent.KEYCODE_F4, - KeyEvent.KEYCODE_F5, - KeyEvent.KEYCODE_F6, - KeyEvent.KEYCODE_F7, - KeyEvent.KEYCODE_F8, - KeyEvent.KEYCODE_F9, - KeyEvent.KEYCODE_F10, - KeyEvent.KEYCODE_NUM_LOCK, - KeyEvent.KEYCODE_SCROLL_LOCK, - KeyEvent.KEYCODE_NUMPAD_7, - KeyEvent.KEYCODE_NUMPAD_8, - KeyEvent.KEYCODE_NUMPAD_9, - KeyEvent.KEYCODE_NUMPAD_SUBTRACT, - KeyEvent.KEYCODE_NUMPAD_4, - KeyEvent.KEYCODE_NUMPAD_5, - KeyEvent.KEYCODE_NUMPAD_6, - KeyEvent.KEYCODE_NUMPAD_ADD, - KeyEvent.KEYCODE_NUMPAD_1, - KeyEvent.KEYCODE_NUMPAD_2, - KeyEvent.KEYCODE_NUMPAD_3, - KeyEvent.KEYCODE_NUMPAD_0, - KeyEvent.KEYCODE_NUMPAD_DOT, - 0, - 0, //KeyEvent.VK_ZENKAKUHANKAKU, - 0, //KeyEvent.VK_102ND, - KeyEvent.KEYCODE_F11, - KeyEvent.KEYCODE_F12, - 0, //KeyEvent.VK_RO, - 0, //KeyEvent.VK_KATAKANA, - 0, //KeyEvent.VK_HIRAGANA, - 0, //KeyEvent.VK_HENKAN, - 0, //KeyEvent.VK_KATAKANAHIRAGANA, - 0, //KeyEvent.VK_MUHENKAN, - 0, //KeyEvent.VK_KPJPCOMMA, - KeyEvent.KEYCODE_NUMPAD_ENTER, - KeyEvent.KEYCODE_CTRL_RIGHT, - KeyEvent.KEYCODE_NUMPAD_DIVIDE, - KeyEvent.KEYCODE_SYSRQ, - KeyEvent.KEYCODE_ALT_RIGHT, - 0, //KeyEvent.VK_LINEFEED, - KeyEvent.KEYCODE_HOME, - KeyEvent.KEYCODE_DPAD_UP, - KeyEvent.KEYCODE_PAGE_UP, - KeyEvent.KEYCODE_DPAD_LEFT, - KeyEvent.KEYCODE_DPAD_RIGHT, - KeyEvent.KEYCODE_MOVE_END, - KeyEvent.KEYCODE_DPAD_DOWN, - KeyEvent.KEYCODE_PAGE_DOWN, - KeyEvent.KEYCODE_INSERT, - KeyEvent.KEYCODE_FORWARD_DEL, - 0, //KeyEvent.VK_MACRO, - 0, //KeyEvent.VK_MUTE, - 0, //KeyEvent.VK_VOLUMEDOWN, - 0, //KeyEvent.VK_VOLUMEUP, - 0, //KeyEvent.VK_POWER, /* SC System Power Down */ - KeyEvent.KEYCODE_NUMPAD_EQUALS, - 0, //KeyEvent.VK_KPPLUSMINUS, - KeyEvent.KEYCODE_BREAK, - 0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */ - }; - - public static short translateEvdevKeyCode(short evdevKeyCode) { - if (evdevKeyCode < EVDEV_KEY_CODES.length) { - return EVDEV_KEY_CODES[evdevKeyCode]; - } - - return 0; - } + private static final short[] EVDEV_KEY_CODES = { + 0, //KeyEvent.VK_RESERVED + KeyEvent.KEYCODE_ESCAPE, + KeyEvent.KEYCODE_1, + KeyEvent.KEYCODE_2, + KeyEvent.KEYCODE_3, + KeyEvent.KEYCODE_4, + KeyEvent.KEYCODE_5, + KeyEvent.KEYCODE_6, + KeyEvent.KEYCODE_7, + KeyEvent.KEYCODE_8, + KeyEvent.KEYCODE_9, + KeyEvent.KEYCODE_0, + KeyEvent.KEYCODE_MINUS, + KeyEvent.KEYCODE_EQUALS, + KeyEvent.KEYCODE_DEL, + KeyEvent.KEYCODE_TAB, + KeyEvent.KEYCODE_Q, + KeyEvent.KEYCODE_W, + KeyEvent.KEYCODE_E, + KeyEvent.KEYCODE_R, + KeyEvent.KEYCODE_T, + KeyEvent.KEYCODE_Y, + KeyEvent.KEYCODE_U, + KeyEvent.KEYCODE_I, + KeyEvent.KEYCODE_O, + KeyEvent.KEYCODE_P, + KeyEvent.KEYCODE_LEFT_BRACKET, + KeyEvent.KEYCODE_RIGHT_BRACKET, + KeyEvent.KEYCODE_ENTER, + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.KEYCODE_A, + KeyEvent.KEYCODE_S, + KeyEvent.KEYCODE_D, + KeyEvent.KEYCODE_F, + KeyEvent.KEYCODE_G, + KeyEvent.KEYCODE_H, + KeyEvent.KEYCODE_J, + KeyEvent.KEYCODE_K, + KeyEvent.KEYCODE_L, + KeyEvent.KEYCODE_SEMICOLON, + KeyEvent.KEYCODE_APOSTROPHE, + KeyEvent.KEYCODE_GRAVE, + KeyEvent.KEYCODE_SHIFT_LEFT, + KeyEvent.KEYCODE_BACKSLASH, + KeyEvent.KEYCODE_Z, + KeyEvent.KEYCODE_X, + KeyEvent.KEYCODE_C, + KeyEvent.KEYCODE_V, + KeyEvent.KEYCODE_B, + KeyEvent.KEYCODE_N, + KeyEvent.KEYCODE_M, + KeyEvent.KEYCODE_COMMA, + KeyEvent.KEYCODE_PERIOD, + KeyEvent.KEYCODE_SLASH, + KeyEvent.KEYCODE_SHIFT_RIGHT, + KeyEvent.KEYCODE_NUMPAD_MULTIPLY, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_SPACE, + KeyEvent.KEYCODE_CAPS_LOCK, + KeyEvent.KEYCODE_F1, + KeyEvent.KEYCODE_F2, + KeyEvent.KEYCODE_F3, + KeyEvent.KEYCODE_F4, + KeyEvent.KEYCODE_F5, + KeyEvent.KEYCODE_F6, + KeyEvent.KEYCODE_F7, + KeyEvent.KEYCODE_F8, + KeyEvent.KEYCODE_F9, + KeyEvent.KEYCODE_F10, + KeyEvent.KEYCODE_NUM_LOCK, + KeyEvent.KEYCODE_SCROLL_LOCK, + KeyEvent.KEYCODE_NUMPAD_7, + KeyEvent.KEYCODE_NUMPAD_8, + KeyEvent.KEYCODE_NUMPAD_9, + KeyEvent.KEYCODE_NUMPAD_SUBTRACT, + KeyEvent.KEYCODE_NUMPAD_4, + KeyEvent.KEYCODE_NUMPAD_5, + KeyEvent.KEYCODE_NUMPAD_6, + KeyEvent.KEYCODE_NUMPAD_ADD, + KeyEvent.KEYCODE_NUMPAD_1, + KeyEvent.KEYCODE_NUMPAD_2, + KeyEvent.KEYCODE_NUMPAD_3, + KeyEvent.KEYCODE_NUMPAD_0, + KeyEvent.KEYCODE_NUMPAD_DOT, + 0, + 0, //KeyEvent.VK_ZENKAKUHANKAKU, + 0, //KeyEvent.VK_102ND, + KeyEvent.KEYCODE_F11, + KeyEvent.KEYCODE_F12, + 0, //KeyEvent.VK_RO, + 0, //KeyEvent.VK_KATAKANA, + 0, //KeyEvent.VK_HIRAGANA, + 0, //KeyEvent.VK_HENKAN, + 0, //KeyEvent.VK_KATAKANAHIRAGANA, + 0, //KeyEvent.VK_MUHENKAN, + 0, //KeyEvent.VK_KPJPCOMMA, + KeyEvent.KEYCODE_NUMPAD_ENTER, + KeyEvent.KEYCODE_CTRL_RIGHT, + KeyEvent.KEYCODE_NUMPAD_DIVIDE, + KeyEvent.KEYCODE_SYSRQ, + KeyEvent.KEYCODE_ALT_RIGHT, + 0, //KeyEvent.VK_LINEFEED, + KeyEvent.KEYCODE_HOME, + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.KEYCODE_PAGE_UP, + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.KEYCODE_MOVE_END, + KeyEvent.KEYCODE_DPAD_DOWN, + KeyEvent.KEYCODE_PAGE_DOWN, + KeyEvent.KEYCODE_INSERT, + KeyEvent.KEYCODE_FORWARD_DEL, + 0, //KeyEvent.VK_MACRO, + 0, //KeyEvent.VK_MUTE, + 0, //KeyEvent.VK_VOLUMEDOWN, + 0, //KeyEvent.VK_VOLUMEUP, + 0, //KeyEvent.VK_POWER, /* SC System Power Down */ + KeyEvent.KEYCODE_NUMPAD_EQUALS, + 0, //KeyEvent.VK_KPPLUSMINUS, + KeyEvent.KEYCODE_BREAK, + 0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */ + }; + + public static short translateEvdevKeyCode(short evdevKeyCode) { + if (evdevKeyCode < EVDEV_KEY_CODES.length) { + return EVDEV_KEY_CODES[evdevKeyCode]; + } + + return 0; + } } diff --git a/app/src/main/java/com/limelight/binding/input/evdev/EvdevWatcher.java b/app/src/main/java/com/limelight/binding/input/evdev/EvdevWatcher.java index e7665826..731168e0 100644 --- a/app/src/main/java/com/limelight/binding/input/evdev/EvdevWatcher.java +++ b/app/src/main/java/com/limelight/binding/input/evdev/EvdevWatcher.java @@ -10,115 +10,115 @@ import android.os.FileObserver; @SuppressWarnings("ALL") public class EvdevWatcher { - private static final String PATH = "/dev/input"; - private static final String REQUIRED_FILE_PREFIX = "event"; - - private final HashMap handlers = new HashMap(); - private boolean shutdown = false; - private boolean init = false; - private boolean ungrabbed = false; - private EvdevListener listener; - private Thread startThread; + private static final String PATH = "/dev/input"; + private static final String REQUIRED_FILE_PREFIX = "event"; + + private final HashMap handlers = new HashMap(); + private boolean shutdown = false; + private boolean init = false; + private boolean ungrabbed = false; + private EvdevListener listener; + private Thread startThread; private static boolean patchedSeLinuxPolicies = false; - - private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) { - @Override - public void onEvent(int event, String fileName) { - if (fileName == null) { - return; - } - - if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) { - return; - } - - synchronized (handlers) { - if (shutdown) { - return; - } - - if ((event & FileObserver.CREATE) != 0) { - LimeLog.info("Starting evdev handler for "+fileName); - - if (!init) { - // If this a real new device, update permissions again so we can read it - EvdevReader.setPermissions(new String[]{PATH + "/" + fileName}, 0666); - } - - EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener); - - // If we're ungrabbed now, don't start the handler - if (!ungrabbed) { - handler.start(); - } - - handlers.put(fileName, handler); - } - - if ((event & FileObserver.DELETE) != 0) { - LimeLog.info("Halting evdev handler for "+fileName); - - EvdevHandler handler = handlers.remove(fileName); - if (handler != null) { - handler.notifyDeleted(); - } - } - } - } - }; - - public EvdevWatcher(EvdevListener listener) { - this.listener = listener; - } - - private File[] rundownWithPermissionsChange(int newPermissions) { - // Rundown existing files - File devInputDir = new File(PATH); - File[] files = devInputDir.listFiles(); - if (files == null) { - return new File[0]; - } - - // Set desired permissions - String[] filePaths = new String[files.length]; - for (int i = 0; i < files.length; i++) { - filePaths[i] = files[i].getAbsolutePath(); - } - EvdevReader.setPermissions(filePaths, newPermissions); - - return files; - } - - public void ungrabAll() { - synchronized (handlers) { - // Note that we're ungrabbed for now - ungrabbed = true; - - // Stop all handlers - for (EvdevHandler handler : handlers.values()) { - handler.stop(); - } - } - } - - public void regrabAll() { - synchronized (handlers) { - // We're regrabbing everything now - ungrabbed = false; - - for (Map.Entry entry : handlers.entrySet()) { - // We need to recreate each entry since we can't reuse a stopped one - entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener)); - entry.getValue().start(); - } - } - } - - public void start() { - startThread = new Thread() { - @Override - public void run() { + + private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) { + @Override + public void onEvent(int event, String fileName) { + if (fileName == null) { + return; + } + + if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) { + return; + } + + synchronized (handlers) { + if (shutdown) { + return; + } + + if ((event & FileObserver.CREATE) != 0) { + LimeLog.info("Starting evdev handler for "+fileName); + + if (!init) { + // If this a real new device, update permissions again so we can read it + EvdevReader.setPermissions(new String[]{PATH + "/" + fileName}, 0666); + } + + EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener); + + // If we're ungrabbed now, don't start the handler + if (!ungrabbed) { + handler.start(); + } + + handlers.put(fileName, handler); + } + + if ((event & FileObserver.DELETE) != 0) { + LimeLog.info("Halting evdev handler for "+fileName); + + EvdevHandler handler = handlers.remove(fileName); + if (handler != null) { + handler.notifyDeleted(); + } + } + } + } + }; + + public EvdevWatcher(EvdevListener listener) { + this.listener = listener; + } + + private File[] rundownWithPermissionsChange(int newPermissions) { + // Rundown existing files + File devInputDir = new File(PATH); + File[] files = devInputDir.listFiles(); + if (files == null) { + return new File[0]; + } + + // Set desired permissions + String[] filePaths = new String[files.length]; + for (int i = 0; i < files.length; i++) { + filePaths[i] = files[i].getAbsolutePath(); + } + EvdevReader.setPermissions(filePaths, newPermissions); + + return files; + } + + public void ungrabAll() { + synchronized (handlers) { + // Note that we're ungrabbed for now + ungrabbed = true; + + // Stop all handlers + for (EvdevHandler handler : handlers.values()) { + handler.stop(); + } + } + } + + public void regrabAll() { + synchronized (handlers) { + // We're regrabbing everything now + ungrabbed = false; + + for (Map.Entry entry : handlers.entrySet()) { + // We need to recreate each entry since we can't reuse a stopped one + entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener)); + entry.getValue().start(); + } + } + } + + public void start() { + startThread = new Thread() { + @Override + public void run() { // Initialize the root shell EvdevShell.getInstance().startShell(); @@ -128,61 +128,61 @@ public class EvdevWatcher { patchedSeLinuxPolicies = true; } - // List all files and allow us access - File[] files = rundownWithPermissionsChange(0666); - - init = true; - for (File f : files) { - observer.onEvent(FileObserver.CREATE, f.getName()); - } - - // Done with initial onEvent calls - init = false; - - // Start watching for new files - observer.startWatching(); - - synchronized (startThread) { - // Wait to be awoken again by shutdown() - try { - startThread.wait(); - } catch (InterruptedException e) {} - } - - // Giveup eventX permissions - rundownWithPermissionsChange(0660); + // List all files and allow us access + File[] files = rundownWithPermissionsChange(0666); + + init = true; + for (File f : files) { + observer.onEvent(FileObserver.CREATE, f.getName()); + } + + // Done with initial onEvent calls + init = false; + + // Start watching for new files + observer.startWatching(); + + synchronized (startThread) { + // Wait to be awoken again by shutdown() + try { + startThread.wait(); + } catch (InterruptedException e) {} + } + + // Giveup eventX permissions + rundownWithPermissionsChange(0660); // Kill the root shell try { EvdevShell.getInstance().stopShell(); } catch (InterruptedException e) {} - } - }; - startThread.start(); - } - - public void shutdown() { - // Let start thread cleanup on it's own sweet time - synchronized (startThread) { - startThread.notify(); - } - - // Stop the observer - observer.stopWatching(); - - synchronized (handlers) { - // Stop creating new handlers - shutdown = true; - - // If we've already ungrabbed, there's nothing else to do - if (ungrabbed) { - return; - } - - // Stop all handlers - for (EvdevHandler handler : handlers.values()) { - handler.stop(); - } - } - } + } + }; + startThread.start(); + } + + public void shutdown() { + // Let start thread cleanup on it's own sweet time + synchronized (startThread) { + startThread.notify(); + } + + // Stop the observer + observer.stopWatching(); + + synchronized (handlers) { + // Stop creating new handlers + shutdown = true; + + // If we've already ungrabbed, there's nothing else to do + if (ungrabbed) { + return; + } + + // Stop all handlers + for (EvdevHandler handler : handlers.values()) { + handler.stop(); + } + } + } } diff --git a/app/src/main/java/com/limelight/binding/video/AndroidCpuDecoderRenderer.java b/app/src/main/java/com/limelight/binding/video/AndroidCpuDecoderRenderer.java index 60ed4297..7d93ebf5 100644 --- a/app/src/main/java/com/limelight/binding/video/AndroidCpuDecoderRenderer.java +++ b/app/src/main/java/com/limelight/binding/video/AndroidCpuDecoderRenderer.java @@ -20,140 +20,140 @@ import com.limelight.nvstream.av.video.cpu.AvcDecoder; @SuppressWarnings("EmptyCatchBlock") public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer { - private Thread rendererThread, decoderThread; - private int targetFps; - - private static final int DECODER_BUFFER_SIZE = 92*1024; - private ByteBuffer decoderBuffer; - - // Only sleep if the difference is above this value - private static final int WAIT_CEILING_MS = 5; - - private static final int LOW_PERF = 1; - private static final int MED_PERF = 2; - private static final int HIGH_PERF = 3; - - private int totalFrames; - private long totalTimeMs; - - private final int cpuCount = Runtime.getRuntime().availableProcessors(); - - @SuppressWarnings("unused") - private int findOptimalPerformanceLevel() { - StringBuilder cpuInfo = new StringBuilder(); - BufferedReader br = null; - try { - br = new BufferedReader(new FileReader(new File("/proc/cpuinfo"))); - for (;;) { - int ch = br.read(); - if (ch == -1) - break; - cpuInfo.append((char)ch); - } - - // Here we're doing very simple heuristics based on CPU model - String cpuInfoStr = cpuInfo.toString(); + private Thread rendererThread, decoderThread; + private int targetFps; - // We order them from greatest to least for proper detection - // of devices with multiple sets of cores (like Exynos 5 Octa) - // TODO Make this better (only even kind of works on ARM) - if (Build.FINGERPRINT.contains("generic")) { - // Emulator - return LOW_PERF; - } - else if (cpuInfoStr.contains("0xc0f")) { - // Cortex-A15 - return MED_PERF; - } - else if (cpuInfoStr.contains("0xc09")) { - // Cortex-A9 - return LOW_PERF; - } - else if (cpuInfoStr.contains("0xc07")) { - // Cortex-A7 - return LOW_PERF; - } - else { - // Didn't have anything we're looking for - return MED_PERF; - } - } catch (IOException e) { - } finally { - if (br != null) { - try { - br.close(); - } catch (IOException e) {} - } - } - - // Couldn't read cpuinfo, so assume medium - return MED_PERF; - } - - @Override - public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) { - this.targetFps = redrawRate; - - int perfLevel = LOW_PERF; //findOptimalPerformanceLevel(); - int threadCount; - - int avcFlags = 0; - switch (perfLevel) { - case HIGH_PERF: - // Single threaded low latency decode is ideal but hard to acheive - avcFlags = AvcDecoder.LOW_LATENCY_DECODE; - threadCount = 1; - break; + private static final int DECODER_BUFFER_SIZE = 92*1024; + private ByteBuffer decoderBuffer; - case LOW_PERF: - // Disable the loop filter for performance reasons - avcFlags = AvcDecoder.FAST_BILINEAR_FILTERING; - - // Use plenty of threads to try to utilize the CPU as best we can - threadCount = cpuCount - 1; - break; + // Only sleep if the difference is above this value + private static final int WAIT_CEILING_MS = 5; - default: - case MED_PERF: - avcFlags = AvcDecoder.BILINEAR_FILTERING; - - // Only use 2 threads to minimize frame processing latency - threadCount = 2; - break; - } - - // If the user wants quality, we'll remove the low IQ flags - if ((drFlags & VideoDecoderRenderer.FLAG_PREFER_QUALITY) != 0) { - // Make sure the loop filter is enabled - avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER; - - // Disable the non-compliant speed optimizations - avcFlags &= ~AvcDecoder.FAST_DECODE; - - LimeLog.info("Using high quality decoding"); - } - - SurfaceHolder sh = (SurfaceHolder)renderTarget; - sh.setFormat(PixelFormat.RGBX_8888); - - int err = AvcDecoder.init(width, height, avcFlags, threadCount); - if (err != 0) { - throw new IllegalStateException("AVC decoder initialization failure: "+err); - } - - if (!AvcDecoder.setRenderTarget(sh.getSurface())) { + private static final int LOW_PERF = 1; + private static final int MED_PERF = 2; + private static final int HIGH_PERF = 3; + + private int totalFrames; + private long totalTimeMs; + + private final int cpuCount = Runtime.getRuntime().availableProcessors(); + + @SuppressWarnings("unused") + private int findOptimalPerformanceLevel() { + StringBuilder cpuInfo = new StringBuilder(); + BufferedReader br = null; + try { + br = new BufferedReader(new FileReader(new File("/proc/cpuinfo"))); + for (;;) { + int ch = br.read(); + if (ch == -1) + break; + cpuInfo.append((char)ch); + } + + // Here we're doing very simple heuristics based on CPU model + String cpuInfoStr = cpuInfo.toString(); + + // We order them from greatest to least for proper detection + // of devices with multiple sets of cores (like Exynos 5 Octa) + // TODO Make this better (only even kind of works on ARM) + if (Build.FINGERPRINT.contains("generic")) { + // Emulator + return LOW_PERF; + } + else if (cpuInfoStr.contains("0xc0f")) { + // Cortex-A15 + return MED_PERF; + } + else if (cpuInfoStr.contains("0xc09")) { + // Cortex-A9 + return LOW_PERF; + } + else if (cpuInfoStr.contains("0xc07")) { + // Cortex-A7 + return LOW_PERF; + } + else { + // Didn't have anything we're looking for + return MED_PERF; + } + } catch (IOException e) { + } finally { + if (br != null) { + try { + br.close(); + } catch (IOException e) {} + } + } + + // Couldn't read cpuinfo, so assume medium + return MED_PERF; + } + + @Override + public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) { + this.targetFps = redrawRate; + + int perfLevel = LOW_PERF; //findOptimalPerformanceLevel(); + int threadCount; + + int avcFlags = 0; + switch (perfLevel) { + case HIGH_PERF: + // Single threaded low latency decode is ideal but hard to acheive + avcFlags = AvcDecoder.LOW_LATENCY_DECODE; + threadCount = 1; + break; + + case LOW_PERF: + // Disable the loop filter for performance reasons + avcFlags = AvcDecoder.FAST_BILINEAR_FILTERING; + + // Use plenty of threads to try to utilize the CPU as best we can + threadCount = cpuCount - 1; + break; + + default: + case MED_PERF: + avcFlags = AvcDecoder.BILINEAR_FILTERING; + + // Only use 2 threads to minimize frame processing latency + threadCount = 2; + break; + } + + // If the user wants quality, we'll remove the low IQ flags + if ((drFlags & VideoDecoderRenderer.FLAG_PREFER_QUALITY) != 0) { + // Make sure the loop filter is enabled + avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER; + + // Disable the non-compliant speed optimizations + avcFlags &= ~AvcDecoder.FAST_DECODE; + + LimeLog.info("Using high quality decoding"); + } + + SurfaceHolder sh = (SurfaceHolder)renderTarget; + sh.setFormat(PixelFormat.RGBX_8888); + + int err = AvcDecoder.init(width, height, avcFlags, threadCount); + if (err != 0) { + throw new IllegalStateException("AVC decoder initialization failure: "+err); + } + + if (!AvcDecoder.setRenderTarget(sh.getSurface())) { return false; } - - decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize()); - - LimeLog.info("Using software decoding (performance level: "+perfLevel+")"); - - return true; - } - @Override - public boolean start(final VideoDepacketizer depacketizer) { + decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize()); + + LimeLog.info("Using software decoding (performance level: "+perfLevel+")"); + + return true; + } + + @Override + public boolean start(final VideoDepacketizer depacketizer) { decoderThread = new Thread() { @Override public void run() { @@ -174,112 +174,112 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer { decoderThread.setPriority(Thread.MAX_PRIORITY - 1); decoderThread.start(); - rendererThread = new Thread() { - @Override - public void run() { - long nextFrameTime = System.currentTimeMillis(); - DecodeUnit du; - while (!isInterrupted()) - { - long diff = nextFrameTime - System.currentTimeMillis(); + rendererThread = new Thread() { + @Override + public void run() { + long nextFrameTime = System.currentTimeMillis(); + DecodeUnit du; + while (!isInterrupted()) + { + long diff = nextFrameTime - System.currentTimeMillis(); - if (diff > WAIT_CEILING_MS) { + if (diff > WAIT_CEILING_MS) { try { Thread.sleep(diff - WAIT_CEILING_MS); } catch (InterruptedException e) { return; } continue; - } + } - nextFrameTime = computePresentationTimeMs(targetFps); - AvcDecoder.redraw(); - } - } - }; - rendererThread.setName("Video - Renderer (CPU)"); - rendererThread.setPriority(Thread.MAX_PRIORITY); - rendererThread.start(); - return true; - } - - private long computePresentationTimeMs(int frameRate) { - return System.currentTimeMillis() + (1000 / frameRate); - } + nextFrameTime = computePresentationTimeMs(targetFps); + AvcDecoder.redraw(); + } + } + }; + rendererThread.setName("Video - Renderer (CPU)"); + rendererThread.setPriority(Thread.MAX_PRIORITY); + rendererThread.start(); + return true; + } - @Override - public void stop() { - rendererThread.interrupt(); + private long computePresentationTimeMs(int frameRate) { + return System.currentTimeMillis() + (1000 / frameRate); + } + + @Override + public void stop() { + rendererThread.interrupt(); decoderThread.interrupt(); - - try { + + try { rendererThread.join(); } catch (InterruptedException e) { } try { decoderThread.join(); } catch (InterruptedException e) { } - } + } - @Override - public void release() { - AvcDecoder.destroy(); - } + @Override + public void release() { + AvcDecoder.destroy(); + } - private boolean submitDecodeUnit(DecodeUnit decodeUnit) { - byte[] data; - - // Use the reserved decoder buffer if this decode unit will fit - if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) { - decoderBuffer.clear(); - - for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) { - decoderBuffer.put(bbd.data, bbd.offset, bbd.length); - } - - data = decoderBuffer.array(); - } - else { - data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()]; - - int offset = 0; - for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) { - System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length); - offset += bbd.length; - } - } - - boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0); - if (success) { - long timeAfterDecode = System.currentTimeMillis(); - - // Add delta time to the totals (excluding probable outliers) - long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp(); - if (delta >= 0 && delta < 1000) { - totalTimeMs += delta; - totalFrames++; - } - } - - return success; - } + private boolean submitDecodeUnit(DecodeUnit decodeUnit) { + byte[] data; - @Override - public int getCapabilities() { - return 0; - } + // Use the reserved decoder buffer if this decode unit will fit + if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) { + decoderBuffer.clear(); - @Override - public int getAverageDecoderLatency() { - return 0; - } + for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) { + decoderBuffer.put(bbd.data, bbd.offset, bbd.length); + } - @Override - public int getAverageEndToEndLatency() { - if (totalFrames == 0) { - return 0; - } - return (int)(totalTimeMs / totalFrames); - } + data = decoderBuffer.array(); + } + else { + data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()]; + + int offset = 0; + for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) { + System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length); + offset += bbd.length; + } + } + + boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0); + if (success) { + long timeAfterDecode = System.currentTimeMillis(); + + // Add delta time to the totals (excluding probable outliers) + long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp(); + if (delta >= 0 && delta < 1000) { + totalTimeMs += delta; + totalFrames++; + } + } + + return success; + } + + @Override + public int getCapabilities() { + return 0; + } + + @Override + public int getAverageDecoderLatency() { + return 0; + } + + @Override + public int getAverageEndToEndLatency() { + if (totalFrames == 0) { + return 0; + } + return (int)(totalTimeMs / totalFrames); + } @Override public String getDecoderName() { diff --git a/app/src/main/java/com/limelight/binding/video/ConfigurableDecoderRenderer.java b/app/src/main/java/com/limelight/binding/video/ConfigurableDecoderRenderer.java index e0528416..071b2294 100644 --- a/app/src/main/java/com/limelight/binding/video/ConfigurableDecoderRenderer.java +++ b/app/src/main/java/com/limelight/binding/video/ConfigurableDecoderRenderer.java @@ -5,75 +5,75 @@ import com.limelight.nvstream.av.video.VideoDepacketizer; public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer { - private EnhancedDecoderRenderer decoderRenderer; - - @Override - public void release() { - if (decoderRenderer != null) { - decoderRenderer.release(); - } - } + private EnhancedDecoderRenderer decoderRenderer; - @Override - public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) { - if (decoderRenderer == null) { - throw new IllegalStateException("ConfigurableDecoderRenderer not initialized"); - } - return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags); - } - - public void initializeWithFlags(int drFlags) { - if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 || - ((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 && - MediaCodecHelper.findProbableSafeDecoder() != null)) { - decoderRenderer = new MediaCodecDecoderRenderer(); - } - else { - decoderRenderer = new AndroidCpuDecoderRenderer(); - } - } - - public boolean isHardwareAccelerated() { - if (decoderRenderer == null) { - throw new IllegalStateException("ConfigurableDecoderRenderer not initialized"); - } - return (decoderRenderer instanceof MediaCodecDecoderRenderer); - } + @Override + public void release() { + if (decoderRenderer != null) { + decoderRenderer.release(); + } + } - @Override - public boolean start(VideoDepacketizer depacketizer) { - return decoderRenderer.start(depacketizer); - } + @Override + public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) { + if (decoderRenderer == null) { + throw new IllegalStateException("ConfigurableDecoderRenderer not initialized"); + } + return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags); + } - @Override - public void stop() { - decoderRenderer.stop(); - } + public void initializeWithFlags(int drFlags) { + if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 || + ((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 && + MediaCodecHelper.findProbableSafeDecoder() != null)) { + decoderRenderer = new MediaCodecDecoderRenderer(); + } + else { + decoderRenderer = new AndroidCpuDecoderRenderer(); + } + } - @Override - public int getCapabilities() { - return decoderRenderer.getCapabilities(); - } + public boolean isHardwareAccelerated() { + if (decoderRenderer == null) { + throw new IllegalStateException("ConfigurableDecoderRenderer not initialized"); + } + return (decoderRenderer instanceof MediaCodecDecoderRenderer); + } - @Override - public int getAverageDecoderLatency() { - if (decoderRenderer != null) { - return decoderRenderer.getAverageDecoderLatency(); - } - else { - return 0; - } - } + @Override + public boolean start(VideoDepacketizer depacketizer) { + return decoderRenderer.start(depacketizer); + } - @Override - public int getAverageEndToEndLatency() { - if (decoderRenderer != null) { - return decoderRenderer.getAverageEndToEndLatency(); - } - else { - return 0; - } - } + @Override + public void stop() { + decoderRenderer.stop(); + } + + @Override + public int getCapabilities() { + return decoderRenderer.getCapabilities(); + } + + @Override + public int getAverageDecoderLatency() { + if (decoderRenderer != null) { + return decoderRenderer.getAverageDecoderLatency(); + } + else { + return 0; + } + } + + @Override + public int getAverageEndToEndLatency() { + if (decoderRenderer != null) { + return decoderRenderer.getAverageEndToEndLatency(); + } + else { + return 0; + } + } @Override public String getDecoderName() { diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java index 5b73ed8a..5a24de9b 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java @@ -25,391 +25,391 @@ import android.view.SurfaceHolder; @SuppressWarnings("unused") public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { - private ByteBuffer[] videoDecoderInputBuffers; - private MediaCodec videoDecoder; - private Thread rendererThread; - private boolean needsSpsBitstreamFixup, isExynos4; - private VideoDepacketizer depacketizer; - private boolean adaptivePlayback; - private int initialWidth, initialHeight; + private ByteBuffer[] videoDecoderInputBuffers; + private MediaCodec videoDecoder; + private Thread rendererThread; + private boolean needsSpsBitstreamFixup, isExynos4; + private VideoDepacketizer depacketizer; + private boolean adaptivePlayback; + private int initialWidth, initialHeight; private boolean needsBaselineSpsHack; private SeqParameterSet savedSps; - - private long lastTimestampUs; - private long totalTimeMs; - private long decoderTimeMs; - private int totalFrames; - - private String decoderName; - private int numSpsIn; - private int numPpsIn; - private int numIframeIn; - - private static final boolean ENABLE_ASYNC_RENDERER = false; - - @TargetApi(Build.VERSION_CODES.KITKAT) - public MediaCodecDecoderRenderer() { - //dumpDecoders(); - - MediaCodecInfo decoder = MediaCodecHelper.findProbableSafeDecoder(); - if (decoder == null) { - decoder = MediaCodecHelper.findFirstDecoder(); - } - if (decoder == null) { - // This case is handled later in setup() - return; - } - - decoderName = decoder.getName(); - - // Set decoder-specific attributes - adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder); - needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder); + + private long lastTimestampUs; + private long totalTimeMs; + private long decoderTimeMs; + private int totalFrames; + + private String decoderName; + private int numSpsIn; + private int numPpsIn; + private int numIframeIn; + + private static final boolean ENABLE_ASYNC_RENDERER = false; + + @TargetApi(Build.VERSION_CODES.KITKAT) + public MediaCodecDecoderRenderer() { + //dumpDecoders(); + + MediaCodecInfo decoder = MediaCodecHelper.findProbableSafeDecoder(); + if (decoder == null) { + decoder = MediaCodecHelper.findFirstDecoder(); + } + if (decoder == null) { + // This case is handled later in setup() + return; + } + + decoderName = decoder.getName(); + + // Set decoder-specific attributes + adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder); + needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder); needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder); isExynos4 = MediaCodecHelper.isExynos4Device(); if (needsSpsBitstreamFixup) { - LimeLog.info("Decoder "+decoderName+" needs SPS bitstream restrictions fixup"); - } + LimeLog.info("Decoder "+decoderName+" needs SPS bitstream restrictions fixup"); + } if (needsBaselineSpsHack) { LimeLog.info("Decoder "+decoderName+" needs baseline SPS hack"); } - if (isExynos4) { - LimeLog.info("Decoder "+decoderName+" is on Exynos 4"); - } - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) { - this.initialWidth = width; - this.initialHeight = height; - - if (decoderName == null) { - LimeLog.severe("No available hardware decoder!"); - return false; - } - - // Codecs have been known to throw all sorts of crazy runtime exceptions - // due to implementation problems - try { - videoDecoder = MediaCodec.createByCodecName(decoderName); - } catch (Exception e) { - return false; - } - - MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height); - - // Adaptive playback can also be enabled by the whitelist on pre-KitKat devices - // so we don't fill these pre-KitKat - if (adaptivePlayback && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - videoFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, width); - videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height); - } - - // On Lollipop, we use asynchronous mode to avoid having a busy looping renderer thread - if (ENABLE_ASYNC_RENDERER && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - videoDecoder.setCallback(new MediaCodec.Callback() { - @Override - public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { - LimeLog.info("Output format changed"); - LimeLog.info("New output Format: " + format); - } - - @Override - public void onOutputBufferAvailable(MediaCodec codec, int index, - BufferInfo info) { - try { - // FIXME: It looks like we can't frameskip here - codec.releaseOutputBuffer(index, true); - } catch (Exception e) { - handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); - } - } - - @Override - public void onInputBufferAvailable(MediaCodec codec, int index) { - try { - submitDecodeUnit(depacketizer.takeNextDecodeUnit(), codec.getInputBuffer(index), index); - } catch (InterruptedException e) { - // What do we do here? - e.printStackTrace(); - } catch (Exception e) { - handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); - } - } - - @Override - public void onError(MediaCodec codec, CodecException e) { - if (e.isTransient()) { - LimeLog.warning(e.getDiagnosticInfo()); - e.printStackTrace(); - } - else { - LimeLog.severe(e.getDiagnosticInfo()); - e.printStackTrace(); - } - } - }); - } - - videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0); - videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT); - - LimeLog.info("Using hardware decoding"); - - return true; - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private void handleDecoderException(MediaCodecDecoderRenderer dr, Exception e, ByteBuffer buf, int codecFlags) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - if (e instanceof CodecException) { - CodecException codecExc = (CodecException) e; - - if (codecExc.isTransient()) { - // We'll let transient exceptions go - LimeLog.warning(codecExc.getDiagnosticInfo()); - return; - } - - LimeLog.severe(codecExc.getDiagnosticInfo()); - } - } - - if (buf != null || codecFlags != 0) { - throw new RendererException(dr, e, buf, codecFlags); - } - else { - throw new RendererException(dr, e); - } - } - - private void startRendererThread() - { - rendererThread = new Thread() { - @SuppressWarnings("deprecation") - @Override - public void run() { - BufferInfo info = new BufferInfo(); - DecodeUnit du = null; - int inputIndex = -1; - while (!isInterrupted()) - { - // In order to get as much data to the decoder as early as possible, - // try to submit up to 5 decode units at once without blocking. - if (inputIndex == -1 && du == null) { - try { - for (int i = 0; i < 5; i++) { - inputIndex = videoDecoder.dequeueInputBuffer(0); - du = depacketizer.pollNextDecodeUnit(); + if (isExynos4) { + LimeLog.info("Decoder "+decoderName+" is on Exynos 4"); + } + } - // Stop if we can't get a DU or input buffer - if (du == null || inputIndex == -1) { - break; - } - - submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex); - - du = null; - inputIndex = -1; - } - } catch (Exception e) { - inputIndex = -1; - handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); - } - } - - // Grab an input buffer if we don't have one already. - // This way we can have one ready hopefully by the time - // the depacketizer is done with this frame. It's important - // that this can timeout because it's possible that we could exhaust - // the decoder's input buffers and deadlocks because aren't pulling - // frames out of the other end. - if (inputIndex == -1) { - try { - // If we've got a DU waiting to be given to the decoder, - // wait a full 3 ms for an input buffer. Otherwise - // just see if we can get one immediately. - inputIndex = videoDecoder.dequeueInputBuffer(du != null ? 3000 : 0); - } catch (Exception e) { - inputIndex = -1; - handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); - } - } - - // Grab a decode unit if we don't have one already - if (du == null) { - du = depacketizer.pollNextDecodeUnit(); - } - - // If we've got both a decode unit and an input buffer, we'll - // submit now. Otherwise, we wait until we have one. - if (du != null && inputIndex >= 0) { - submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex); - - // DU and input buffer have both been consumed - du = null; - inputIndex = -1; - } - - // Try to output a frame - try { - int outIndex = videoDecoder.dequeueOutputBuffer(info, 0); + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) { + this.initialWidth = width; + this.initialHeight = height; - if (outIndex >= 0) { - long presentationTimeUs = info.presentationTimeUs; - int lastIndex = outIndex; - - // Get the last output buffer in the queue - while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) { - videoDecoder.releaseOutputBuffer(lastIndex, false); - lastIndex = outIndex; - presentationTimeUs = info.presentationTimeUs; - } - - // Render the last buffer - videoDecoder.releaseOutputBuffer(lastIndex, true); - - // Add delta time to the totals (excluding probable outliers) - long delta = System.currentTimeMillis()-(presentationTimeUs/1000); - if (delta >= 0 && delta < 1000) { - decoderTimeMs += delta; - totalTimeMs += delta; - } - } else { - switch (outIndex) { - case MediaCodec.INFO_TRY_AGAIN_LATER: - // Getting an input buffer may already block - // so don't park if we still need to do that - if (inputIndex >= 0) { - LockSupport.parkNanos(1); - } - break; - case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED: - LimeLog.info("Output buffers changed"); - break; - case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: - LimeLog.info("Output format changed"); - LimeLog.info("New output Format: " + videoDecoder.getOutputFormat()); - break; - default: - break; - } - } - } catch (Exception e) { - handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); - } - } - } - }; - rendererThread.setName("Video - Renderer (MediaCodec)"); - rendererThread.setPriority(Thread.MAX_PRIORITY); - rendererThread.start(); - } + if (decoderName == null) { + LimeLog.severe("No available hardware decoder!"); + return false; + } - @SuppressWarnings("deprecation") - @Override - public boolean start(VideoDepacketizer depacketizer) { - this.depacketizer = depacketizer; - - // Start the decoder - videoDecoder.start(); - - // On devices pre-Lollipop, we'll use a rendering thread - if (!ENABLE_ASYNC_RENDERER || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - videoDecoderInputBuffers = videoDecoder.getInputBuffers(); - startRendererThread(); - } - return true; - } + // Codecs have been known to throw all sorts of crazy runtime exceptions + // due to implementation problems + try { + videoDecoder = MediaCodec.createByCodecName(decoderName); + } catch (Exception e) { + return false; + } - @Override - public void stop() { - if (rendererThread != null) { - // Halt the rendering thread - rendererThread.interrupt(); - try { - rendererThread.join(); - } catch (InterruptedException ignored) { } - } - - // Stop the decoder - videoDecoder.stop(); - } + MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height); - @Override - public void release() { - if (videoDecoder != null) { - videoDecoder.release(); - } - } - - private void queueInputBuffer(int inputBufferIndex, int offset, int length, long timestampUs, int codecFlags) { - // Try 25 times to submit the input buffer before throwing a real exception - int i; - Exception lastException = null; - - for (i = 0; i < 25; i++) { - try { - videoDecoder.queueInputBuffer(inputBufferIndex, - 0, length, - timestampUs, codecFlags); - break; - } catch (Exception e) { - handleDecoderException(this, e, null, codecFlags); - lastException = e; - } - } - - if (i == 25) { - throw new RendererException(this, lastException, null, codecFlags); - } - } + // Adaptive playback can also be enabled by the whitelist on pre-KitKat devices + // so we don't fill these pre-KitKat + if (adaptivePlayback && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + videoFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, width); + videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height); + } - @SuppressWarnings("deprecation") - private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) { - long currentTime = System.currentTimeMillis(); - long delta = currentTime-decodeUnit.getReceiveTimestamp(); - if (delta >= 0 && delta < 1000) { - totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp(); - totalFrames++; - } - - long timestampUs = currentTime * 1000; - if (timestampUs <= lastTimestampUs) { - // We can't submit multiple buffers with the same timestamp - // so bump it up by one before queuing - timestampUs = lastTimestampUs + 1; - } - lastTimestampUs = timestampUs; - - // Clear old input data - buf.clear(); - - int codecFlags = 0; - int decodeUnitFlags = decodeUnit.getFlags(); - if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) { - codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG; - } - if ((decodeUnitFlags & DecodeUnit.DU_FLAG_SYNC_FRAME) != 0) { - codecFlags |= MediaCodec.BUFFER_FLAG_SYNC_FRAME; - numIframeIn++; - } + // On Lollipop, we use asynchronous mode to avoid having a busy looping renderer thread + if (ENABLE_ASYNC_RENDERER && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + videoDecoder.setCallback(new MediaCodec.Callback() { + @Override + public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { + LimeLog.info("Output format changed"); + LimeLog.info("New output Format: " + format); + } + + @Override + public void onOutputBufferAvailable(MediaCodec codec, int index, + BufferInfo info) { + try { + // FIXME: It looks like we can't frameskip here + codec.releaseOutputBuffer(index, true); + } catch (Exception e) { + handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); + } + } + + @Override + public void onInputBufferAvailable(MediaCodec codec, int index) { + try { + submitDecodeUnit(depacketizer.takeNextDecodeUnit(), codec.getInputBuffer(index), index); + } catch (InterruptedException e) { + // What do we do here? + e.printStackTrace(); + } catch (Exception e) { + handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); + } + } + + @Override + public void onError(MediaCodec codec, CodecException e) { + if (e.isTransient()) { + LimeLog.warning(e.getDiagnosticInfo()); + e.printStackTrace(); + } + else { + LimeLog.severe(e.getDiagnosticInfo()); + e.printStackTrace(); + } + } + }); + } + + videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0); + videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT); + + LimeLog.info("Using hardware decoding"); + + return true; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void handleDecoderException(MediaCodecDecoderRenderer dr, Exception e, ByteBuffer buf, int codecFlags) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (e instanceof CodecException) { + CodecException codecExc = (CodecException) e; + + if (codecExc.isTransient()) { + // We'll let transient exceptions go + LimeLog.warning(codecExc.getDiagnosticInfo()); + return; + } + + LimeLog.severe(codecExc.getDiagnosticInfo()); + } + } + + if (buf != null || codecFlags != 0) { + throw new RendererException(dr, e, buf, codecFlags); + } + else { + throw new RendererException(dr, e); + } + } + + private void startRendererThread() + { + rendererThread = new Thread() { + @SuppressWarnings("deprecation") + @Override + public void run() { + BufferInfo info = new BufferInfo(); + DecodeUnit du = null; + int inputIndex = -1; + while (!isInterrupted()) + { + // In order to get as much data to the decoder as early as possible, + // try to submit up to 5 decode units at once without blocking. + if (inputIndex == -1 && du == null) { + try { + for (int i = 0; i < 5; i++) { + inputIndex = videoDecoder.dequeueInputBuffer(0); + du = depacketizer.pollNextDecodeUnit(); + + // Stop if we can't get a DU or input buffer + if (du == null || inputIndex == -1) { + break; + } + + submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex); + + du = null; + inputIndex = -1; + } + } catch (Exception e) { + inputIndex = -1; + handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); + } + } + + // Grab an input buffer if we don't have one already. + // This way we can have one ready hopefully by the time + // the depacketizer is done with this frame. It's important + // that this can timeout because it's possible that we could exhaust + // the decoder's input buffers and deadlocks because aren't pulling + // frames out of the other end. + if (inputIndex == -1) { + try { + // If we've got a DU waiting to be given to the decoder, + // wait a full 3 ms for an input buffer. Otherwise + // just see if we can get one immediately. + inputIndex = videoDecoder.dequeueInputBuffer(du != null ? 3000 : 0); + } catch (Exception e) { + inputIndex = -1; + handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); + } + } + + // Grab a decode unit if we don't have one already + if (du == null) { + du = depacketizer.pollNextDecodeUnit(); + } + + // If we've got both a decode unit and an input buffer, we'll + // submit now. Otherwise, we wait until we have one. + if (du != null && inputIndex >= 0) { + submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex); + + // DU and input buffer have both been consumed + du = null; + inputIndex = -1; + } + + // Try to output a frame + try { + int outIndex = videoDecoder.dequeueOutputBuffer(info, 0); + + if (outIndex >= 0) { + long presentationTimeUs = info.presentationTimeUs; + int lastIndex = outIndex; + + // Get the last output buffer in the queue + while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) { + videoDecoder.releaseOutputBuffer(lastIndex, false); + lastIndex = outIndex; + presentationTimeUs = info.presentationTimeUs; + } + + // Render the last buffer + videoDecoder.releaseOutputBuffer(lastIndex, true); + + // Add delta time to the totals (excluding probable outliers) + long delta = System.currentTimeMillis()-(presentationTimeUs/1000); + if (delta >= 0 && delta < 1000) { + decoderTimeMs += delta; + totalTimeMs += delta; + } + } else { + switch (outIndex) { + case MediaCodec.INFO_TRY_AGAIN_LATER: + // Getting an input buffer may already block + // so don't park if we still need to do that + if (inputIndex >= 0) { + LockSupport.parkNanos(1); + } + break; + case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED: + LimeLog.info("Output buffers changed"); + break; + case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: + LimeLog.info("Output format changed"); + LimeLog.info("New output Format: " + videoDecoder.getOutputFormat()); + break; + default: + break; + } + } + } catch (Exception e) { + handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); + } + } + } + }; + rendererThread.setName("Video - Renderer (MediaCodec)"); + rendererThread.setPriority(Thread.MAX_PRIORITY); + rendererThread.start(); + } + + @SuppressWarnings("deprecation") + @Override + public boolean start(VideoDepacketizer depacketizer) { + this.depacketizer = depacketizer; + + // Start the decoder + videoDecoder.start(); + + // On devices pre-Lollipop, we'll use a rendering thread + if (!ENABLE_ASYNC_RENDERER || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + videoDecoderInputBuffers = videoDecoder.getInputBuffers(); + startRendererThread(); + } + return true; + } + + @Override + public void stop() { + if (rendererThread != null) { + // Halt the rendering thread + rendererThread.interrupt(); + try { + rendererThread.join(); + } catch (InterruptedException ignored) { } + } + + // Stop the decoder + videoDecoder.stop(); + } + + @Override + public void release() { + if (videoDecoder != null) { + videoDecoder.release(); + } + } + + private void queueInputBuffer(int inputBufferIndex, int offset, int length, long timestampUs, int codecFlags) { + // Try 25 times to submit the input buffer before throwing a real exception + int i; + Exception lastException = null; + + for (i = 0; i < 25; i++) { + try { + videoDecoder.queueInputBuffer(inputBufferIndex, + 0, length, + timestampUs, codecFlags); + break; + } catch (Exception e) { + handleDecoderException(this, e, null, codecFlags); + lastException = e; + } + } + + if (i == 25) { + throw new RendererException(this, lastException, null, codecFlags); + } + } + + @SuppressWarnings("deprecation") + private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) { + long currentTime = System.currentTimeMillis(); + long delta = currentTime-decodeUnit.getReceiveTimestamp(); + if (delta >= 0 && delta < 1000) { + totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp(); + totalFrames++; + } + + long timestampUs = currentTime * 1000; + if (timestampUs <= lastTimestampUs) { + // We can't submit multiple buffers with the same timestamp + // so bump it up by one before queuing + timestampUs = lastTimestampUs + 1; + } + lastTimestampUs = timestampUs; + + // Clear old input data + buf.clear(); + + int codecFlags = 0; + int decodeUnitFlags = decodeUnit.getFlags(); + if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) { + codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG; + } + if ((decodeUnitFlags & DecodeUnit.DU_FLAG_SYNC_FRAME) != 0) { + codecFlags |= MediaCodec.BUFFER_FLAG_SYNC_FRAME; + numIframeIn++; + } boolean needsSpsReplay = false; - - if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) { - ByteBufferDescriptor header = decodeUnit.getBufferList().get(0); - if (header.data[header.offset+4] == 0x67) { - numSpsIn++; - - ByteBuffer spsBuf = ByteBuffer.wrap(header.data); - - // Skip to the start of the NALU data - spsBuf.position(header.offset+5); - - SeqParameterSet sps = SeqParameterSet.read(spsBuf); + + if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) { + ByteBufferDescriptor header = decodeUnit.getBufferList().get(0); + if (header.data[header.offset+4] == 0x67) { + numSpsIn++; + + ByteBuffer spsBuf = ByteBuffer.wrap(header.data); + + // Skip to the start of the NALU data + spsBuf.position(header.offset+5); + + SeqParameterSet sps = SeqParameterSet.read(spsBuf); // Some decoders rely on H264 level to decide how many buffers are needed // Since we only need one frame buffered, we'll set the level as low as we can @@ -427,30 +427,30 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { else { // Leave the profile alone (currently 5.0) } - - // TI OMAP4 requires a reference frame count of 1 to decode successfully. Exynos 4 - // also requires this fixup. - // - // I'm doing this fixup for all devices because I haven't seen any devices that - // this causes issues for. At worst, it seems to do nothing and at best it fixes - // issues with video lag, hangs, and crashes. - LimeLog.info("Patching num_ref_frames in SPS"); - sps.num_ref_frames = 1; - - if (needsSpsBitstreamFixup || isExynos4) { - // The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag - // or max_dec_frame_buffering which increases decoding latency on Tegra. - LimeLog.info("Adding bitstream restrictions"); - sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction(); - sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true; - sps.vuiParams.bitstreamRestriction.max_bytes_per_pic_denom = 2; - sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1; - sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16; - sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16; - sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0; - sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = 1; - } + // TI OMAP4 requires a reference frame count of 1 to decode successfully. Exynos 4 + // also requires this fixup. + // + // I'm doing this fixup for all devices because I haven't seen any devices that + // this causes issues for. At worst, it seems to do nothing and at best it fixes + // issues with video lag, hangs, and crashes. + LimeLog.info("Patching num_ref_frames in SPS"); + sps.num_ref_frames = 1; + + if (needsSpsBitstreamFixup || isExynos4) { + // The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag + // or max_dec_frame_buffering which increases decoding latency on Tegra. + LimeLog.info("Adding bitstream restrictions"); + + sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction(); + sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true; + sps.vuiParams.bitstreamRestriction.max_bytes_per_pic_denom = 2; + sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1; + sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16; + sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16; + sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0; + sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = 1; + } // If we need to hack this SPS to say we're baseline, do so now if (needsBaselineSpsHack) { @@ -458,21 +458,21 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { sps.profile_idc = 66; savedSps = sps; } - - // Write the annex B header - buf.put(header.data, header.offset, 5); - - // Write the modified SPS to the input buffer - sps.write(buf); - - queueInputBuffer(inputBufferIndex, - 0, buf.position(), - timestampUs, codecFlags); - - depacketizer.freeDecodeUnit(decodeUnit); - return; - } else if (header.data[header.offset+4] == 0x68) { - numPpsIn++; + + // Write the annex B header + buf.put(header.data, header.offset, 5); + + // Write the modified SPS to the input buffer + sps.write(buf); + + queueInputBuffer(inputBufferIndex, + 0, buf.position(), + timestampUs, codecFlags); + + depacketizer.freeDecodeUnit(decodeUnit); + return; + } else if (header.data[header.offset+4] == 0x68) { + numPpsIn++; if (needsBaselineSpsHack) { LimeLog.info("Saw PPS; disabling SPS hack"); @@ -481,25 +481,25 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { // Give the decoder the SPS again with the proper profile now needsSpsReplay = true; } - } - } + } + } - // Copy data from our buffer list into the input buffer - for (ByteBufferDescriptor desc : decodeUnit.getBufferList()) - { - buf.put(desc.data, desc.offset, desc.length); - } + // Copy data from our buffer list into the input buffer + for (ByteBufferDescriptor desc : decodeUnit.getBufferList()) + { + buf.put(desc.data, desc.offset, desc.length); + } - queueInputBuffer(inputBufferIndex, - 0, decodeUnit.getDataLength(), - timestampUs, codecFlags); - - depacketizer.freeDecodeUnit(decodeUnit); + queueInputBuffer(inputBufferIndex, + 0, decodeUnit.getDataLength(), + timestampUs, codecFlags); + + depacketizer.freeDecodeUnit(decodeUnit); if (needsSpsReplay) { replaySps(); } - } + } private void replaySps() { int inputIndex = videoDecoder.dequeueInputBuffer(-1); @@ -528,27 +528,27 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { LimeLog.info("SPS replay complete"); } - @Override - public int getCapabilities() { - return adaptivePlayback ? - VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0; - } + @Override + public int getCapabilities() { + return adaptivePlayback ? + VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0; + } - @Override - public int getAverageDecoderLatency() { - if (totalFrames == 0) { - return 0; - } - return (int)(decoderTimeMs / totalFrames); - } + @Override + public int getAverageDecoderLatency() { + if (totalFrames == 0) { + return 0; + } + return (int)(decoderTimeMs / totalFrames); + } - @Override - public int getAverageEndToEndLatency() { - if (totalFrames == 0) { - return 0; - } - return (int)(totalTimeMs / totalFrames); - } + @Override + public int getAverageEndToEndLatency() { + if (totalFrames == 0) { + return 0; + } + return (int)(totalTimeMs / totalFrames); + } @Override public String getDecoderName() { @@ -556,62 +556,62 @@ 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 ByteBuffer currentBuffer; - private int currentCodecFlags; - - public RendererException(MediaCodecDecoderRenderer renderer, Exception e) { - this.renderer = renderer; - this.originalException = e; - } - - public RendererException(MediaCodecDecoderRenderer renderer, Exception e, ByteBuffer currentBuffer, int currentCodecFlags) { - this.renderer = renderer; - this.originalException = e; - this.currentBuffer = currentBuffer; - this.currentCodecFlags = currentCodecFlags; - } - - public String toString() { - String str = ""; - - str += "Decoder: "+renderer.decoderName+"\n"; - str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n"; - str += "In stats: "+renderer.numSpsIn+", "+renderer.numPpsIn+", "+renderer.numIframeIn+"\n"; - str += "Total frames: "+renderer.totalFrames+"\n"; - - if (currentBuffer != null) { - str += "Current buffer: "; - currentBuffer.flip(); - while (currentBuffer.hasRemaining() && currentBuffer.position() < 10) { - str += String.format((Locale)null, "%02x ", currentBuffer.get()); - } - str += "\n"; - str += "Buffer codec flags: "+currentCodecFlags+"\n"; - } - - str += "Is Exynos 4: "+renderer.isExynos4+"\n"; - - str += "/proc/cpuinfo:\n"; - try { - str += MediaCodecHelper.readCpuinfo(); - } catch (Exception e) { - str += e.getMessage(); - } - - str += "Full decoder dump:\n"; - try { - str += MediaCodecHelper.dumpDecoders(); - } catch (Exception e) { - str += e.getMessage(); - } - - str += originalException.toString(); - - return str; - } - } + private static final long serialVersionUID = 8985937536997012406L; + + private final Exception originalException; + private final MediaCodecDecoderRenderer renderer; + private ByteBuffer currentBuffer; + private int currentCodecFlags; + + public RendererException(MediaCodecDecoderRenderer renderer, Exception e) { + this.renderer = renderer; + this.originalException = e; + } + + public RendererException(MediaCodecDecoderRenderer renderer, Exception e, ByteBuffer currentBuffer, int currentCodecFlags) { + this.renderer = renderer; + this.originalException = e; + this.currentBuffer = currentBuffer; + this.currentCodecFlags = currentCodecFlags; + } + + public String toString() { + String str = ""; + + str += "Decoder: "+renderer.decoderName+"\n"; + str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n"; + str += "In stats: "+renderer.numSpsIn+", "+renderer.numPpsIn+", "+renderer.numIframeIn+"\n"; + str += "Total frames: "+renderer.totalFrames+"\n"; + + if (currentBuffer != null) { + str += "Current buffer: "; + currentBuffer.flip(); + while (currentBuffer.hasRemaining() && currentBuffer.position() < 10) { + str += String.format((Locale)null, "%02x ", currentBuffer.get()); + } + str += "\n"; + str += "Buffer codec flags: "+currentCodecFlags+"\n"; + } + + str += "Is Exynos 4: "+renderer.isExynos4+"\n"; + + str += "/proc/cpuinfo:\n"; + try { + str += MediaCodecHelper.readCpuinfo(); + } catch (Exception e) { + str += e.getMessage(); + } + + str += "Full decoder dump:\n"; + try { + str += MediaCodecHelper.dumpDecoders(); + } catch (Exception e) { + str += e.getMessage(); + } + + str += originalException.toString(); + + return str; + } + } } diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java index 76f96104..d3befe7d 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java @@ -72,8 +72,8 @@ public class MediaCodecHelper { @TargetApi(Build.VERSION_CODES.KITKAT) public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) { /* - FIXME: Intel's decoder on Nexus Player forces the high latency path if adaptive playback is enabled - so we'll keep it off for now, since we don't know whether other devices also do the same + FIXME: Intel's decoder on Nexus Player forces the high latency path if adaptive playback is enabled + so we'll keep it off for now, since we don't know whether other devices also do the same if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) { LimeLog.info("Adaptive playback supported (whitelist)"); diff --git a/app/src/main/java/com/limelight/computers/ComputerDatabaseManager.java b/app/src/main/java/com/limelight/computers/ComputerDatabaseManager.java index 701b1222..c5c5585d 100644 --- a/app/src/main/java/com/limelight/computers/ComputerDatabaseManager.java +++ b/app/src/main/java/com/limelight/computers/ComputerDatabaseManager.java @@ -17,153 +17,153 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; public class ComputerDatabaseManager { - private static final String COMPUTER_DB_NAME = "computers.db"; - private static final String COMPUTER_TABLE_NAME = "Computers"; - private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName"; - private static final String COMPUTER_UUID_COLUMN_NAME = "UUID"; - private static final String LOCAL_IP_COLUMN_NAME = "LocalIp"; - private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp"; - private static final String MAC_COLUMN_NAME = "Mac"; - - private SQLiteDatabase computerDb; - - public ComputerDatabaseManager(Context c) { - try { - // Create or open an existing DB - computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null); - } catch (SQLiteException e) { - // Delete the DB and try again - c.deleteDatabase(COMPUTER_DB_NAME); - computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null); - } - initializeDb(); - } - - public void close() { - computerDb.close(); - } - - private void initializeDb() { - // Create tables if they aren't already there - computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," + - " %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)", - COMPUTER_TABLE_NAME, - COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME, - REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME)); - } - - public void deleteComputer(String name) { - computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null); - } - - public boolean updateComputer(ComputerDetails details) { - ContentValues values = new ContentValues(); - values.put(COMPUTER_NAME_COLUMN_NAME, details.name); - values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString()); - values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress()); - values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress()); - values.put(MAC_COLUMN_NAME, details.macAddress); - return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE); - } - - public List getAllComputers() { - Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null); - LinkedList computerList = new LinkedList(); - while (c.moveToNext()) { - ComputerDetails details = new ComputerDetails(); - - details.name = c.getString(0); - - String uuidStr = c.getString(1); - try { - details.uuid = UUID.fromString(uuidStr); - } catch (IllegalArgumentException e) { - // We'll delete this entry - LimeLog.severe("DB: Corrupted UUID for "+details.name); - } - - try { - details.localIp = InetAddress.getByAddress(c.getBlob(2)); - } catch (UnknownHostException e) { - // We'll delete this entry - LimeLog.severe("DB: Corrupted local IP for "+details.name); - } - - try { - details.remoteIp = InetAddress.getByAddress(c.getBlob(3)); - } catch (UnknownHostException e) { - // We'll delete this entry - LimeLog.severe("DB: Corrupted remote IP for "+details.name); - } - - details.macAddress = c.getString(4); - - // This signifies we don't have dynamic state (like pair state) + private static final String COMPUTER_DB_NAME = "computers.db"; + private static final String COMPUTER_TABLE_NAME = "Computers"; + private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName"; + private static final String COMPUTER_UUID_COLUMN_NAME = "UUID"; + private static final String LOCAL_IP_COLUMN_NAME = "LocalIp"; + private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp"; + private static final String MAC_COLUMN_NAME = "Mac"; + + private SQLiteDatabase computerDb; + + public ComputerDatabaseManager(Context c) { + try { + // Create or open an existing DB + computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null); + } catch (SQLiteException e) { + // Delete the DB and try again + c.deleteDatabase(COMPUTER_DB_NAME); + computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null); + } + initializeDb(); + } + + public void close() { + computerDb.close(); + } + + private void initializeDb() { + // Create tables if they aren't already there + computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," + + " %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)", + COMPUTER_TABLE_NAME, + COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME, + REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME)); + } + + public void deleteComputer(String name) { + computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null); + } + + public boolean updateComputer(ComputerDetails details) { + ContentValues values = new ContentValues(); + values.put(COMPUTER_NAME_COLUMN_NAME, details.name); + values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString()); + values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress()); + values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress()); + values.put(MAC_COLUMN_NAME, details.macAddress); + return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE); + } + + public List getAllComputers() { + Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null); + LinkedList computerList = new LinkedList(); + while (c.moveToNext()) { + ComputerDetails details = new ComputerDetails(); + + details.name = c.getString(0); + + String uuidStr = c.getString(1); + try { + details.uuid = UUID.fromString(uuidStr); + } catch (IllegalArgumentException e) { + // We'll delete this entry + LimeLog.severe("DB: Corrupted UUID for "+details.name); + } + + try { + details.localIp = InetAddress.getByAddress(c.getBlob(2)); + } catch (UnknownHostException e) { + // We'll delete this entry + LimeLog.severe("DB: Corrupted local IP for "+details.name); + } + + try { + details.remoteIp = InetAddress.getByAddress(c.getBlob(3)); + } catch (UnknownHostException e) { + // We'll delete this entry + LimeLog.severe("DB: Corrupted remote IP for "+details.name); + } + + details.macAddress = c.getString(4); + + // This signifies we don't have dynamic state (like pair state) details.state = ComputerDetails.State.UNKNOWN; details.reachability = ComputerDetails.Reachability.UNKNOWN; - - // If a field is corrupt or missing, skip the database entry - if (details.uuid == null || details.localIp == null || details.remoteIp == null || - details.macAddress == null) { - continue; - } + + // If a field is corrupt or missing, skip the database entry + if (details.uuid == null || details.localIp == null || details.remoteIp == null || + details.macAddress == null) { + continue; + } computerList.add(details); - } - - c.close(); - - return computerList; - } - - public ComputerDetails getComputerByName(String name) { - Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null); - ComputerDetails details = new ComputerDetails(); - if (!c.moveToFirst()) { - // No matching computer - c.close(); - return null; - } + } - details.name = c.getString(0); - - String uuidStr = c.getString(1); - try { - details.uuid = UUID.fromString(uuidStr); - } catch (IllegalArgumentException e) { - // We'll delete this entry - LimeLog.severe("DB: Corrupted UUID for "+details.name); - } - - try { - details.localIp = InetAddress.getByAddress(c.getBlob(2)); - } catch (UnknownHostException e) { - // We'll delete this entry - LimeLog.severe("DB: Corrupted local IP for "+details.name); - } - - try { - details.remoteIp = InetAddress.getByAddress(c.getBlob(3)); - } catch (UnknownHostException e) { - // We'll delete this entry - LimeLog.severe("DB: Corrupted remote IP for "+details.name); - } - - details.macAddress = c.getString(4); - - c.close(); + c.close(); + + return computerList; + } + + public ComputerDetails getComputerByName(String name) { + Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null); + ComputerDetails details = new ComputerDetails(); + if (!c.moveToFirst()) { + // No matching computer + c.close(); + return null; + } + + details.name = c.getString(0); + + String uuidStr = c.getString(1); + try { + details.uuid = UUID.fromString(uuidStr); + } catch (IllegalArgumentException e) { + // We'll delete this entry + LimeLog.severe("DB: Corrupted UUID for "+details.name); + } + + try { + details.localIp = InetAddress.getByAddress(c.getBlob(2)); + } catch (UnknownHostException e) { + // We'll delete this entry + LimeLog.severe("DB: Corrupted local IP for "+details.name); + } + + try { + details.remoteIp = InetAddress.getByAddress(c.getBlob(3)); + } catch (UnknownHostException e) { + // We'll delete this entry + LimeLog.severe("DB: Corrupted remote IP for "+details.name); + } + + details.macAddress = c.getString(4); + + c.close(); details.state = ComputerDetails.State.UNKNOWN; details.reachability = ComputerDetails.Reachability.UNKNOWN; - - // If a field is corrupt or missing, delete the database entry - if (details.uuid == null || details.localIp == null || details.remoteIp == null || - details.macAddress == null) { - deleteComputer(details.name); - return null; - } - - return details; - } + + // If a field is corrupt or missing, delete the database entry + if (details.uuid == null || details.localIp == null || details.remoteIp == null || + details.macAddress == null) { + deleteComputer(details.name); + return null; + } + + return details; + } } diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerListener.java b/app/src/main/java/com/limelight/computers/ComputerManagerListener.java index 51e21a0a..43083602 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerListener.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerListener.java @@ -3,5 +3,5 @@ package com.limelight.computers; import com.limelight.nvstream.http.ComputerDetails; public interface ComputerManagerListener { - public void notifyComputerUpdated(ComputerDetails details); + public void notifyComputerUpdated(ComputerDetails details); } diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index b85b0bf6..3d331f6d 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -29,39 +29,39 @@ import android.os.IBinder; 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 final ComputerManagerBinder binder = new ComputerManagerBinder(); - - private ComputerDatabaseManager dbManager; - private final AtomicInteger dbRefCount = new AtomicInteger(0); - - private IdentityManager idManager; - private final LinkedList pollingTuples = new LinkedList(); - private ComputerManagerListener listener = null; - private final AtomicInteger activePolls = new AtomicInteger(0); + private static final int POLLING_PERIOD_MS = 3000; + private static final int MDNS_QUERY_PERIOD_MS = 1000; + + private final ComputerManagerBinder binder = new ComputerManagerBinder(); + + private ComputerDatabaseManager dbManager; + private final AtomicInteger dbRefCount = new AtomicInteger(0); + + private IdentityManager idManager; + private final LinkedList pollingTuples = new LinkedList(); + private ComputerManagerListener listener = null; + private final AtomicInteger activePolls = new AtomicInteger(0); private boolean pollingActive = false; - private DiscoveryService.DiscoveryBinder discoveryBinder; - private final ServiceConnection discoveryServiceConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName className, IBinder binder) { - synchronized (discoveryServiceConnection) { - DiscoveryService.DiscoveryBinder privateBinder = ((DiscoveryService.DiscoveryBinder)binder); - - // Set us as the event listener - privateBinder.setListener(createDiscoveryListener()); - - // Signal a possible waiter that we're all setup - discoveryBinder = privateBinder; - discoveryServiceConnection.notifyAll(); - } - } + private DiscoveryService.DiscoveryBinder discoveryBinder; + private final ServiceConnection discoveryServiceConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder binder) { + synchronized (discoveryServiceConnection) { + DiscoveryService.DiscoveryBinder privateBinder = ((DiscoveryService.DiscoveryBinder)binder); - public void onServiceDisconnected(ComponentName className) { - discoveryBinder = null; - } - }; + // Set us as the event listener + privateBinder.setListener(createDiscoveryListener()); + + // Signal a possible waiter that we're all setup + discoveryBinder = privateBinder; + discoveryServiceConnection.notifyAll(); + } + } + + public void onServiceDisconnected(ComponentName className) { + discoveryBinder = null; + } + }; // Returns true if the details object was modified private boolean runPoll(ComputerDetails details, boolean newPc) @@ -124,17 +124,17 @@ public class ComputerManagerService extends Service { t.setName("Polling thread for "+details.localIp.getHostAddress()); return t; } - - public class ComputerManagerBinder extends Binder { - public void startPolling(ComputerManagerListener listener) { + + public class ComputerManagerBinder extends Binder { + public void startPolling(ComputerManagerListener listener) { // Polling is active pollingActive = true; - // Set the listener - ComputerManagerService.this.listener = listener; - - // Start mDNS autodiscovery too - discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS); + // Set the listener + ComputerManagerService.this.listener = listener; + + // Start mDNS autodiscovery too + discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS); synchronized (pollingTuples) { for (PollingTuple tuple : pollingTuples) { @@ -148,48 +148,48 @@ public class ComputerManagerService extends Service { } } } - } - - public void waitForReady() { - synchronized (discoveryServiceConnection) { - try { - while (discoveryBinder == null) { - // Wait for the bind notification - discoveryServiceConnection.wait(1000); - } - } catch (InterruptedException ignored) { - } - } - } - - public void waitForPollingStopped() { - while (activePolls.get() != 0) { - try { - Thread.sleep(250); - } catch (InterruptedException ignored) {} - } - } - - public boolean addComputerBlocking(InetAddress addr) { - return ComputerManagerService.this.addComputerBlocking(addr); - } - - public void removeComputer(String name) { - ComputerManagerService.this.removeComputer(name); - } - - public void stopPolling() { - // Just call the unbind handler to cleanup - ComputerManagerService.this.onUnbind(null); - } + } + + public void waitForReady() { + synchronized (discoveryServiceConnection) { + try { + while (discoveryBinder == null) { + // Wait for the bind notification + discoveryServiceConnection.wait(1000); + } + } catch (InterruptedException ignored) { + } + } + } + + public void waitForPollingStopped() { + while (activePolls.get() != 0) { + try { + Thread.sleep(250); + } catch (InterruptedException ignored) {} + } + } + + public boolean addComputerBlocking(InetAddress addr) { + return ComputerManagerService.this.addComputerBlocking(addr); + } + + public void removeComputer(String name) { + ComputerManagerService.this.removeComputer(name); + } + + public void stopPolling() { + // Just call the unbind handler to cleanup + ComputerManagerService.this.onUnbind(null); + } public ApplistPoller createAppListPoller(ComputerDetails computer) { return new ApplistPoller(computer); } - - public String getUniqueId() { - return idManager.getUniqueId(); - } + + public String getUniqueId() { + return idManager.getUniqueId(); + } public ComputerDetails getComputer(UUID uuid) { synchronized (pollingTuples) { @@ -202,14 +202,14 @@ public class ComputerManagerService extends Service { return null; } - } - - @Override - public boolean onUnbind(Intent intent) { - // Stop mDNS autodiscovery - discoveryBinder.stopDiscovery(); - - // Stop polling + } + + @Override + public boolean onUnbind(Intent intent) { + // Stop mDNS autodiscovery + discoveryBinder.stopDiscovery(); + + // Stop polling pollingActive = false; synchronized (pollingTuples) { for (PollingTuple tuple : pollingTuples) { @@ -220,33 +220,33 @@ public class ComputerManagerService extends Service { } } } - - // Remove the listener - listener = null; - - return false; - } - - private MdnsDiscoveryListener createDiscoveryListener() { - return new MdnsDiscoveryListener() { - @Override - public void notifyComputerAdded(MdnsComputer computer) { - // Kick off a serverinfo poll on this machine - addComputerBlocking(computer.getAddress()); - } - @Override - public void notifyComputerRemoved(MdnsComputer computer) { - // Nothing to do here - } + // Remove the listener + listener = null; - @Override - public void notifyDiscoveryFailure(Exception e) { - LimeLog.severe("mDNS discovery failed"); - e.printStackTrace(); - } - }; - } + return false; + } + + private MdnsDiscoveryListener createDiscoveryListener() { + return new MdnsDiscoveryListener() { + @Override + public void notifyComputerAdded(MdnsComputer computer) { + // Kick off a serverinfo poll on this machine + addComputerBlocking(computer.getAddress()); + } + + @Override + public void notifyComputerRemoved(MdnsComputer computer) { + // Nothing to do here + } + + @Override + public void notifyDiscoveryFailure(Exception e) { + LimeLog.severe("mDNS discovery failed"); + e.printStackTrace(); + } + }; + } private void addTuple(ComputerDetails details) { synchronized (pollingTuples) { @@ -278,17 +278,17 @@ public class ComputerManagerService extends Service { } } - public boolean addComputerBlocking(InetAddress addr) { - // Setup a placeholder - ComputerDetails fakeDetails = new ComputerDetails(); - fakeDetails.localIp = addr; - fakeDetails.remoteIp = addr; + public boolean addComputerBlocking(InetAddress addr) { + // Setup a placeholder + ComputerDetails fakeDetails = new ComputerDetails(); + fakeDetails.localIp = addr; + fakeDetails.remoteIp = addr; - // Block while we try to fill the details + // Block while we try to fill the details runPoll(fakeDetails, true); - - // If the machine is reachable, it was successful - if (fakeDetails.state == ComputerDetails.State.ONLINE) { + + // 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 @@ -298,15 +298,15 @@ public class ComputerManagerService extends Service { else { return false; } - } - - public void removeComputer(String name) { - if (!getLocalDatabaseReference()) { - return; - } - - // Remove it from the database - dbManager.deleteComputer(name); + } + + public void removeComputer(String name) { + if (!getLocalDatabaseReference()) { + return; + } + + // Remove it from the database + dbManager.deleteComputer(name); synchronized (pollingTuples) { // Remove the computer from the computer list @@ -321,31 +321,31 @@ public class ComputerManagerService extends Service { } } } - - releaseLocalDatabaseReference(); - } - - private boolean getLocalDatabaseReference() { - if (dbRefCount.get() == 0) { - return false; - } - - dbRefCount.incrementAndGet(); - return true; - } - - private void releaseLocalDatabaseReference() { - if (dbRefCount.decrementAndGet() == 0) { - dbManager.close(); - } - } - - private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) { - try { - NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(), - null, PlatformBinding.getCryptoProvider(ComputerManagerService.this)); - ComputerDetails newDetails = http.getComputerDetails(); + releaseLocalDatabaseReference(); + } + + private boolean getLocalDatabaseReference() { + if (dbRefCount.get() == 0) { + return false; + } + + dbRefCount.incrementAndGet(); + return true; + } + + private void releaseLocalDatabaseReference() { + if (dbRefCount.decrementAndGet() == 0) { + dbManager.close(); + } + } + + private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) { + try { + NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(), + null, PlatformBinding.getCryptoProvider(ComputerManagerService.this)); + + ComputerDetails newDetails = http.getComputerDetails(); // Check if this is the PC we expected if (details.uuid != null && newDetails.uuid != null && @@ -356,58 +356,58 @@ public class ComputerManagerService extends Service { } return newDetails; - } catch (Exception e) { - return null; - } - } - - private boolean pollComputer(ComputerDetails details, boolean localFirst) { - ComputerDetails polledDetails; + } catch (Exception e) { + return null; + } + } + + private boolean pollComputer(ComputerDetails details, boolean localFirst) { + ComputerDetails polledDetails; // If the local address is routable across the Internet, // always consider this PC remote to be conservative if (details.localIp.equals(details.remoteIp)) { localFirst = false; } - - if (localFirst) { - polledDetails = tryPollIp(details, details.localIp); - } - else { - polledDetails = tryPollIp(details, details.remoteIp); - } - - if (polledDetails == null && !details.localIp.equals(details.remoteIp)) { - // Failed, so let's try the fallback - if (!localFirst) { - polledDetails = tryPollIp(details, details.localIp); - } - else { - polledDetails = tryPollIp(details, details.remoteIp); - } - - // The fallback poll worked - if (polledDetails != null) { - polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL : - ComputerDetails.Reachability.REMOTE; - } - } - else if (polledDetails != null) { - polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL : - ComputerDetails.Reachability.REMOTE; - } - - // Machine was unreachable both tries - if (polledDetails == null) { - return false; - } - - // If we got here, it's reachable - details.update(polledDetails); - return true; - } - - private boolean doPollMachine(ComputerDetails details) { + + if (localFirst) { + polledDetails = tryPollIp(details, details.localIp); + } + else { + polledDetails = tryPollIp(details, details.remoteIp); + } + + if (polledDetails == null && !details.localIp.equals(details.remoteIp)) { + // Failed, so let's try the fallback + if (!localFirst) { + polledDetails = tryPollIp(details, details.localIp); + } + else { + polledDetails = tryPollIp(details, details.remoteIp); + } + + // The fallback poll worked + if (polledDetails != null) { + polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL : + ComputerDetails.Reachability.REMOTE; + } + } + else if (polledDetails != null) { + polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL : + ComputerDetails.Reachability.REMOTE; + } + + // Machine was unreachable both tries + if (polledDetails == null) { + return false; + } + + // If we got here, it's reachable + details.update(polledDetails); + 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 @@ -420,20 +420,20 @@ public class ComputerManagerService extends Service { // always try that one first return pollComputer(details, details.reachability == ComputerDetails.Reachability.LOCAL); } - } - - @Override - public void onCreate() { - // Bind to the discovery service - bindService(new Intent(this, DiscoveryService.class), - discoveryServiceConnection, Service.BIND_AUTO_CREATE); - - // Lookup or generate this device's UID - idManager = new IdentityManager(this); - - // Initialize the DB - dbManager = new ComputerDatabaseManager(this); - dbRefCount.set(1); + } + + @Override + public void onCreate() { + // Bind to the discovery service + bindService(new Intent(this, DiscoveryService.class), + discoveryServiceConnection, Service.BIND_AUTO_CREATE); + + // Lookup or generate this device's UID + idManager = new IdentityManager(this); + + // Initialize the DB + dbManager = new ComputerDatabaseManager(this); + dbRefCount.set(1); // Grab known machines into our computer list if (!getLocalDatabaseReference()) { @@ -446,25 +446,25 @@ public class ComputerManagerService extends Service { } releaseLocalDatabaseReference(); - } - - @Override - public void onDestroy() { - if (discoveryBinder != null) { - // Unbind from the discovery service - unbindService(discoveryServiceConnection); - } - - // FIXME: Should await termination here but we have timeout issues in HttpURLConnection - - // Remove the initial DB reference - releaseLocalDatabaseReference(); - } - - @Override - public IBinder onBind(Intent intent) { - return binder; - } + } + + @Override + public void onDestroy() { + if (discoveryBinder != null) { + // Unbind from the discovery service + unbindService(discoveryServiceConnection); + } + + // FIXME: Should await termination here but we have timeout issues in HttpURLConnection + + // Remove the initial DB reference + releaseLocalDatabaseReference(); + } + + @Override + public IBinder onBind(Intent intent) { + return binder; + } public class ApplistPoller { private Thread thread; diff --git a/app/src/main/java/com/limelight/computers/IdentityManager.java b/app/src/main/java/com/limelight/computers/IdentityManager.java index 8734d124..e6251d4f 100644 --- a/app/src/main/java/com/limelight/computers/IdentityManager.java +++ b/app/src/main/java/com/limelight/computers/IdentityManager.java @@ -12,75 +12,75 @@ import com.limelight.LimeLog; import android.content.Context; public class IdentityManager { - private static final String UNIQUE_ID_FILE_NAME = "uniqueid"; - private static final int UID_SIZE_IN_BYTES = 8; - - private String uniqueId; - - public IdentityManager(Context c) { - uniqueId = loadUniqueId(c); - if (uniqueId == null) { - uniqueId = generateNewUniqueId(c); - } - - LimeLog.info("UID is now: "+uniqueId); - } - - public String getUniqueId() { - return uniqueId; - } - - private static String loadUniqueId(Context c) { - // 2 Hex digits per byte - char[] uid = new char[UID_SIZE_IN_BYTES * 2]; - InputStreamReader reader = null; - LimeLog.info("Reading UID from disk"); - try { - reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME)); - if (reader.read(uid) != UID_SIZE_IN_BYTES * 2) - { - LimeLog.severe("UID file data is truncated"); - return null; - } - return new String(uid); - } catch (FileNotFoundException e) { - LimeLog.info("No UID file found"); - return null; - } catch (IOException e) { - LimeLog.severe("Error while reading UID file"); - e.printStackTrace(); - return null; - } finally { - if (reader != null) { - try { - reader.close(); - } catch (IOException ignored) {} - } - } - } - - private static String generateNewUniqueId(Context c) { - // Generate a new UID hex string - LimeLog.info("Generating new UID"); - String uidStr = String.format((Locale)null, "%016x", new Random().nextLong()); - - OutputStreamWriter writer = null; - try { - writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0)); - writer.write(uidStr); - LimeLog.info("UID written to disk"); - } catch (IOException e) { - LimeLog.severe("Error while writing UID file"); - e.printStackTrace(); - } finally { - if (writer != null) { - try { - writer.close(); - } catch (IOException ignored) {} - } - } - - // We can return a UID even if I/O fails - return uidStr; - } + private static final String UNIQUE_ID_FILE_NAME = "uniqueid"; + private static final int UID_SIZE_IN_BYTES = 8; + + private String uniqueId; + + public IdentityManager(Context c) { + uniqueId = loadUniqueId(c); + if (uniqueId == null) { + uniqueId = generateNewUniqueId(c); + } + + LimeLog.info("UID is now: "+uniqueId); + } + + public String getUniqueId() { + return uniqueId; + } + + private static String loadUniqueId(Context c) { + // 2 Hex digits per byte + char[] uid = new char[UID_SIZE_IN_BYTES * 2]; + InputStreamReader reader = null; + LimeLog.info("Reading UID from disk"); + try { + reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME)); + if (reader.read(uid) != UID_SIZE_IN_BYTES * 2) + { + LimeLog.severe("UID file data is truncated"); + return null; + } + return new String(uid); + } catch (FileNotFoundException e) { + LimeLog.info("No UID file found"); + return null; + } catch (IOException e) { + LimeLog.severe("Error while reading UID file"); + e.printStackTrace(); + return null; + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException ignored) {} + } + } + } + + private static String generateNewUniqueId(Context c) { + // Generate a new UID hex string + LimeLog.info("Generating new UID"); + String uidStr = String.format((Locale)null, "%016x", new Random().nextLong()); + + OutputStreamWriter writer = null; + try { + writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0)); + writer.write(uidStr); + LimeLog.info("UID written to disk"); + } catch (IOException e) { + LimeLog.severe("Error while writing UID file"); + e.printStackTrace(); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException ignored) {} + } + } + + // We can return a UID even if I/O fails + return uidStr; + } } diff --git a/app/src/main/java/com/limelight/discovery/DiscoveryService.java b/app/src/main/java/com/limelight/discovery/DiscoveryService.java index 7e6497fa..5f68fb02 100644 --- a/app/src/main/java/com/limelight/discovery/DiscoveryService.java +++ b/app/src/main/java/com/limelight/discovery/DiscoveryService.java @@ -15,76 +15,76 @@ import android.os.Binder; import android.os.IBinder; public class DiscoveryService extends Service { - - private MdnsDiscoveryAgent discoveryAgent; - private MdnsDiscoveryListener boundListener; - private MulticastLock multicastLock; - - public class DiscoveryBinder extends Binder { - public void setListener(MdnsDiscoveryListener listener) { - boundListener = listener; - } - - public void startDiscovery(int queryIntervalMs) { - multicastLock.acquire(); - discoveryAgent.startDiscovery(queryIntervalMs); - } - - public void stopDiscovery() { - discoveryAgent.stopDiscovery(); - multicastLock.release(); - } - - public List getComputerSet() { - return discoveryAgent.getComputerSet(); - } - } - - @Override - public void onCreate() { - WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE); - multicastLock = wifiMgr.createMulticastLock("Limelight mDNS"); - multicastLock.setReferenceCounted(false); - - discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() { - @Override - public void notifyComputerAdded(MdnsComputer computer) { - if (boundListener != null) { - boundListener.notifyComputerAdded(computer); - } - } - @Override - public void notifyComputerRemoved(MdnsComputer computer) { - if (boundListener != null) { - boundListener.notifyComputerRemoved(computer); - } - } + private MdnsDiscoveryAgent discoveryAgent; + private MdnsDiscoveryListener boundListener; + private MulticastLock multicastLock; - @Override - public void notifyDiscoveryFailure(Exception e) { - if (boundListener != null) { - boundListener.notifyDiscoveryFailure(e); - } - } - }); - } - - private final DiscoveryBinder binder = new DiscoveryBinder(); - - @Override - public IBinder onBind(Intent intent) { - return binder; - } - - @Override - public boolean onUnbind(Intent intent) { - // Stop any discovery session - discoveryAgent.stopDiscovery(); - multicastLock.release(); - - // Unbind the listener - boundListener = null; - return false; - } + public class DiscoveryBinder extends Binder { + public void setListener(MdnsDiscoveryListener listener) { + boundListener = listener; + } + + public void startDiscovery(int queryIntervalMs) { + multicastLock.acquire(); + discoveryAgent.startDiscovery(queryIntervalMs); + } + + public void stopDiscovery() { + discoveryAgent.stopDiscovery(); + multicastLock.release(); + } + + public List getComputerSet() { + return discoveryAgent.getComputerSet(); + } + } + + @Override + public void onCreate() { + WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE); + multicastLock = wifiMgr.createMulticastLock("Limelight mDNS"); + multicastLock.setReferenceCounted(false); + + discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() { + @Override + public void notifyComputerAdded(MdnsComputer computer) { + if (boundListener != null) { + boundListener.notifyComputerAdded(computer); + } + } + + @Override + public void notifyComputerRemoved(MdnsComputer computer) { + if (boundListener != null) { + boundListener.notifyComputerRemoved(computer); + } + } + + @Override + public void notifyDiscoveryFailure(Exception e) { + if (boundListener != null) { + boundListener.notifyDiscoveryFailure(e); + } + } + }); + } + + private final DiscoveryBinder binder = new DiscoveryBinder(); + + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + @Override + public boolean onUnbind(Intent intent) { + // Stop any discovery session + discoveryAgent.stopDiscovery(); + multicastLock.release(); + + // Unbind the listener + boundListener = null; + return false; + } } diff --git a/app/src/main/java/com/limelight/nvstream/av/video/cpu/AvcDecoder.java b/app/src/main/java/com/limelight/nvstream/av/video/cpu/AvcDecoder.java index d3e504f2..326fc18d 100644 --- a/app/src/main/java/com/limelight/nvstream/av/video/cpu/AvcDecoder.java +++ b/app/src/main/java/com/limelight/nvstream/av/video/cpu/AvcDecoder.java @@ -1,44 +1,44 @@ package com.limelight.nvstream.av.video.cpu; public class AvcDecoder { - static { - // FFMPEG dependencies - System.loadLibrary("avutil-52"); - System.loadLibrary("swresample-0"); - System.loadLibrary("swscale-2"); - System.loadLibrary("avcodec-55"); - System.loadLibrary("avformat-55"); - - System.loadLibrary("nv_avc_dec"); - } - - /** Disables the deblocking filter at the cost of image quality */ - public static final int DISABLE_LOOP_FILTER = 0x1; - /** Uses the low latency decode flag (disables multithreading) */ - public static final int LOW_LATENCY_DECODE = 0x2; - /** Threads process each slice, rather than each frame */ - public static final int SLICE_THREADING = 0x4; - /** Uses nonstandard speedup tricks */ - public static final int FAST_DECODE = 0x8; - /** Uses bilinear filtering instead of bicubic */ - public static final int BILINEAR_FILTERING = 0x10; - /** Uses a faster bilinear filtering with lower image quality */ - public static final int FAST_BILINEAR_FILTERING = 0x20; - /** Disables color conversion (output is NV21) */ - public static final int NO_COLOR_CONVERSION = 0x40; - - public static native int init(int width, int height, int perflvl, int threadcount); - public static native void destroy(); - - // Rendering API when NO_COLOR_CONVERSION == 0 - public static native boolean setRenderTarget(Object androidSurface); - public static native boolean getRgbFrameInt(int[] rgbFrame, int bufferSize); - public static native boolean getRgbFrame(byte[] rgbFrame, int bufferSize); - public static native boolean redraw(); + static { + // FFMPEG dependencies + System.loadLibrary("avutil-52"); + System.loadLibrary("swresample-0"); + System.loadLibrary("swscale-2"); + System.loadLibrary("avcodec-55"); + System.loadLibrary("avformat-55"); - // Rendering API when NO_COLOR_CONVERSION == 1 - public static native boolean getRawFrame(byte[] yuvFrame, int bufferSize); - - public static native int getInputPaddingSize(); - public static native int decode(byte[] indata, int inoff, int inlen); + System.loadLibrary("nv_avc_dec"); + } + + /** Disables the deblocking filter at the cost of image quality */ + public static final int DISABLE_LOOP_FILTER = 0x1; + /** Uses the low latency decode flag (disables multithreading) */ + public static final int LOW_LATENCY_DECODE = 0x2; + /** Threads process each slice, rather than each frame */ + public static final int SLICE_THREADING = 0x4; + /** Uses nonstandard speedup tricks */ + public static final int FAST_DECODE = 0x8; + /** Uses bilinear filtering instead of bicubic */ + public static final int BILINEAR_FILTERING = 0x10; + /** Uses a faster bilinear filtering with lower image quality */ + public static final int FAST_BILINEAR_FILTERING = 0x20; + /** Disables color conversion (output is NV21) */ + public static final int NO_COLOR_CONVERSION = 0x40; + + public static native int init(int width, int height, int perflvl, int threadcount); + public static native void destroy(); + + // Rendering API when NO_COLOR_CONVERSION == 0 + public static native boolean setRenderTarget(Object androidSurface); + public static native boolean getRgbFrameInt(int[] rgbFrame, int bufferSize); + public static native boolean getRgbFrame(byte[] rgbFrame, int bufferSize); + public static native boolean redraw(); + + // Rendering API when NO_COLOR_CONVERSION == 1 + public static native boolean getRawFrame(byte[] yuvFrame, int bufferSize); + + public static native int getInputPaddingSize(); + public static native int decode(byte[] indata, int inoff, int inlen); } diff --git a/app/src/main/java/com/limelight/preferences/AddComputerManually.java b/app/src/main/java/com/limelight/preferences/AddComputerManually.java index 71f84746..c484df75 100644 --- a/app/src/main/java/com/limelight/preferences/AddComputerManually.java +++ b/app/src/main/java/com/limelight/preferences/AddComputerManually.java @@ -25,127 +25,127 @@ import android.widget.TextView; import android.widget.Toast; public class AddComputerManually extends Activity { - private TextView hostText; - private ComputerManagerService.ComputerManagerBinder managerBinder; - private final LinkedBlockingQueue computersToAdd = new LinkedBlockingQueue(); - private Thread addThread; - private final ServiceConnection serviceConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName className, final IBinder binder) { - managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder); - startAddThread(); - } + private TextView hostText; + private ComputerManagerService.ComputerManagerBinder managerBinder; + private final LinkedBlockingQueue computersToAdd = new LinkedBlockingQueue(); + private Thread addThread; + private final ServiceConnection serviceConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, final IBinder binder) { + managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder); + startAddThread(); + } - public void onServiceDisconnected(ComponentName className) { - joinAddThread(); - managerBinder = null; - } - }; - - private void doAddPc(String host) { - String msg; - boolean finish = false; + public void onServiceDisconnected(ComponentName className) { + joinAddThread(); + managerBinder = null; + } + }; + + private void doAddPc(String host) { + String msg; + boolean finish = false; SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc), - getResources().getString(R.string.msg_add_pc), false); + getResources().getString(R.string.msg_add_pc), false); - try { - InetAddress addr = InetAddress.getByName(host); - - if (!managerBinder.addComputerBlocking(addr)){ - msg = getResources().getString(R.string.addpc_fail); - } - else { - msg = getResources().getString(R.string.addpc_success); - finish = true; - } - } catch (UnknownHostException e) { - msg = getResources().getString(R.string.addpc_unknown_host); - } + try { + InetAddress addr = InetAddress.getByName(host); + + if (!managerBinder.addComputerBlocking(addr)){ + msg = getResources().getString(R.string.addpc_fail); + } + else { + msg = getResources().getString(R.string.addpc_success); + finish = true; + } + } catch (UnknownHostException e) { + msg = getResources().getString(R.string.addpc_unknown_host); + } dialog.dismiss(); final boolean toastFinish = finish; - final String toastMsg = msg; - AddComputerManually.this.runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show(); - - if (toastFinish && !isFinishing()) { - // Close the activity - AddComputerManually.this.finish(); - } - } - }); - } - - private void startAddThread() { - addThread = new Thread() { - @Override - public void run() { - while (!isInterrupted()) { - String computer; - - try { - computer = computersToAdd.take(); - } catch (InterruptedException e) { - return; - } - - doAddPc(computer); - } - } - }; - addThread.setName("UI - AddComputerManually"); - addThread.start(); - } - - private void joinAddThread() { - if (addThread != null) { - addThread.interrupt(); - - try { - addThread.join(); - } catch (InterruptedException ignored) {} - - addThread = null; - } - } + final String toastMsg = msg; + AddComputerManually.this.runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show(); - @Override - protected void onStop() { - super.onStop(); - - Dialog.closeDialogs(); + if (toastFinish && !isFinishing()) { + // Close the activity + AddComputerManually.this.finish(); + } + } + }); + } + + private void startAddThread() { + addThread = new Thread() { + @Override + public void run() { + while (!isInterrupted()) { + String computer; + + try { + computer = computersToAdd.take(); + } catch (InterruptedException e) { + return; + } + + doAddPc(computer); + } + } + }; + addThread.setName("UI - AddComputerManually"); + addThread.start(); + } + + private void joinAddThread() { + if (addThread != null) { + addThread.interrupt(); + + try { + addThread.join(); + } catch (InterruptedException ignored) {} + + addThread = null; + } + } + + @Override + protected void onStop() { + super.onStop(); + + Dialog.closeDialogs(); SpinnerDialog.closeDialogs(this); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - - if (managerBinder != null) { - joinAddThread(); - unbindService(serviceConnection); - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - String locale = PreferenceConfiguration.readPreferences(this).language; - if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) { - Configuration config = new Configuration(getResources().getConfiguration()); - config.locale = new Locale(locale); - getResources().updateConfiguration(config, getResources().getDisplayMetrics()); - } - - setContentView(R.layout.activity_add_computer_manually); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + if (managerBinder != null) { + joinAddThread(); + unbindService(serviceConnection); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + String locale = PreferenceConfiguration.readPreferences(this).language; + if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) { + Configuration config = new Configuration(getResources().getConfiguration()); + config.locale = new Locale(locale); + getResources().updateConfiguration(config, getResources().getDisplayMetrics()); + } + + setContentView(R.layout.activity_add_computer_manually); UiHelper.notifyNewRootView(this); - this.hostText = (TextView) findViewById(R.id.hostTextView); + this.hostText = (TextView) findViewById(R.id.hostTextView); hostText.setImeOptions(EditorInfo.IME_ACTION_DONE); hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override @@ -165,9 +165,9 @@ public class AddComputerManually extends Activity { return false; } }); - - // Bind to the ComputerManager service - bindService(new Intent(AddComputerManually.this, - ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE); - } + + // Bind to the ComputerManager service + bindService(new Intent(AddComputerManually.this, + ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE); + } } diff --git a/app/src/main/java/com/limelight/utils/Dialog.java b/app/src/main/java/com/limelight/utils/Dialog.java index 720f687a..0621ba7f 100644 --- a/app/src/main/java/com/limelight/utils/Dialog.java +++ b/app/src/main/java/com/limelight/utils/Dialog.java @@ -7,71 +7,71 @@ import android.app.AlertDialog; import android.content.DialogInterface; public class Dialog implements Runnable { - private final String title; + private final String title; private final String message; - private final Activity activity; - private final boolean endAfterDismiss; - - private AlertDialog alert; - - private static final ArrayList rundownDialogs = new ArrayList(); - - private Dialog(Activity activity, String title, String message, boolean endAfterDismiss) - { - this.activity = activity; - this.title = title; - this.message = message; - this.endAfterDismiss = endAfterDismiss; - } - - public static void closeDialogs() - { - synchronized (rundownDialogs) { - for (Dialog d : rundownDialogs) { - if (d.alert.isShowing()) { - d.alert.dismiss(); - } - } - - rundownDialogs.clear(); - } - } - - public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss) - { - activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss)); - } - - @Override - public void run() { - // If we're dying, don't bother creating a dialog - if (activity.isFinishing()) - return; - - alert = new AlertDialog.Builder(activity).create(); + private final Activity activity; + private final boolean endAfterDismiss; - alert.setTitle(title); - alert.setMessage(message); - alert.setCancelable(false); - alert.setCanceledOnTouchOutside(false); + private AlertDialog alert; + + private static final ArrayList rundownDialogs = new ArrayList(); + + private Dialog(Activity activity, String title, String message, boolean endAfterDismiss) + { + this.activity = activity; + this.title = title; + this.message = message; + this.endAfterDismiss = endAfterDismiss; + } + + public static void closeDialogs() + { + synchronized (rundownDialogs) { + for (Dialog d : rundownDialogs) { + if (d.alert.isShowing()) { + d.alert.dismiss(); + } + } + + rundownDialogs.clear(); + } + } + + public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss) + { + activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss)); + } + + @Override + public void run() { + // If we're dying, don't bother creating a dialog + if (activity.isFinishing()) + return; + + alert = new AlertDialog.Builder(activity).create(); + + alert.setTitle(title); + alert.setMessage(message); + alert.setCancelable(false); + alert.setCanceledOnTouchOutside(false); - alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - synchronized (rundownDialogs) { - rundownDialogs.remove(Dialog.this); - alert.dismiss(); - } - - if (endAfterDismiss) { - activity.finish(); - } - } - }); - - synchronized (rundownDialogs) { - rundownDialogs.add(this); - alert.show(); - } - } + alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + synchronized (rundownDialogs) { + rundownDialogs.remove(Dialog.this); + alert.dismiss(); + } + + if (endAfterDismiss) { + activity.finish(); + } + } + }); + + synchronized (rundownDialogs) { + rundownDialogs.add(this); + alert.show(); + } + } } diff --git a/app/src/main/java/com/limelight/utils/SpinnerDialog.java b/app/src/main/java/com/limelight/utils/SpinnerDialog.java index df97b2c3..7b65435b 100644 --- a/app/src/main/java/com/limelight/utils/SpinnerDialog.java +++ b/app/src/main/java/com/limelight/utils/SpinnerDialog.java @@ -9,112 +9,112 @@ import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; public class SpinnerDialog implements Runnable,OnCancelListener { - private final String title; + private final String title; private final String message; - private final Activity activity; - private ProgressDialog progress; - private final boolean finish; - - private static final ArrayList rundownDialogs = new ArrayList(); - - private SpinnerDialog(Activity activity, String title, String message, boolean finish) - { - this.activity = activity; - this.title = title; - this.message = message; - this.progress = null; - this.finish = finish; - } - - public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish) - { - SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish); - activity.runOnUiThread(spinner); - return spinner; - } - - public static void closeDialogs(Activity activity) - { - synchronized (rundownDialogs) { - Iterator i = rundownDialogs.iterator(); - while (i.hasNext()) { - SpinnerDialog dialog = i.next(); - if (dialog.activity == activity) { - i.remove(); - if (dialog.progress.isShowing()) { - dialog.progress.dismiss(); - } - } - } - } - } - - public void dismiss() - { - // Running again with progress != null will destroy it - activity.runOnUiThread(this); - } - - public void setMessage(final String message) - { - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - progress.setMessage(message); - } - }); - } - - @Override - public void run() { + private final Activity activity; + private ProgressDialog progress; + private final boolean finish; - // If we're dying, don't bother doing anything - if (activity.isFinishing()) { - return; - } - - if (progress == null) - { - progress = new ProgressDialog(activity); - - progress.setTitle(title); - progress.setMessage(message); - progress.setProgressStyle(ProgressDialog.STYLE_SPINNER); - progress.setOnCancelListener(this); - - // If we want to finish the activity when this is killed, make it cancellable - if (finish) - { - progress.setCancelable(true); - progress.setCanceledOnTouchOutside(false); - } - else - { - progress.setCancelable(false); - } - - synchronized (rundownDialogs) { - rundownDialogs.add(this); - progress.show(); - } - } - else - { - synchronized (rundownDialogs) { - if (rundownDialogs.remove(this) && progress.isShowing()) { - progress.dismiss(); - } - } - } - } + private static final ArrayList rundownDialogs = new ArrayList(); - @Override - public void onCancel(DialogInterface dialog) { - synchronized (rundownDialogs) { - rundownDialogs.remove(this); - } - - // This will only be called if finish was true, so we don't need to check again - activity.finish(); - } + private SpinnerDialog(Activity activity, String title, String message, boolean finish) + { + this.activity = activity; + this.title = title; + this.message = message; + this.progress = null; + this.finish = finish; + } + + public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish) + { + SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish); + activity.runOnUiThread(spinner); + return spinner; + } + + public static void closeDialogs(Activity activity) + { + synchronized (rundownDialogs) { + Iterator i = rundownDialogs.iterator(); + while (i.hasNext()) { + SpinnerDialog dialog = i.next(); + if (dialog.activity == activity) { + i.remove(); + if (dialog.progress.isShowing()) { + dialog.progress.dismiss(); + } + } + } + } + } + + public void dismiss() + { + // Running again with progress != null will destroy it + activity.runOnUiThread(this); + } + + public void setMessage(final String message) + { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + progress.setMessage(message); + } + }); + } + + @Override + public void run() { + + // If we're dying, don't bother doing anything + if (activity.isFinishing()) { + return; + } + + if (progress == null) + { + progress = new ProgressDialog(activity); + + progress.setTitle(title); + progress.setMessage(message); + progress.setProgressStyle(ProgressDialog.STYLE_SPINNER); + progress.setOnCancelListener(this); + + // If we want to finish the activity when this is killed, make it cancellable + if (finish) + { + progress.setCancelable(true); + progress.setCanceledOnTouchOutside(false); + } + else + { + progress.setCancelable(false); + } + + synchronized (rundownDialogs) { + rundownDialogs.add(this); + progress.show(); + } + } + else + { + synchronized (rundownDialogs) { + if (rundownDialogs.remove(this) && progress.isShowing()) { + progress.dismiss(); + } + } + } + } + + @Override + public void onCancel(DialogInterface dialog) { + synchronized (rundownDialogs) { + rundownDialogs.remove(this); + } + + // This will only be called if finish was true, so we don't need to check again + activity.finish(); + } } From 5c938535beea8fe799867dcba7ea9325761e1b91 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 7 Feb 2015 13:20:01 -0500 Subject: [PATCH 015/202] Fix app list focus issues with remotes/gamepads --- app/src/main/java/com/limelight/AppView.java | 1 + app/src/main/res/layout/app_grid_view.xml | 2 ++ app/src/main/res/layout/app_grid_view_small.xml | 2 ++ app/src/main/res/layout/list_view.xml | 2 ++ app/src/main/res/layout/pc_grid_view.xml | 2 ++ app/src/main/res/layout/pc_grid_view_small.xml | 2 ++ 6 files changed, 11 insertions(+) diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index baefa505..4ebdb8f2 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -494,6 +494,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { } }); registerForContextMenu(listView); + listView.requestFocus(); } public class AppObject { diff --git a/app/src/main/res/layout/app_grid_view.xml b/app/src/main/res/layout/app_grid_view.xml index 980ef19a..192bbf37 100644 --- a/app/src/main/res/layout/app_grid_view.xml +++ b/app/src/main/res/layout/app_grid_view.xml @@ -9,5 +9,7 @@ android:numColumns="auto_fit" android:columnWidth="160dp" android:stretchMode="spacingWidth" + android:focusable="true" + android:focusableInTouchMode="true" android:gravity="center"/> \ No newline at end of file diff --git a/app/src/main/res/layout/app_grid_view_small.xml b/app/src/main/res/layout/app_grid_view_small.xml index cb7b5518..1d7c97f0 100644 --- a/app/src/main/res/layout/app_grid_view_small.xml +++ b/app/src/main/res/layout/app_grid_view_small.xml @@ -9,5 +9,7 @@ android:numColumns="auto_fit" android:columnWidth="105dp" android:stretchMode="spacingWidth" + android:focusable="true" + android:focusableInTouchMode="true" android:gravity="center"/> \ No newline at end of file diff --git a/app/src/main/res/layout/list_view.xml b/app/src/main/res/layout/list_view.xml index e303b15c..06b044df 100644 --- a/app/src/main/res/layout/list_view.xml +++ b/app/src/main/res/layout/list_view.xml @@ -10,6 +10,8 @@ android:background="@drawable/list_view_unselected" android:fastScrollEnabled="true" android:longClickable="false" + android:focusable="true" + android:focusableInTouchMode="true" android:stackFromBottom="false" > diff --git a/app/src/main/res/layout/pc_grid_view.xml b/app/src/main/res/layout/pc_grid_view.xml index 312d840c..bce5a0e2 100644 --- a/app/src/main/res/layout/pc_grid_view.xml +++ b/app/src/main/res/layout/pc_grid_view.xml @@ -8,5 +8,7 @@ android:layout_height="fill_parent" android:numColumns="auto_fit" android:columnWidth="160dp" + android:focusable="true" + android:focusableInTouchMode="true" android:gravity="center"/> \ No newline at end of file diff --git a/app/src/main/res/layout/pc_grid_view_small.xml b/app/src/main/res/layout/pc_grid_view_small.xml index 22486764..1dde64e4 100644 --- a/app/src/main/res/layout/pc_grid_view_small.xml +++ b/app/src/main/res/layout/pc_grid_view_small.xml @@ -8,5 +8,7 @@ android:layout_height="fill_parent" android:numColumns="auto_fit" android:columnWidth="105dp" + android:focusable="true" + android:focusableInTouchMode="true" android:gravity="center"/> \ No newline at end of file From 3d95ac1f938e017a8e47328db77d8440c66c2f47 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 7 Feb 2015 13:42:49 -0500 Subject: [PATCH 016/202] Fix keyboard dismissal on Fire TV devices --- .../com/limelight/preferences/AddComputerManually.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/com/limelight/preferences/AddComputerManually.java b/app/src/main/java/com/limelight/preferences/AddComputerManually.java index c484df75..cbbc8786 100644 --- a/app/src/main/java/com/limelight/preferences/AddComputerManually.java +++ b/app/src/main/java/com/limelight/preferences/AddComputerManually.java @@ -14,6 +14,7 @@ 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; @@ -21,6 +22,7 @@ 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; @@ -161,6 +163,12 @@ 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; } From 5519d922431d57c483b29973b831b8f07b163a89 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 7 Feb 2015 13:58:53 -0500 Subject: [PATCH 017/202] Disable the start key shortcut to start the keyboard because the keyboard can't receive input after it's started --- .../java/com/limelight/binding/input/ControllerHandler.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java index 24551bad..551f96b8 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -578,7 +578,9 @@ public class ControllerHandler implements InputManager.InputDeviceListener { case KeyEvent.KEYCODE_BUTTON_START: case KeyEvent.KEYCODE_MENU: if (SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) { - gestures.showKeyboard(); + // FIXME: The stock keyboard doesn't have controller focus so isn't usable. I'm not enabling this shortcut + // until we have a custom keyboard or some other fix + //gestures.showKeyboard(); } context.inputMap &= ~ControllerPacket.PLAY_FLAG; break; From aee34f6365c09085f8fc24aacd51cb31b42d1d93 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 8 Feb 2015 23:44:33 -0500 Subject: [PATCH 018/202] Remove redundant null checks --- app/src/main/java/com/limelight/AppView.java | 18 +++--------------- app/src/main/java/com/limelight/PcView.java | 6 ++++-- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index 4ebdb8f2..08e6b4dc 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -264,10 +264,6 @@ 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; @@ -282,10 +278,6 @@ 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()) { @@ -380,10 +372,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { // 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()) { @@ -481,9 +469,6 @@ 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) { @@ -501,6 +486,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { public final NvApp app; public AppObject(NvApp app) { + if (app == null) { + throw new IllegalArgumentException("app must not be null"); + } this.app = app; } diff --git a/app/src/main/java/com/limelight/PcView.java b/app/src/main/java/com/limelight/PcView.java index c6a44a9e..376e0b6b 100644 --- a/app/src/main/java/com/limelight/PcView.java +++ b/app/src/main/java/com/limelight/PcView.java @@ -237,8 +237,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position); - if (computer == null || computer.details == null || - computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) { + if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) { startComputerUpdates(); return; } @@ -587,6 +586,9 @@ 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; } From 057530eed0d80b8fa08545eca69a588a3d0c436c Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 8 Feb 2015 23:46:03 -0500 Subject: [PATCH 019/202] Correctly identify computers that are the same --- app/src/main/java/com/limelight/AppView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index 08e6b4dc..ea838010 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -127,7 +127,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { @Override public void notifyComputerUpdated(ComputerDetails details) { // Don't care about other computers - if (details != computer) { + if (!details.uuid.toString().equalsIgnoreCase(uuidString)) { return; } From 43fa1a72453854113d520a2e655c46ae37e24ccf Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 9 Feb 2015 00:12:29 -0500 Subject: [PATCH 020/202] Update common to fix null app name issue --- app/libs/limelight-common.jar | Bin 958435 -> 958781 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/libs/limelight-common.jar b/app/libs/limelight-common.jar index 654e0ca406c9cb3f7251966ab6c868ef57fecfa6..af46940fc72f96fa474bb4ec26a102815dd7f20c 100644 GIT binary patch delta 12657 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%0AEc>RxB!C@njz>X#n8%)>Kn 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 From 7b0ddfae423224b59302581e205b4dec725437de Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Wed, 11 Feb 2015 00:34:56 -0500 Subject: [PATCH 021/202] Update iml files --- app/app.iml | 4 +--- limelight-android.iml | 6 ++++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/app.iml b/app/app.iml index 87814d7a..f7cfabed 100644 --- a/app/app.iml +++ b/app/app.iml @@ -1,5 +1,5 @@ - + @@ -9,7 +9,6 @@ - diff --git a/limelight-android.iml b/limelight-android.iml index 0bb6048a..2a022014 100644 --- a/limelight-android.iml +++ b/limelight-android.iml @@ -1,5 +1,5 @@ - + @@ -7,7 +7,9 @@ - + + + From 7d25d07c6d7764508b817706e4f159675a8b9436 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Wed, 11 Feb 2015 00:55:47 -0500 Subject: [PATCH 022/202] Update version to 3.1.1 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 6f5d10e7..d88154d1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { minSdkVersion 16 targetSdkVersion 21 - versionName "3.1" - versionCode = 53 + versionName "3.1.1" + versionCode = 54 } productFlavors { From e04ff048b8d3953053392c0bc64dec7ea9824547 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Wed, 11 Feb 2015 17:04:31 -0500 Subject: [PATCH 023/202] Implement a fast polling method to speed up polling. Save the old MAC address if it's empty. --- .../computers/ComputerManagerService.java | 141 ++++++++++++++---- 1 file changed, 112 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index 3d331f6d..78262358 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -4,6 +4,8 @@ import java.io.FileOutputStream; import java.io.IOException; 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; @@ -31,6 +33,7 @@ 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 final ComputerManagerBinder binder = new ComputerManagerBinder(); @@ -64,8 +67,7 @@ public class ComputerManagerService extends Service { }; // Returns true if the details object was modified - private boolean runPoll(ComputerDetails details, boolean newPc) - { + private boolean runPoll(ComputerDetails details, boolean newPc) throws InterruptedException { if (!getLocalDatabaseReference()) { return false; } @@ -73,13 +75,18 @@ public class ComputerManagerService extends Service { activePolls.incrementAndGet(); // Poll the machine - if (!doPollMachine(details)) { - details.state = ComputerDetails.State.OFFLINE; - details.reachability = ComputerDetails.Reachability.OFFLINE; + try { + if (!pollComputer(details)) { + details.state = ComputerDetails.State.OFFLINE; + details.reachability = ComputerDetails.Reachability.OFFLINE; + } + } catch (InterruptedException e) { + releaseLocalDatabaseReference(); + throw e; + } finally { + activePolls.decrementAndGet(); } - activePolls.decrementAndGet(); - // If it's online, update our persistent state if (details.state == ComputerDetails.State.ONLINE) { if (!newPc) { @@ -109,11 +116,11 @@ public class ComputerManagerService extends Service { @Override public void run() { while (!isInterrupted() && pollingActive) { - // Check if this poll has modified the details - runPoll(details, false); - - // Wait until the next polling interval try { + // Check if this poll has modified the details + pollComputer(details); + + // Wait until the next polling interval Thread.sleep(POLLING_PERIOD_MS); } catch (InterruptedException e) { break; @@ -285,7 +292,11 @@ public class ComputerManagerService extends Service { fakeDetails.remoteIp = addr; // Block while we try to fill the details - runPoll(fakeDetails, true); + try { + pollComputer(fakeDetails); + } catch (InterruptedException e) { + return false; + } // If the machine is reachable, it was successful if (fakeDetails.state == ComputerDetails.State.ONLINE) { @@ -361,14 +372,91 @@ public class ComputerManagerService extends Service { } } - private boolean pollComputer(ComputerDetails details, boolean localFirst) { + // Just try to establish a TCP connection to speculatively detect a running + // GFE server + private boolean fastPollIp(InetAddress addr) { + Socket s = new Socket(); + try { + s.connect(new InetSocketAddress(addr, NvHTTP.PORT), FAST_POLL_TIMEOUT); + s.close(); + return true; + } catch (IOException e) { + return false; + } + } + + private void startFastPollThread(final InetAddress addr, final boolean[] info) { + Thread t = new Thread() { + @Override + public void run() { + boolean pollRes = fastPollIp(addr); + + synchronized (info) { + info[0] = true; // Done + info[1] = pollRes; // Polling result + + info.notify(); + } + } + }; + t.setName("Fast Poll - "+addr.getHostAddress()); + t.start(); + } + + private ComputerDetails.Reachability fastPollPc(final InetAddress local, final InetAddress remote) throws InterruptedException { + final boolean[] remoteInfo = new boolean[2]; + final boolean[] localInfo = new boolean[2]; + + startFastPollThread(local, localInfo); + startFastPollThread(remote, remoteInfo); + + // Check local first + synchronized (localInfo) { + while (!localInfo[0]) { + localInfo.wait(500); + } + + if (localInfo[1]) { + return ComputerDetails.Reachability.LOCAL; + } + } + + // Now remote + synchronized (remoteInfo) { + while (!remoteInfo[0]) { + remoteInfo.wait(500); + } + + if (remoteInfo[1]) { + return ComputerDetails.Reachability.REMOTE; + } + } + + return ComputerDetails.Reachability.OFFLINE; + } + + private boolean pollComputer(ComputerDetails details) throws InterruptedException { ComputerDetails polledDetails; + ComputerDetails.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)) { - localFirst = false; + /*if (details.localIp.equals(details.remoteIp)) { + reachability = ComputerDetails.Reachability.REMOTE; } + else*/ { + // Do a TCP-level connection to the HTTP server to see if it's listening + LimeLog.info("Starting fast poll for "+details.name+" ("+details.localIp+", "+details.remoteIp+")"); + reachability = fastPollPc(details.localIp, details.remoteIp); + LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString()); + } + + // If no connection could be established to either IP address, there's nothing we can do + if (reachability == ComputerDetails.Reachability.OFFLINE) { + return false; + } + + boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL); if (localFirst) { polledDetails = tryPollIp(details, details.localIp); @@ -402,24 +490,19 @@ 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); - 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); + // 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")) { + LimeLog.info("MAC address was empty; using existing value: "+savedMacAddress); + details.macAddress = savedMacAddress; } + + return true; } @Override From 59df38ae8adad9e5a351fa308ccc0efbde73460b Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 22 Feb 2015 17:49:52 -0500 Subject: [PATCH 024/202] Cancel app icon requests when the view is recycled --- .../com/limelight/grid/AppGridAdapter.java | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index b2ca369e..2056c320 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -48,7 +48,8 @@ public class AppGridAdapter extends GenericGridAdapter { private final String uniqueId; private final LimelightCryptoProvider cryptoProvider; private final SSLContext sslContext; - private final HashMap pendingRequests = new HashMap(); + private final HashMap pendingIonRequests = new HashMap(); + private final HashMap pendingCacheRequests = new HashMap(); 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); @@ -132,9 +133,9 @@ public class AppGridAdapter extends GenericGridAdapter { public void abortPendingRequests() { HashMap tempMap; - synchronized (pendingRequests) { + synchronized (pendingIonRequests) { // Copy the pending requests under a lock - tempMap = new HashMap(pendingRequests); + tempMap = new HashMap(pendingIonRequests); } for (Future f : tempMap.values()) { @@ -143,10 +144,10 @@ public class AppGridAdapter extends GenericGridAdapter { } } - synchronized (pendingRequests) { + synchronized (pendingIonRequests) { // Remove cancelled requests for (ImageView v : tempMap.keySet()) { - pendingRequests.remove(v); + pendingIonRequests.remove(v); } } } @@ -165,11 +166,31 @@ public class AppGridAdapter extends GenericGridAdapter { @Override public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) { + // Cancel any pending cache requests for this view + synchronized (pendingCacheRequests) { + ImageCacheRequest req = pendingCacheRequests.remove(imgView); + if (req != null) { + req.cancel(false); + } + } + + // Cancel any pending Ion requests for this view + synchronized (pendingIonRequests) { + Future f = pendingIonRequests.remove(imgView); + if (f != null && !f.isCancelled() && !f.isDone()) { + f.cancel(true); + } + } + // Clear existing contents of the image view imgView.setAlpha(0.0f); // Check the on-disk cache - new ImageCacheRequest(imgView, obj.app.getAppId()).execute(); + ImageCacheRequest req = new ImageCacheRequest(imgView, obj.app.getAppId()); + synchronized (pendingCacheRequests) { + pendingCacheRequests.put(imgView, req); + } + req.execute(); return true; } @@ -228,6 +249,13 @@ public class AppGridAdapter extends GenericGridAdapter { @Override protected void onPostExecute(Bitmap result) { + // Check if the cache request is still live + synchronized (pendingCacheRequests) { + if (pendingCacheRequests.remove(view) == null) { + return; + } + } + if (result != null) { // Disk cache was read successfully LimeLog.info("Image disk cache hit for (" + computer.uuid + ", " + appId + ")"); @@ -249,7 +277,7 @@ public class AppGridAdapter extends GenericGridAdapter { Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setHostnameVerifier(hv); // Kick off the deferred image load - synchronized (pendingRequests) { + synchronized (pendingIonRequests) { Future f = Ion.with(context) .load("https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" + appId + "&AssetType=2&AssetIdx=0") @@ -257,8 +285,11 @@ public class AppGridAdapter extends GenericGridAdapter { .setCallback(new FutureCallback() { @Override public void onCompleted(Exception e, final Bitmap result) { - synchronized (pendingRequests) { - pendingRequests.remove(view); + synchronized (pendingIonRequests) { + // Don't set this image if the request was cancelled + if (pendingIonRequests.remove(view) == null) { + return; + } } if (result != null) { @@ -281,8 +312,8 @@ public class AppGridAdapter extends GenericGridAdapter { } } }); - pendingRequests.put(view, f); - } + pendingIonRequests.put(view, f); + } } } } From bf795ab7a51f2bee3f2055a5152ba2a32e928a41 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 22 Feb 2015 18:04:42 -0500 Subject: [PATCH 025/202] Fix removal of apps in app list updates --- app/src/main/java/com/limelight/AppView.java | 21 +++++++++++++++++++ .../com/limelight/grid/AppGridAdapter.java | 4 ++++ 2 files changed, 25 insertions(+) diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index ea838010..f160181c 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -366,6 +366,7 @@ 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; @@ -395,6 +396,26 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { } } + // Next handle app removals + for (int i = 0; i < appGridAdapter.getCount(); i++) { + 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; + } + } + if (updated) { appGridAdapter.notifyDataSetChanged(); } diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index 2056c320..477a1a99 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -130,6 +130,10 @@ public class AppGridAdapter extends GenericGridAdapter { sortList(); } + public void removeApp(AppView.AppObject app) { + itemList.remove(app); + } + public void abortPendingRequests() { HashMap tempMap; From 0b7becb1614c148eba4801025ac813d104810096 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 22 Feb 2015 18:10:08 -0500 Subject: [PATCH 026/202] Remove unused function --- .../com/limelight/grid/AppGridAdapter.java | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index 477a1a99..0ff2f89e 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -134,28 +134,6 @@ public class AppGridAdapter extends GenericGridAdapter { itemList.remove(app); } - public void abortPendingRequests() { - HashMap tempMap; - - synchronized (pendingIonRequests) { - // Copy the pending requests under a lock - tempMap = new HashMap(pendingIonRequests); - } - - for (Future f : tempMap.values()) { - if (!f.isCancelled() && !f.isDone()) { - f.cancel(true); - } - } - - synchronized (pendingIonRequests) { - // Remove cancelled requests - for (ImageView v : tempMap.keySet()) { - pendingIonRequests.remove(v); - } - } - } - // TODO: Handle pruning of bitmap cache private void populateBitmapCache(UUID uuid, int appId, Bitmap bitmap) { try { From e222f2f6c3b4ef7875a67273df0a72c8677a0866 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 22 Feb 2015 18:34:28 -0500 Subject: [PATCH 027/202] Fix fast polling --- .../com/limelight/computers/ComputerManagerService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index 78262358..72f4a3bb 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -118,7 +118,7 @@ public class ComputerManagerService extends Service { while (!isInterrupted() && pollingActive) { try { // Check if this poll has modified the details - pollComputer(details); + runPoll(details, false); // Wait until the next polling interval Thread.sleep(POLLING_PERIOD_MS); @@ -293,7 +293,7 @@ public class ComputerManagerService extends Service { // Block while we try to fill the details try { - pollComputer(fakeDetails); + runPoll(fakeDetails, true); } catch (InterruptedException e) { return false; } @@ -441,10 +441,10 @@ public class ComputerManagerService extends Service { // If the local address is routable across the Internet, // always consider this PC remote to be conservative - /*if (details.localIp.equals(details.remoteIp)) { + if (details.localIp.equals(details.remoteIp)) { reachability = ComputerDetails.Reachability.REMOTE; } - else*/ { + 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); From ee58071ff176a07c97a3b33931555d57a5149ca7 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Wed, 25 Feb 2015 21:07:35 -0500 Subject: [PATCH 028/202] Fix huge performance issues when dealing with large app lists --- app/src/main/java/com/limelight/AppView.java | 13 +- .../com/limelight/grid/AppGridAdapter.java | 290 ++++-------------- .../grid/assets/CachedAppAssetLoader.java | 156 ++++++++++ .../grid/assets/DiskAssetLoader.java | 56 ++++ .../grid/assets/MemoryAssetLoader.java | 35 +++ .../grid/assets/NetworkAssetLoader.java | 120 ++++++++ 6 files changed, 439 insertions(+), 231 deletions(-) create mode 100644 app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java create mode 100644 app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java create mode 100644 app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java create mode 100644 app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index f160181c..89e6891c 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -13,6 +13,7 @@ import com.limelight.computers.ComputerManagerListener; import com.limelight.computers.ComputerManagerService; import com.limelight.grid.AppGridAdapter; import com.limelight.nvstream.http.ComputerDetails; +import com.limelight.nvstream.http.GfeHttpResponseException; import com.limelight.nvstream.http.NvApp; import com.limelight.nvstream.http.NvHTTP; import com.limelight.preferences.PreferenceConfiguration; @@ -446,10 +447,18 @@ 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(); + 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()+")"; } else { - message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName(); + message = e.getMessage(); } } catch (UnknownHostException e) { message = getResources().getString(R.string.error_unknown_host); diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index 0ff2f89e..bf6b9144 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -1,112 +1,38 @@ package com.limelight.grid; -import android.content.Context; +import android.app.Activity; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.os.AsyncTask; import android.widget.ImageView; import android.widget.TextView; -import com.koushikdutta.async.future.FutureCallback; -import com.koushikdutta.ion.Ion; import com.limelight.AppView; -import com.limelight.LimeLog; import com.limelight.R; -import com.limelight.binding.PlatformBinding; +import com.limelight.grid.assets.CachedAppAssetLoader; +import com.limelight.grid.assets.DiskAssetLoader; +import com.limelight.grid.assets.MemoryAssetLoader; +import com.limelight.grid.assets.NetworkAssetLoader; import com.limelight.nvstream.http.ComputerDetails; -import com.limelight.nvstream.http.LimelightCryptoProvider; -import com.limelight.utils.CacheHelper; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.InetAddress; -import java.net.Socket; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; -import java.security.Principal; -import java.security.PrivateKey; -import java.security.SecureRandom; import java.util.Collections; import java.util.Comparator; -import java.util.HashMap; -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; +import java.util.concurrent.ConcurrentHashMap; public class AppGridAdapter extends GenericGridAdapter { + private final Activity activity; - private final ComputerDetails computer; - private final String uniqueId; - private final LimelightCryptoProvider cryptoProvider; - private final SSLContext sslContext; - private final HashMap pendingIonRequests = new HashMap(); - private final HashMap pendingCacheRequests = new HashMap(); + private final CachedAppAssetLoader loader; + private final ConcurrentHashMap loadingTuples = 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; - - cryptoProvider = PlatformBinding.getCryptoProvider(context); - - sslContext = SSLContext.getInstance("SSL"); - sslContext.init(ourKeyman, trustAllCerts, new SecureRandom()); + this.activity = activity; + this.loader = new CachedAppAssetLoader(computer, uniqueId, new NetworkAssetLoader(context), + new MemoryAssetLoader(), new DiskAssetLoader(context.getCacheDir())); } - private final TrustManager[] trustAllCerts = new TrustManager[] { - new X509TrustManager() { - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } - public void checkClientTrusted(X509Certificate[] certs, String authType) {} - public void checkServerTrusted(X509Certificate[] certs, String authType) {} - }}; - - private final KeyManager[] ourKeyman = new KeyManager[] { - new X509KeyManager() { - public String chooseClientAlias(String[] keyTypes, - Principal[] issuers, Socket socket) { - return "Limelight-RSA"; - } - - public String chooseServerAlias(String keyType, Principal[] issuers, - Socket socket) { - return null; - } - - public X509Certificate[] getCertificateChain(String alias) { - return new X509Certificate[] {cryptoProvider.getClientCertificate()}; - } - - public String[] getClientAliases(String keyType, Principal[] issuers) { - return null; - } - - public PrivateKey getPrivateKey(String alias) { - return cryptoProvider.getClientPrivateKey(); - } - - public String[] getServerAliases(String keyType, Principal[] issuers) { - return null; - } - } - }; - - // Ignore differences between given hostname and certificate hostname - HostnameVerifier hv = new HostnameVerifier() { - public boolean verify(String hostname, SSLSession session) { return true; } - }; - private void sortList() { Collections.sort(itemList, new Comparator() { @Override @@ -116,15 +42,6 @@ public class AppGridAdapter extends GenericGridAdapter { }); } - 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(); @@ -134,46 +51,59 @@ public class AppGridAdapter extends GenericGridAdapter { itemList.remove(app); } - // 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(); - } - } + private final CachedAppAssetLoader.LoadListener loadListener = new CachedAppAssetLoader.LoadListener() { + @Override + public void notifyLongLoad(Object object) { + final ImageView view = (ImageView) object; + + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + view.setImageResource(R.drawable.image_loading); + fadeInImage(view); + } + }); + } + + @Override + public void notifyLoadComplete(Object object, final Bitmap bitmap) { + final ImageView view = (ImageView) object; + + loadingTuples.remove(view); + + // Just leave the loading icon in place + if (bitmap == null) { + return; + } + + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + view.setImageBitmap(bitmap); + fadeInImage(view); + } + }); + } + }; - @Override public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) { - // Cancel any pending cache requests for this view - synchronized (pendingCacheRequests) { - ImageCacheRequest req = pendingCacheRequests.remove(imgView); - if (req != null) { - req.cancel(false); - } - } - - // Cancel any pending Ion requests for this view - synchronized (pendingIonRequests) { - Future f = pendingIonRequests.remove(imgView); - if (f != null && !f.isCancelled() && !f.isDone()) { - f.cancel(true); - } + // Cancel pending loads on this image view + CachedAppAssetLoader.LoaderTuple tuple = loadingTuples.remove(imgView); + if (tuple != null) { + // 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 + tuple.cancel(); } // Clear existing contents of the image view imgView.setAlpha(0.0f); - // Check the on-disk cache - ImageCacheRequest req = new ImageCacheRequest(imgView, obj.app.getAppId()); - synchronized (pendingCacheRequests) { - pendingCacheRequests.put(imgView, req); + // Start loading the bitmap + tuple = loader.loadBitmapWithContext(obj.app, imgView, loadListener); + if (tuple != null) { + // The load was issued asynchronously + loadingTuples.put(imgView, tuple); } - req.execute(); - return true; } @@ -198,105 +128,7 @@ public class AppGridAdapter extends GenericGridAdapter { return false; } - private class ImageCacheRequest extends AsyncTask { - private final ImageView view; - private final int appId; - - public ImageCacheRequest(ImageView view, int appId) { - this.view = view; - this.appId = appId; - } - - @Override - protected Bitmap doInBackground(Void... v) { - InputStream in = null; - try { - in = CacheHelper.openCacheFileForInput(context.getCacheDir(), "boxart", computer.uuid.toString(), appId + ".png"); - return BitmapFactory.decodeStream(in); - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (in != null) { - try { - in.close(); - } catch (IOException ignored) {} - } - } - return null; - } - - private void fadeInImage(ImageView view) { - view.animate().alpha(1.0f).setDuration(250).start(); - } - - @Override - protected void onPostExecute(Bitmap result) { - // Check if the cache request is still live - synchronized (pendingCacheRequests) { - if (pendingCacheRequests.remove(view) == null) { - return; - } - } - - if (result != null) { - // Disk cache was read successfully - LimeLog.info("Image disk cache hit for (" + computer.uuid + ", " + appId + ")"); - view.setImageBitmap(result); - fadeInImage(view); - } - else { - LimeLog.info("Image disk cache miss for ("+computer.uuid+", "+appId+")"); - LimeLog.info("Requesting: "+"https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" + - appId + "&AssetType=2&AssetIdx=0"); - - // Load the placeholder image - view.setImageResource(defaultImageRes); - fadeInImage(view); - - // Set SSL contexts correctly to allow us to authenticate - Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts); - Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext); - Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setHostnameVerifier(hv); - - // Kick off the deferred image load - synchronized (pendingIonRequests) { - Future f = Ion.with(context) - .load("https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" + - appId + "&AssetType=2&AssetIdx=0") - .asBitmap() - .setCallback(new FutureCallback() { - @Override - public void onCompleted(Exception e, final Bitmap result) { - synchronized (pendingIonRequests) { - // Don't set this image if the request was cancelled - if (pendingIonRequests.remove(view) == null) { - return; - } - } - - if (result != null) { - // Make the view visible now - view.setImageBitmap(result); - fadeInImage(view); - - // Populate the disk cache if we got an image back. - // We do it in a new thread because it can be very expensive, especially - // when we do the initial load where lots of disk I/O is happening at once. - new Thread() { - @Override - public void run() { - populateBitmapCache(computer.uuid, appId, result); - } - }.start(); - } - else { - // Leave the loading icon as is (probably should change this eventually...) - } - } - }); - pendingIonRequests.put(view, f); - } - } - } + private static void fadeInImage(ImageView view) { + view.animate().alpha(1.0f).setDuration(100).start(); } } diff --git a/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java new file mode 100644 index 00000000..898ce908 --- /dev/null +++ b/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java @@ -0,0 +1,156 @@ +package com.limelight.grid.assets; + +import android.graphics.Bitmap; + +import com.limelight.nvstream.http.ComputerDetails; +import com.limelight.nvstream.http.NvApp; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class CachedAppAssetLoader { + private final ComputerDetails computer; + private final String uniqueId; + private final ThreadPoolExecutor executor = new ThreadPoolExecutor(8, 8, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue()); + private final NetworkLoader networkLoader; + private final CachedLoader memoryLoader; + private final CachedLoader diskLoader; + + public CachedAppAssetLoader(ComputerDetails computer, String uniqueId, NetworkLoader networkLoader, CachedLoader memoryLoader, CachedLoader diskLoader) { + this.computer = computer; + this.uniqueId = uniqueId; + + this.networkLoader = networkLoader; + this.memoryLoader = memoryLoader; + this.diskLoader = diskLoader; + } + + 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); + 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; + } + + bmp = networkLoader.loadBitmap(tuple); + if (bmp != null) { + break; + } + + // Wait 1 second with a bit of fuzz + try { + Thread.sleep((int) (1000 + (Math.random()*500))); + } catch (InterruptedException e) {} + } + + if (bmp != null) { + // Populate the disk cache + diskLoader.populateCache(tuple, bmp); + } + } + + 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) { + LoaderTuple tuple = new LoaderTuple(computer, uniqueId, app); + + // First, try the memory cache in the current context + Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple); + if (bmp != null) { + synchronized (tuple) { + if (tuple.cancelled) { + return null; + } + else { + tuple.notified = true; + } + } + + listener.notifyLoadComplete(context, bmp); + return null; + } + + // If it's not in memory, throw this in our executor + executor.execute(createLoaderRunnable(tuple, context, listener)); + return tuple; + } + + public class LoaderTuple { + public final ComputerDetails computer; + public final String uniqueId; + public final NvApp app; + + public boolean notified; + public boolean cancelled; + + public LoaderTuple(ComputerDetails computer, String uniqueId, NvApp app) { + this.computer = computer; + this.uniqueId = uniqueId; + this.app = app; + } + + public boolean cancel() { + synchronized (this) { + cancelled = true; + return !notified; + } + } + + @Override + public String toString() { + return "("+computer.uuid+", "+app.getAppId()+")"; + } + } + + public interface NetworkLoader { + public Bitmap loadBitmap(LoaderTuple tuple); + } + + public interface CachedLoader { + public Bitmap loadBitmapFromCache(LoaderTuple tuple); + public void populateCache(LoaderTuple tuple, Bitmap bitmap); + } + + 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); + } +} diff --git a/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java new file mode 100644 index 00000000..6845395b --- /dev/null +++ b/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java @@ -0,0 +1,56 @@ +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.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +public class DiskAssetLoader implements CachedAppAssetLoader.CachedLoader { + private final File cacheDir; + + public DiskAssetLoader(File cacheDir) { + this.cacheDir = cacheDir; + } + + @Override + public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) { + InputStream in = null; + Bitmap bmp = null; + try { + in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png"); + bmp = BitmapFactory.decodeStream(in); + } 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; + } + + @Override + public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) { + try { + // PNG ignores quality setting + FileOutputStream out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png"); + bitmap.compress(Bitmap.CompressFormat.PNG, 0, out); + out.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java new file mode 100644 index 00000000..c343e939 --- /dev/null +++ b/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java @@ -0,0 +1,35 @@ +package com.limelight.grid.assets; + +import android.graphics.Bitmap; +import android.util.LruCache; + +import com.limelight.LimeLog; + +public class MemoryAssetLoader implements CachedAppAssetLoader.CachedLoader { + private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); + private static final LruCache memoryCache = new LruCache(maxMemory / 4) { + @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(); + } + + @Override + 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; + } + + @Override + public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) { + memoryCache.put(constructKey(tuple), bitmap); + } +} diff --git a/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java new file mode 100644 index 00000000..117f2b66 --- /dev/null +++ b/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java @@ -0,0 +1,120 @@ +package com.limelight.grid.assets; + +import android.content.Context; +import android.graphics.Bitmap; + +import com.koushikdutta.ion.Ion; +import com.limelight.LimeLog; +import com.limelight.binding.PlatformBinding; +import com.limelight.nvstream.http.ComputerDetails; +import com.limelight.nvstream.http.LimelightCryptoProvider; + +import java.net.InetAddress; +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; + +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; + +public class NetworkAssetLoader implements CachedAppAssetLoader.NetworkLoader { + private final Context context; + private final LimelightCryptoProvider cryptoProvider; + private final SSLContext sslContext; + + public NetworkAssetLoader(Context context) throws NoSuchAlgorithmException, KeyManagementException { + this.context = context; + + cryptoProvider = PlatformBinding.getCryptoProvider(context); + + sslContext = SSLContext.getInstance("SSL"); + sslContext.init(ourKeyman, trustAllCerts, new SecureRandom()); + } + + private final TrustManager[] trustAllCerts = new TrustManager[] { + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + public void checkClientTrusted(X509Certificate[] certs, String authType) {} + public void checkServerTrusted(X509Certificate[] certs, String authType) {} + }}; + + private final KeyManager[] ourKeyman = new KeyManager[] { + new X509KeyManager() { + public String chooseClientAlias(String[] keyTypes, + Principal[] issuers, Socket socket) { + return "Limelight-RSA"; + } + + public String chooseServerAlias(String keyType, Principal[] issuers, + Socket socket) { + return null; + } + + public X509Certificate[] getCertificateChain(String alias) { + return new X509Certificate[] {cryptoProvider.getClientCertificate()}; + } + + public String[] getClientAliases(String keyType, Principal[] issuers) { + return null; + } + + public PrivateKey getPrivateKey(String alias) { + return cryptoProvider.getClientPrivateKey(); + } + + public String[] getServerAliases(String keyType, Principal[] issuers) { + return null; + } + } + }; + + // Ignore differences between given hostname and certificate hostname + private final HostnameVerifier hv = new HostnameVerifier() { + public boolean verify(String hostname, SSLSession session) { return true; } + }; + + @Override + public Bitmap loadBitmap(CachedAppAssetLoader.LoaderTuple tuple) { + // Set SSL contexts correctly to allow us to authenticate + Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts); + Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext); + Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setHostnameVerifier(hv); + + Bitmap bmp = Ion.with(context) + .load("https://" + getCurrentAddress(tuple.computer).getHostAddress() + ":47984/appasset?uniqueid=" + + tuple.uniqueId + "&appid=" + tuple.app.getAppId() + "&AssetType=2&AssetIdx=0") + .asBitmap() + .tryGet(); + if (bmp != null) { + LimeLog.info("Network asset load complete: " + tuple); + + // Scale the bitmap to half size + bmp = Bitmap.createScaledBitmap(bmp, bmp.getWidth() / 2, bmp.getHeight() / 2, true); + } + else { + LimeLog.info("Network asset load failed: " + tuple); + } + + return bmp; + } + + private static InetAddress getCurrentAddress(ComputerDetails computer) { + if (computer.reachability == ComputerDetails.Reachability.LOCAL) { + return computer.localIp; + } + else { + return computer.remoteIp; + } + } +} From 2681036c32c18163a1a285d685573c2f00c57f4e Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Wed, 25 Feb 2015 21:07:44 -0500 Subject: [PATCH 029/202] Update common --- app/libs/limelight-common.jar | Bin 958781 -> 952778 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/libs/limelight-common.jar b/app/libs/limelight-common.jar index af46940fc72f96fa474bb4ec26a102815dd7f20c..e368c6e8bd664f3af2344e02feb73706b4f62bb2 100644 GIT binary patch delta 24147 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�}Oq<2Hd+&JU0W0v0~*VIo_mUQ!9Ox>|Nhb{N| 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 delta 29827 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
s$y7HOx&@2*Zz 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 From 90209f2ca2d827642c7f9131fc0a41fc9bb5e9d8 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Wed, 25 Feb 2015 21:07:59 -0500 Subject: [PATCH 030/202] Update iml files generated by Android Studio 1.1 --- app/app.iml | 4 +++- limelight-android.iml | 6 ++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/app.iml b/app/app.iml index f7cfabed..87814d7a 100644 --- a/app/app.iml +++ b/app/app.iml @@ -1,5 +1,5 @@ - + @@ -9,6 +9,7 @@ + diff --git a/limelight-android.iml b/limelight-android.iml index 2a022014..0bb6048a 100644 --- a/limelight-android.iml +++ b/limelight-android.iml @@ -1,5 +1,5 @@ - + @@ -7,9 +7,7 @@ - - - + From 833b7c3916e1394524335452dc54ed7f72f970b5 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Wed, 25 Feb 2015 21:57:54 -0500 Subject: [PATCH 031/202] Fetch app assets in the background while in the app view --- .../com/limelight/grid/AppGridAdapter.java | 15 +++++++++++++-- .../grid/assets/CachedAppAssetLoader.java | 18 ++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index bf6b9144..31ca74fb 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -43,6 +43,9 @@ public class AppGridAdapter extends GenericGridAdapter { } public void addApp(AppView.AppObject app) { + // Queue a request to fetch this bitmap in the background + loader.loadBitmapWithContextInBackground(app.app, null, backgroundLoadListener); + itemList.add(app); sortList(); } @@ -51,7 +54,7 @@ public class AppGridAdapter extends GenericGridAdapter { itemList.remove(app); } - private final CachedAppAssetLoader.LoadListener loadListener = new CachedAppAssetLoader.LoadListener() { + private final CachedAppAssetLoader.LoadListener imageViewLoadListener = new CachedAppAssetLoader.LoadListener() { @Override public void notifyLongLoad(Object object) { final ImageView view = (ImageView) object; @@ -86,6 +89,14 @@ public class AppGridAdapter extends GenericGridAdapter { } }; + private final CachedAppAssetLoader.LoadListener backgroundLoadListener = new CachedAppAssetLoader.LoadListener() { + @Override + public void notifyLongLoad(Object object) {} + + @Override + public void notifyLoadComplete(Object object, final Bitmap bitmap) {} + }; + public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) { // Cancel pending loads on this image view CachedAppAssetLoader.LoaderTuple tuple = loadingTuples.remove(imgView); @@ -99,7 +110,7 @@ public class AppGridAdapter extends GenericGridAdapter { imgView.setAlpha(0.0f); // Start loading the bitmap - tuple = loader.loadBitmapWithContext(obj.app, imgView, loadListener); + tuple = loader.loadBitmapWithContext(obj.app, imgView, imageViewLoadListener); if (tuple != null) { // The load was issued asynchronously loadingTuples.put(imgView, tuple); diff --git a/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java index 898ce908..172b05c2 100644 --- a/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java @@ -12,7 +12,8 @@ import java.util.concurrent.TimeUnit; public class CachedAppAssetLoader { private final ComputerDetails computer; private final String uniqueId; - private final ThreadPoolExecutor executor = new ThreadPoolExecutor(8, 8, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue()); + private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(8, 8, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue()); + private final ThreadPoolExecutor backgroundExecutor = new ThreadPoolExecutor(2, 2, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue()); private final NetworkLoader networkLoader; private final CachedLoader memoryLoader; private final CachedLoader diskLoader; @@ -86,6 +87,14 @@ public class CachedAppAssetLoader { } 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, uniqueId, app); // First, try the memory cache in the current context @@ -105,7 +114,12 @@ public class CachedAppAssetLoader { } // If it's not in memory, throw this in our executor - executor.execute(createLoaderRunnable(tuple, context, listener)); + if (background) { + backgroundExecutor.execute(createLoaderRunnable(tuple, context, listener)); + } + else { + foregroundExecutor.execute(createLoaderRunnable(tuple, context, listener)); + } return tuple; } From cc3f2ecb075c48130637ce4813e11fa9d8c9d948 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Wed, 25 Feb 2015 22:15:41 -0500 Subject: [PATCH 032/202] Always close the cache output stream if an exception occurs --- .../computers/ComputerManagerService.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index 72f4a3bb..a9c0873a 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -612,9 +612,19 @@ public class ComputerManagerService extends Service { List list = NvHTTP.getAppListByReader(new StringReader(appList)); if (appList != null && !appList.isEmpty() && !list.isEmpty()) { // Open the cache file - FileOutputStream cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString()); - CacheHelper.writeStringToOutputStream(cacheOut, appList); - cacheOut.close(); + FileOutputStream cacheOut = null; + try { + 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; From 7838a787df4ce4d8be3c68df41e02942658b5bc2 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Wed, 25 Feb 2015 22:27:38 -0500 Subject: [PATCH 033/202] Fix a bug in app removal --- app/src/main/java/com/limelight/AppView.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index 89e6891c..5ec10b46 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -398,7 +398,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { } // Next handle app removals - for (int i = 0; i < appGridAdapter.getCount(); i++) { + int i = 0; + while (i < appGridAdapter.getCount()) { boolean foundExistingApp = false; AppObject existingApp = (AppObject) appGridAdapter.getItem(i); @@ -414,7 +415,14 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { 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) { From f2b8461bb9eac3daf50920b601cd2a1cf6793317 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Wed, 25 Feb 2015 23:15:10 -0500 Subject: [PATCH 034/202] Increment version to beta 3.1.2 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d88154d1..bff822c5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { minSdkVersion 16 targetSdkVersion 21 - versionName "3.1.1" - versionCode = 54 + versionName "3.1.2-beta1" + versionCode = 55 } productFlavors { From 95ea88e93291b55552043ffde31cfedad252ed62 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Thu, 26 Feb 2015 15:11:46 -0500 Subject: [PATCH 035/202] Only replace the MAC address if the existing one is non-null --- .../java/com/limelight/computers/ComputerManagerService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index a9c0873a..652d2c43 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -497,7 +497,7 @@ public class ComputerManagerService extends Service { 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")) { + 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; } From f1787c43e5f8e323190816281c0e210b6e73a313 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Thu, 26 Feb 2015 18:27:50 -0500 Subject: [PATCH 036/202] Generalize the polling grace period to all users of CMS --- app/src/main/java/com/limelight/AppView.java | 27 +++++++------------ .../computers/ComputerManagerService.java | 25 +++++++++++++---- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index 5ec10b46..05dcfddc 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -55,9 +55,6 @@ 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; @@ -133,25 +130,19 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { } if (details.state != ComputerDetails.State.ONLINE) { - consecutiveAppListFailures++; - - if (consecutiveAppListFailures >= CONSECUTIVE_FAILURE_LIMIT) { - // The PC is unreachable now - AppView.this.runOnUiThread(new Runnable() { - @Override - public void run() { - // Display a toast to the user and quit the activity - Toast.makeText(AppView.this, getResources().getText(R.string.lost_connection), Toast.LENGTH_SHORT).show(); - finish(); - } - }); - } + // The PC is unreachable now + AppView.this.runOnUiThread(new Runnable() { + @Override + public void run() { + // Display a toast to the user and quit the activity + Toast.makeText(AppView.this, getResources().getText(R.string.lost_connection), Toast.LENGTH_SHORT).show(); + finish(); + } + }); return; } - consecutiveAppListFailures = 0; - // App list is the same or empty; nothing to do if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) { return; diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index 652d2c43..56e8d157 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -2,6 +2,7 @@ package com.limelight.computers; import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStreamWriter; import java.io.StringReader; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -34,6 +35,7 @@ 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(); @@ -67,7 +69,7 @@ public class ComputerManagerService extends Service { }; // Returns true if the details object was modified - private boolean runPoll(ComputerDetails details, boolean newPc) throws InterruptedException { + private boolean runPoll(ComputerDetails details, boolean newPc, int offlineCount) throws InterruptedException { if (!getLocalDatabaseReference()) { return false; } @@ -77,6 +79,11 @@ public class ComputerManagerService extends Service { // Poll the machine try { if (!pollComputer(details)) { + if (!newPc && offlineCount < OFFLINE_POLL_TRIES) { + // Return without calling the listener + return false; + } + details.state = ComputerDetails.State.OFFLINE; details.reachability = ComputerDetails.Reachability.OFFLINE; } @@ -95,7 +102,7 @@ public class ComputerManagerService extends Service { if (dbManager.getComputerByName(details.name) == null) { // It's gone releaseLocalDatabaseReference(); - return true; + return false; } } @@ -115,13 +122,21 @@ 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 - runPoll(details, false); + if (!runPoll(details, false, offlineCount)) { + LimeLog.warning(details.name + " is offline (try " + offlineCount + ")"); + offlineCount++; + } + else { + offlineCount = 0; + } // Wait until the next polling interval - Thread.sleep(POLLING_PERIOD_MS); + Thread.sleep(POLLING_PERIOD_MS / ((offlineCount > 0) ? 2 : 1)); } catch (InterruptedException e) { break; } @@ -293,7 +308,7 @@ public class ComputerManagerService extends Service { // Block while we try to fill the details try { - runPoll(fakeDetails, true); + runPoll(fakeDetails, true, 0); } catch (InterruptedException e) { return false; } From 1b8d2bc81c6d9641bc7d3750ad173bc3958d3bae Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Thu, 26 Feb 2015 18:30:02 -0500 Subject: [PATCH 037/202] Cancel asset fetching when the app view is paused --- app/src/main/java/com/limelight/AppView.java | 4 +++ .../com/limelight/grid/AppGridAdapter.java | 28 +++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index 05dcfddc..6e263d4c 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -174,6 +174,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { if (managerBinder != null) { managerBinder.stopPolling(); } + + if (appGridAdapter != null) { + appGridAdapter.cancelQueuedOperations(); + } } @Override diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index 31ca74fb..668e9b30 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -15,6 +15,7 @@ import com.limelight.nvstream.http.ComputerDetails; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.concurrent.ConcurrentHashMap; @@ -24,6 +25,7 @@ public class AppGridAdapter extends GenericGridAdapter { private final CachedAppAssetLoader loader; private final ConcurrentHashMap loadingTuples = new ConcurrentHashMap<>(); + private final ConcurrentHashMap backgroundLoadingTuples = new ConcurrentHashMap<>(); 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); @@ -33,6 +35,21 @@ public class AppGridAdapter extends GenericGridAdapter { new MemoryAssetLoader(), new DiskAssetLoader(context.getCacheDir())); } + private static void cancelTuples(ConcurrentHashMap map) { + Collection tuples = map.values(); + + for (CachedAppAssetLoader.LoaderTuple tuple : tuples) { + tuple.cancel(); + } + + map.clear(); + } + + public void cancelQueuedOperations() { + cancelTuples(loadingTuples); + cancelTuples(backgroundLoadingTuples); + } + private void sortList() { Collections.sort(itemList, new Comparator() { @Override @@ -44,7 +61,12 @@ public class AppGridAdapter extends GenericGridAdapter { public void addApp(AppView.AppObject app) { // Queue a request to fetch this bitmap in the background - loader.loadBitmapWithContextInBackground(app.app, null, backgroundLoadListener); + Object tupleKey = new Object(); + CachedAppAssetLoader.LoaderTuple tuple = + loader.loadBitmapWithContextInBackground(app.app, tupleKey, backgroundLoadListener); + if (tuple != null) { + backgroundLoadingTuples.put(tupleKey, tuple); + } itemList.add(app); sortList(); @@ -94,7 +116,9 @@ public class AppGridAdapter extends GenericGridAdapter { public void notifyLongLoad(Object object) {} @Override - public void notifyLoadComplete(Object object, final Bitmap bitmap) {} + public void notifyLoadComplete(Object object, final Bitmap bitmap) { + backgroundLoadingTuples.remove(object); + } }; public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) { From 157450e674def1f5fc7e44fd714e0d4f5d7d7fce Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Thu, 26 Feb 2015 18:33:18 -0500 Subject: [PATCH 038/202] Update common with stricter applist parser --- app/libs/limelight-common.jar | Bin 952778 -> 952898 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/libs/limelight-common.jar b/app/libs/limelight-common.jar index e368c6e8bd664f3af2344e02feb73706b4f62bb2..47febbf9117b0bcb547baccff8d69d641a9007bb 100644 GIT binary patch delta 8142 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'JYr6h@%j=onWf%Q+ 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 Date: Thu, 26 Feb 2015 21:04:17 -0500 Subject: [PATCH 039/202] Always close the output stream --- .../com/limelight/grid/assets/DiskAssetLoader.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java index 6845395b..fd5a313c 100644 --- a/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java @@ -44,13 +44,19 @@ public class DiskAssetLoader implements CachedAppAssetLoader.CachedLoader { @Override public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) { + FileOutputStream out = null; try { // PNG ignores quality setting - FileOutputStream out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png"); + out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png"); bitmap.compress(Bitmap.CompressFormat.PNG, 0, out); - out.close(); } catch (IOException e) { e.printStackTrace(); + } finally { + if (out != null) { + try { + out.close(); + } catch (IOException ignored) {} + } } } } From c5293ef21f22e4f869db4c65d29fa748c9462623 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Thu, 26 Feb 2015 21:04:40 -0500 Subject: [PATCH 040/202] Reduce the size of the LRU cache by 2 --- .../main/java/com/limelight/grid/assets/MemoryAssetLoader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java index c343e939..4fa26fca 100644 --- a/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java @@ -7,7 +7,7 @@ import com.limelight.LimeLog; public class MemoryAssetLoader implements CachedAppAssetLoader.CachedLoader { private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); - private static final LruCache memoryCache = new LruCache(maxMemory / 4) { + private static final LruCache memoryCache = new LruCache(maxMemory / 8) { @Override protected int sizeOf(String key, Bitmap bitmap) { // Sizeof returns kilobytes From 98638186b565caf12e746274d8112068c379f951 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Thu, 26 Feb 2015 21:05:33 -0500 Subject: [PATCH 041/202] Use weak references to allow the image views to be garbage collected while a load is in progress --- .../com/limelight/grid/AppGridAdapter.java | 92 +++++++++++++------ 1 file changed, 63 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index 668e9b30..b4e87227 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -13,18 +13,21 @@ import com.limelight.grid.assets.MemoryAssetLoader; import com.limelight.grid.assets.NetworkAssetLoader; import com.limelight.nvstream.http.ComputerDetails; +import java.lang.ref.WeakReference; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.Iterator; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class AppGridAdapter extends GenericGridAdapter { private final Activity activity; private final CachedAppAssetLoader loader; - private final ConcurrentHashMap loadingTuples = new ConcurrentHashMap<>(); + private final ConcurrentHashMap, CachedAppAssetLoader.LoaderTuple> loadingTuples = new ConcurrentHashMap<>(); private final ConcurrentHashMap backgroundLoadingTuples = new ConcurrentHashMap<>(); public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws KeyManagementException, NoSuchAlgorithmException { @@ -79,33 +82,49 @@ public class AppGridAdapter extends GenericGridAdapter { private final CachedAppAssetLoader.LoadListener imageViewLoadListener = new CachedAppAssetLoader.LoadListener() { @Override public void notifyLongLoad(Object object) { - final ImageView view = (ImageView) object; + final WeakReference viewRef = (WeakReference) object; - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - view.setImageResource(R.drawable.image_loading); - fadeInImage(view); - } - }); - } - - @Override - public void notifyLoadComplete(Object object, final Bitmap bitmap) { - final ImageView view = (ImageView) object; - - loadingTuples.remove(view); - - // Just leave the loading icon in place - if (bitmap == null) { + // 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() { - view.setImageBitmap(bitmap); - fadeInImage(view); + ImageView view = viewRef.get(); + if (view != null) { + view.setImageResource(R.drawable.image_loading); + fadeInImage(view); + } + } + }); + } + + @Override + public void notifyLoadComplete(Object object, final Bitmap bitmap) { + final WeakReference viewRef = (WeakReference) 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); + } } }); } @@ -121,23 +140,38 @@ public class AppGridAdapter extends GenericGridAdapter { } }; + private void reapLoaderTuples(ImageView view) { + // Poor HashMap doesn't deserve this... + Iterator, CachedAppAssetLoader.LoaderTuple>> i = loadingTuples.entrySet().iterator(); + while (i.hasNext()) { + Map.Entry, 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 - CachedAppAssetLoader.LoaderTuple tuple = loadingTuples.remove(imgView); - if (tuple != null) { - // 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 - tuple.cancel(); - } + reapLoaderTuples(imgView); // Clear existing contents of the image view imgView.setAlpha(0.0f); // Start loading the bitmap - tuple = loader.loadBitmapWithContext(obj.app, imgView, imageViewLoadListener); + WeakReference viewRef = new WeakReference<>(imgView); + CachedAppAssetLoader.LoaderTuple tuple = loader.loadBitmapWithContext(obj.app, viewRef, imageViewLoadListener); if (tuple != null) { // The load was issued asynchronously - loadingTuples.put(imgView, tuple); + loadingTuples.put(viewRef, tuple); } return true; } From 010e03252e676a4f1cdd7e4113991835c055423a Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Thu, 26 Feb 2015 21:39:26 -0500 Subject: [PATCH 042/202] Encapsulate the cache IO streams in buffered streams --- .../limelight/computers/ComputerManagerService.java | 3 ++- .../com/limelight/grid/assets/DiskAssetLoader.java | 4 ++-- app/src/main/java/com/limelight/utils/CacheHelper.java | 10 ++++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index 56e8d157..06e3be56 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -2,6 +2,7 @@ package com.limelight.computers; import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.StringReader; import java.net.InetAddress; @@ -627,7 +628,7 @@ public class ComputerManagerService extends Service { List list = NvHTTP.getAppListByReader(new StringReader(appList)); if (appList != null && !appList.isEmpty() && !list.isEmpty()) { // Open the cache file - FileOutputStream cacheOut = null; + OutputStream cacheOut = null; try { cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString()); CacheHelper.writeStringToOutputStream(cacheOut, appList); diff --git a/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java index fd5a313c..219ca8a7 100644 --- a/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java @@ -7,9 +7,9 @@ import com.limelight.LimeLog; import com.limelight.utils.CacheHelper; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; public class DiskAssetLoader implements CachedAppAssetLoader.CachedLoader { private final File cacheDir; @@ -44,7 +44,7 @@ public class DiskAssetLoader implements CachedAppAssetLoader.CachedLoader { @Override public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) { - FileOutputStream out = null; + OutputStream out = null; try { // PNG ignores quality setting out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png"); diff --git a/app/src/main/java/com/limelight/utils/CacheHelper.java b/app/src/main/java/com/limelight/utils/CacheHelper.java index 1da135a4..4bbebd46 100644 --- a/app/src/main/java/com/limelight/utils/CacheHelper.java +++ b/app/src/main/java/com/limelight/utils/CacheHelper.java @@ -1,5 +1,7 @@ package com.limelight.utils; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -28,12 +30,12 @@ public class CacheHelper { return f; } - public static FileInputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException { - return new FileInputStream(openPath(false, root, path)); + public static InputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException { + return new BufferedInputStream(new FileInputStream(openPath(false, root, path))); } - public static FileOutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException { - return new FileOutputStream(openPath(true, root, path)); + public static OutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException { + return new BufferedOutputStream(new FileOutputStream(openPath(true, root, path))); } public static String readInputStreamToString(InputStream in) throws IOException { From 094d642739ce67969f757fff3744a0c145219869 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Thu, 26 Feb 2015 22:04:22 -0500 Subject: [PATCH 043/202] Stop scaling bitmaps down --- .../java/com/limelight/grid/assets/NetworkAssetLoader.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java index 117f2b66..3c17b4bf 100644 --- a/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java @@ -98,9 +98,6 @@ public class NetworkAssetLoader implements CachedAppAssetLoader.NetworkLoader { .tryGet(); if (bmp != null) { LimeLog.info("Network asset load complete: " + tuple); - - // Scale the bitmap to half size - bmp = Bitmap.createScaledBitmap(bmp, bmp.getWidth() / 2, bmp.getHeight() / 2, true); } else { LimeLog.info("Network asset load failed: " + tuple); From 194037ff415996302865f9d6b62a5dd416117538 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Thu, 26 Feb 2015 22:04:39 -0500 Subject: [PATCH 044/202] Clear the bitmap cache since it can get pretty large --- .../main/java/com/limelight/grid/assets/NetworkAssetLoader.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java index 3c17b4bf..0150b230 100644 --- a/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java @@ -90,6 +90,7 @@ public class NetworkAssetLoader implements CachedAppAssetLoader.NetworkLoader { Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts); Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext); Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setHostnameVerifier(hv); + Ion.getDefault(context).getBitmapCache().clear(); Bitmap bmp = Ion.with(context) .load("https://" + getCurrentAddress(tuple.computer).getHostAddress() + ":47984/appasset?uniqueid=" + From 80d8c5953efde1a579f67812a7403aefd418af87 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 27 Feb 2015 01:16:06 -0500 Subject: [PATCH 045/202] Rewrite the app art caching and fetching (again!) to finally address OOM problems and speed up art loading --- app/build.gradle | 4 - app/libs/limelight-common.jar | Bin 952898 -> 953008 bytes .../com/limelight/grid/AppGridAdapter.java | 31 +++++- .../grid/assets/CachedAppAssetLoader.java | 64 +++++++----- .../grid/assets/DiskAssetLoader.java | 15 ++- .../grid/assets/MemoryAssetLoader.java | 6 +- .../grid/assets/NetworkAssetLoader.java | 97 +++--------------- .../java/com/limelight/utils/CacheHelper.java | 9 ++ 8 files changed, 98 insertions(+), 128 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bff822c5..7eb77625 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -67,10 +67,6 @@ dependencies { compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51' compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51' - compile group: 'com.google.android', name: 'support-v4', version:'r7' - compile group: 'com.koushikdutta.ion', name: 'ion', version:'2.0.5' - compile group: 'com.google.code.gson', name: 'gson', version:'2.3.1' - compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.2.0' compile group: 'com.squareup.okio', name:'okio', version:'1.2.0' diff --git a/app/libs/limelight-common.jar b/app/libs/limelight-common.jar index 47febbf9117b0bcb547baccff8d69d641a9007bb..2bffce0616c1c3c0363d09f9ecfb5ddc58d36496 100644 GIT binary patch delta 10222 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

Uc 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

+=EvTctnE|m!{i=MM_Q>WegsY&S_O$h1-k*N7 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 diff --git a/app/src/main/java/com/limelight/binding/video/ConfigurableDecoderRenderer.java b/app/src/main/java/com/limelight/binding/video/ConfigurableDecoderRenderer.java index 071b2294..f65830ed 100644 --- a/app/src/main/java/com/limelight/binding/video/ConfigurableDecoderRenderer.java +++ b/app/src/main/java/com/limelight/binding/video/ConfigurableDecoderRenderer.java @@ -1,5 +1,6 @@ package com.limelight.binding.video; +import com.limelight.nvstream.av.DecodeUnit; import com.limelight.nvstream.av.video.VideoDecoderRenderer; import com.limelight.nvstream.av.video.VideoDepacketizer; @@ -55,6 +56,11 @@ public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer { return decoderRenderer.getCapabilities(); } + @Override + public void directSubmitDecodeUnit(DecodeUnit du) { + decoderRenderer.directSubmitDecodeUnit(du); + } + @Override public int getAverageDecoderLatency() { if (decoderRenderer != null) { diff --git a/app/src/main/java/com/limelight/binding/video/EnhancedDecoderRenderer.java b/app/src/main/java/com/limelight/binding/video/EnhancedDecoderRenderer.java index 56392d89..5343c381 100644 --- a/app/src/main/java/com/limelight/binding/video/EnhancedDecoderRenderer.java +++ b/app/src/main/java/com/limelight/binding/video/EnhancedDecoderRenderer.java @@ -2,6 +2,6 @@ package com.limelight.binding.video; import com.limelight.nvstream.av.video.VideoDecoderRenderer; -public abstract class EnhancedDecoderRenderer implements VideoDecoderRenderer { +public abstract class EnhancedDecoderRenderer extends VideoDecoderRenderer { public abstract String getDecoderName(); } diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java index 5a24de9b..795efc06 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java @@ -2,7 +2,6 @@ package com.limelight.binding.video; import java.nio.ByteBuffer; import java.util.Locale; -import java.util.concurrent.locks.LockSupport; import org.jcodec.codecs.h264.io.model.SeqParameterSet; import org.jcodec.codecs.h264.io.model.VUIParameters; @@ -46,8 +45,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { private int numPpsIn; private int numIframeIn; - private static final boolean ENABLE_ASYNC_RENDERER = false; - @TargetApi(Build.VERSION_CODES.KITKAT) public MediaCodecDecoderRenderer() { //dumpDecoders(); @@ -79,7 +76,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { } } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) { this.initialWidth = width; @@ -107,52 +103,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height); } - // On Lollipop, we use asynchronous mode to avoid having a busy looping renderer thread - if (ENABLE_ASYNC_RENDERER && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - videoDecoder.setCallback(new MediaCodec.Callback() { - @Override - public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { - LimeLog.info("Output format changed"); - LimeLog.info("New output Format: " + format); - } - - @Override - public void onOutputBufferAvailable(MediaCodec codec, int index, - BufferInfo info) { - try { - // FIXME: It looks like we can't frameskip here - codec.releaseOutputBuffer(index, true); - } catch (Exception e) { - handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); - } - } - - @Override - public void onInputBufferAvailable(MediaCodec codec, int index) { - try { - submitDecodeUnit(depacketizer.takeNextDecodeUnit(), codec.getInputBuffer(index), index); - } catch (InterruptedException e) { - // What do we do here? - e.printStackTrace(); - } catch (Exception e) { - handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); - } - } - - @Override - public void onError(MediaCodec codec, CodecException e) { - if (e.isTransient()) { - LimeLog.warning(e.getDiagnosticInfo()); - e.printStackTrace(); - } - else { - LimeLog.severe(e.getDiagnosticInfo()); - e.printStackTrace(); - } - } - }); - } - videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0); videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT); @@ -162,7 +112,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { } @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private void handleDecoderException(MediaCodecDecoderRenderer dr, Exception e, ByteBuffer buf, int codecFlags) { + private void handleDecoderException(Exception e, ByteBuffer buf, int codecFlags) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (e instanceof CodecException) { CodecException codecExc = (CodecException) e; @@ -178,10 +128,10 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { } if (buf != null || codecFlags != 0) { - throw new RendererException(dr, e, buf, codecFlags); + throw new RendererException(this, e, buf, codecFlags); } else { - throw new RendererException(dr, e); + throw new RendererException(this, e); } } @@ -192,71 +142,10 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { @Override public void run() { BufferInfo info = new BufferInfo(); - DecodeUnit du = null; - int inputIndex = -1; - while (!isInterrupted()) - { - // In order to get as much data to the decoder as early as possible, - // try to submit up to 5 decode units at once without blocking. - if (inputIndex == -1 && du == null) { - try { - for (int i = 0; i < 5; i++) { - inputIndex = videoDecoder.dequeueInputBuffer(0); - du = depacketizer.pollNextDecodeUnit(); - - // Stop if we can't get a DU or input buffer - if (du == null || inputIndex == -1) { - break; - } - - submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex); - - du = null; - inputIndex = -1; - } - } catch (Exception e) { - inputIndex = -1; - handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); - } - } - - // Grab an input buffer if we don't have one already. - // This way we can have one ready hopefully by the time - // the depacketizer is done with this frame. It's important - // that this can timeout because it's possible that we could exhaust - // the decoder's input buffers and deadlocks because aren't pulling - // frames out of the other end. - if (inputIndex == -1) { - try { - // If we've got a DU waiting to be given to the decoder, - // wait a full 3 ms for an input buffer. Otherwise - // just see if we can get one immediately. - inputIndex = videoDecoder.dequeueInputBuffer(du != null ? 3000 : 0); - } catch (Exception e) { - inputIndex = -1; - handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); - } - } - - // Grab a decode unit if we don't have one already - if (du == null) { - du = depacketizer.pollNextDecodeUnit(); - } - - // If we've got both a decode unit and an input buffer, we'll - // submit now. Otherwise, we wait until we have one. - if (du != null && inputIndex >= 0) { - submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex); - - // DU and input buffer have both been consumed - du = null; - inputIndex = -1; - } - - // Try to output a frame + while (!isInterrupted()) { try { - int outIndex = videoDecoder.dequeueOutputBuffer(info, 0); - + // Try to output a frame + int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000); if (outIndex >= 0) { long presentationTimeUs = info.presentationTimeUs; int lastIndex = outIndex; @@ -264,6 +153,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { // Get the last output buffer in the queue while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) { videoDecoder.releaseOutputBuffer(lastIndex, false); + lastIndex = outIndex; presentationTimeUs = info.presentationTimeUs; } @@ -272,33 +162,28 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { videoDecoder.releaseOutputBuffer(lastIndex, true); // Add delta time to the totals (excluding probable outliers) - long delta = System.currentTimeMillis()-(presentationTimeUs/1000); + long delta = System.currentTimeMillis() - (presentationTimeUs / 1000); if (delta >= 0 && delta < 1000) { decoderTimeMs += delta; totalTimeMs += delta; } } else { switch (outIndex) { - case MediaCodec.INFO_TRY_AGAIN_LATER: - // Getting an input buffer may already block - // so don't park if we still need to do that - if (inputIndex >= 0) { - LockSupport.parkNanos(1); - } - break; - case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED: - LimeLog.info("Output buffers changed"); - break; - case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: - LimeLog.info("Output format changed"); - LimeLog.info("New output Format: " + videoDecoder.getOutputFormat()); - break; - default: - break; + case MediaCodec.INFO_TRY_AGAIN_LATER: + break; + case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED: + LimeLog.info("Output buffers changed"); + break; + case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: + LimeLog.info("Output format changed"); + LimeLog.info("New output Format: " + videoDecoder.getOutputFormat()); + break; + default: + break; } } } catch (Exception e) { - handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); + handleDecoderException(e, null, 0); } } } @@ -316,11 +201,9 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { // Start the decoder videoDecoder.start(); - // On devices pre-Lollipop, we'll use a rendering thread - if (!ENABLE_ASYNC_RENDERER || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - videoDecoderInputBuffers = videoDecoder.getInputBuffers(); - startRendererThread(); - } + videoDecoderInputBuffers = videoDecoder.getInputBuffers(); + startRendererThread(); + return true; } @@ -357,7 +240,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { timestampUs, codecFlags); break; } catch (Exception e) { - handleDecoderException(this, e, null, codecFlags); + handleDecoderException(e, null, codecFlags); lastException = e; } } @@ -530,8 +413,14 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { @Override public int getCapabilities() { - return adaptivePlayback ? + int caps = 0; + + caps |= adaptivePlayback ? VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0; + + caps |= VideoDecoderRenderer.CAPABILITY_DIRECT_SUBMIT; + + return caps; } @Override @@ -555,6 +444,24 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { return decoderName; } + @Override + public void directSubmitDecodeUnit(DecodeUnit du) { + int inputIndex; + + for (;;) { + try { + inputIndex = videoDecoder.dequeueInputBuffer(-1); + break; + } catch (Exception e) { + handleDecoderException(e, null, 0); + } + } + + if (inputIndex >= 0) { + submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex); + } + } + public class RendererException extends RuntimeException { private static final long serialVersionUID = 8985937536997012406L; From 6d6d7121f697cb7f82beeed6b4ac03e0c112f630 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 15 Mar 2015 14:30:56 -0700 Subject: [PATCH 060/202] Remove the Playpad Pro hack that worked around an issue with old firmware and caused the D-pad to be unresponsive on updated firmware. Fixes #41 --- .../java/com/limelight/binding/input/ControllerHandler.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java index 551f96b8..fcf4345c 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -277,11 +277,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener { context.isRemote = true; } } - // NYKO Playpad has a fake hat that mimics the left stick for some reason - else if (devName.contains("NYKO PLAYPAD")) { - context.hatXAxis = -1; - context.hatYAxis = -1; - } } LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius); From bf2cc2a4d5f390db1158a548c300904a733e0063 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 16 Mar 2015 19:35:43 -0400 Subject: [PATCH 061/202] Don't assign controller numbers to devices that don't have an analog stick --- .../java/com/limelight/binding/input/ControllerHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java index fcf4345c..5892f566 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -286,7 +286,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener { // This is the back button on Shield portable consoles context.controllerNumber = 0; } - else if (multiControllerEnabled) { + else if (multiControllerEnabled && context.hasJoystickAxes) { context.controllerNumber = assignNewControllerNumber(); } else { From 42c65f4f16881e63cf326873456692df91dd2daa Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 16 Mar 2015 19:36:09 -0400 Subject: [PATCH 062/202] Use smaller deadzones for SHIELD controllers --- .../java/com/limelight/binding/input/ControllerHandler.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java index 5892f566..56ea6d56 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -277,6 +277,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener { context.isRemote = true; } } + // SHIELD controllers will use small stick deadzones + else if (devName.contains("SHIELD")) { + context.leftStickDeadzoneRadius = 0.07f; + context.rightStickDeadzoneRadius = 0.07f; + } } LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius); From 7ce29e3a09bcc0a6ad924256143c3edb54a060a1 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 16 Mar 2015 21:26:02 -0400 Subject: [PATCH 063/202] Add a workaround for the Nexus 9 dropping frames with the new renderer --- .../video/MediaCodecDecoderRenderer.java | 14 ++++++++--- .../binding/video/MediaCodecHelper.java | 25 ++++++++++++++++++- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java index 795efc06..bfa12cae 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java @@ -27,10 +27,11 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { private ByteBuffer[] videoDecoderInputBuffers; private MediaCodec videoDecoder; private Thread rendererThread; - private boolean needsSpsBitstreamFixup, isExynos4; + private final boolean needsSpsBitstreamFixup, isExynos4; private VideoDepacketizer depacketizer; - private boolean adaptivePlayback; + private final boolean adaptivePlayback; private int initialWidth, initialHeight; + private final int dequeueOutputBufferTimeout; private boolean needsBaselineSpsHack; private SeqParameterSet savedSps; @@ -55,12 +56,17 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { } if (decoder == null) { // This case is handled later in setup() + needsSpsBitstreamFixup = false; + isExynos4 = false; + adaptivePlayback = false; + dequeueOutputBufferTimeout = 0; return; } decoderName = decoder.getName(); // Set decoder-specific attributes + dequeueOutputBufferTimeout = MediaCodecHelper.getOptimalOutputBufferDequeueTimeout(decoderName, decoder); adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder); needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder); needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder); @@ -145,7 +151,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { while (!isInterrupted()) { try { // Try to output a frame - int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000); + int outIndex = videoDecoder.dequeueOutputBuffer(info, dequeueOutputBufferTimeout); if (outIndex >= 0) { long presentationTimeUs = info.presentationTimeUs; int lastIndex = outIndex; @@ -189,7 +195,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { } }; rendererThread.setName("Video - Renderer (MediaCodec)"); - rendererThread.setPriority(Thread.MAX_PRIORITY); + rendererThread.setPriority(Thread.NORM_PRIORITY + 2); rendererThread.start(); } diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java index d3befe7d..a760bb50 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java @@ -26,7 +26,16 @@ public class MediaCodecHelper { private static final List spsFixupBitstreamFixupDecoderPrefixes; private static final List whitelistedAdaptiveResolutionPrefixes; private static final List baselineProfileHackPrefixes; - + private static final List fastOutputPollPrefixes; + + private static final int FAST_OUTPUT_POLL_US = 3; // 3 us + private static final int NORMAL_OUTPUT_POLL_US = 50000; // 50 ms + + static { + fastOutputPollPrefixes = new LinkedList(); + fastOutputPollPrefixes.add("omx.nvidia"); + } + static { preferredDecoders = new LinkedList(); } @@ -97,6 +106,20 @@ public class MediaCodecHelper { return false; } + + public static int getOptimalOutputBufferDequeueTimeout(String decoderName, MediaCodecInfo decoderInfo) { + // This concept of "fast output polling" is a workaround for certain devices that are powerful enough + // that the governor overzealously reduces the clockspeed of the CPU enough that it causes frames to be + // lost. This (at least) affects the Denver Tegra K1 running Android 5.0. To simplify things, I've simply + // set all Tegra devices to use fast polling. + if (isDecoderInList(fastOutputPollPrefixes, decoderName)) { + LimeLog.info("Decoder "+decoderName+" requires fast output polling"); + return FAST_OUTPUT_POLL_US; + } + else { + return NORMAL_OUTPUT_POLL_US; + } + } public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) { return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName); From 5310375d428ecaca94c9328f72ce0ddf17c3ef46 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 16 Mar 2015 21:28:33 -0400 Subject: [PATCH 064/202] Target Android 5.1 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 716e7b3a..3c84597a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,12 +4,12 @@ import org.apache.tools.ant.taskdefs.condition.Os apply plugin: 'com.android.application' android { - compileSdkVersion 21 + compileSdkVersion 22 buildToolsVersion "21.1.2" defaultConfig { minSdkVersion 16 - targetSdkVersion 21 + targetSdkVersion 22 versionName "3.1.2" versionCode = 56 From 60beb81ae4b00eb29f79294ec387a80fe0825e2f Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 16 Mar 2015 21:28:49 -0400 Subject: [PATCH 065/202] Target API 22 --- app/app.iml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/app.iml b/app/app.iml index 426a9c9e..979dbef8 100644 --- a/app/app.iml +++ b/app/app.iml @@ -103,7 +103,7 @@ - + From 115853fed27161b2eb2fcf287b78494ec6c74b29 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 16 Mar 2015 21:29:07 -0400 Subject: [PATCH 066/202] Update version to 3.1.3-beta1 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3c84597a..e7a733b5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { minSdkVersion 16 targetSdkVersion 22 - versionName "3.1.2" - versionCode = 56 + versionName "3.1.3-beta1" + versionCode = 57 } productFlavors { From 7ab0be3b624a4b79ec47fed1869862a488b95875 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 23 Mar 2015 15:12:25 -0400 Subject: [PATCH 067/202] Optimize app grid performance on lower end devices --- .../com/limelight/computers/ComputerManagerService.java | 7 ++++--- .../com/limelight/grid/assets/CachedAppAssetLoader.java | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index d2621807..1bd1c2bf 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -31,7 +31,8 @@ import android.os.IBinder; import org.xmlpull.v1.XmlPullParserException; public class ComputerManagerService extends Service { - private static final int POLLING_PERIOD_MS = 3000; + private static final int SERVERINFO_POLLING_PERIOD_MS = 3000; + private static final int APPLIST_POLLING_PERIOD_MS = 30000; private static final int MDNS_QUERY_PERIOD_MS = 1000; private static final int FAST_POLL_TIMEOUT = 500; private static final int OFFLINE_POLL_TRIES = 3; @@ -135,7 +136,7 @@ public class ComputerManagerService extends Service { } // Wait until the next polling interval - Thread.sleep(POLLING_PERIOD_MS / ((offlineCount > 0) ? 2 : 1)); + Thread.sleep(SERVERINFO_POLLING_PERIOD_MS / ((offlineCount > 0) ? 2 : 1)); } catch (InterruptedException e) { break; } @@ -581,7 +582,7 @@ public class ComputerManagerService extends Service { private boolean waitPollingDelay() { try { synchronized (pollEvent) { - pollEvent.wait(POLLING_PERIOD_MS); + pollEvent.wait(APPLIST_POLLING_PERIOD_MS); } } catch (InterruptedException e) { return false; diff --git a/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java index f4438cd5..1bf93276 100644 --- a/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java @@ -17,9 +17,9 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class CachedAppAssetLoader { - private static final int MAX_CONCURRENT_DISK_LOADS = 4; - private static final int MAX_CONCURRENT_NETWORK_LOADS = 4; - private static final int MAX_CONCURRENT_CACHE_LOADS = 2; + private static final int MAX_CONCURRENT_DISK_LOADS = 3; + private static final int MAX_CONCURRENT_NETWORK_LOADS = 3; + private static final int MAX_CONCURRENT_CACHE_LOADS = 1; private static final int MAX_PENDING_CACHE_LOADS = 100; private static final int MAX_PENDING_NETWORK_LOADS = 40; From a676b8d8e665b5e17914b0b8b37e4f48d66709d5 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 23 Mar 2015 15:51:11 -0400 Subject: [PATCH 068/202] Restore the legacy path and only use direct submit for certain whitelisted decoders --- .../video/MediaCodecDecoderRenderer.java | 190 ++++++++++++++++-- .../binding/video/MediaCodecHelper.java | 29 +-- 2 files changed, 188 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java index bfa12cae..7fe65e8d 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java @@ -2,6 +2,7 @@ package com.limelight.binding.video; import java.nio.ByteBuffer; import java.util.Locale; +import java.util.concurrent.locks.LockSupport; import org.jcodec.codecs.h264.io.model.SeqParameterSet; import org.jcodec.codecs.h264.io.model.VUIParameters; @@ -29,9 +30,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { private Thread rendererThread; private final boolean needsSpsBitstreamFixup, isExynos4; private VideoDepacketizer depacketizer; - private final boolean adaptivePlayback; + private final boolean adaptivePlayback, directSubmit; private int initialWidth, initialHeight; - private final int dequeueOutputBufferTimeout; private boolean needsBaselineSpsHack; private SeqParameterSet savedSps; @@ -56,17 +56,15 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { } if (decoder == null) { // This case is handled later in setup() - needsSpsBitstreamFixup = false; - isExynos4 = false; - adaptivePlayback = false; - dequeueOutputBufferTimeout = 0; + needsSpsBitstreamFixup = isExynos4 = + adaptivePlayback = directSubmit = false; return; } decoderName = decoder.getName(); // Set decoder-specific attributes - dequeueOutputBufferTimeout = MediaCodecHelper.getOptimalOutputBufferDequeueTimeout(decoderName, decoder); + directSubmit = MediaCodecHelper.decoderCanDirectSubmit(decoderName, decoder); adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder); needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder); needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder); @@ -80,6 +78,9 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { if (isExynos4) { LimeLog.info("Decoder "+decoderName+" is on Exynos 4"); } + if (directSubmit) { + LimeLog.info("Decoder "+decoderName+" will use direct submit"); + } } @Override @@ -141,7 +142,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { } } - private void startRendererThread() + private void startDirectSubmitRendererThread() { rendererThread = new Thread() { @SuppressWarnings("deprecation") @@ -151,7 +152,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { while (!isInterrupted()) { try { // Try to output a frame - int outIndex = videoDecoder.dequeueOutputBuffer(info, dequeueOutputBufferTimeout); + int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000); if (outIndex >= 0) { long presentationTimeUs = info.presentationTimeUs; int lastIndex = outIndex; @@ -199,6 +200,162 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { rendererThread.start(); } + private int dequeueInputBuffer(boolean wait, boolean infiniteWait) { + int index; + long startTime, queueTime; + + startTime = System.currentTimeMillis(); + + index = videoDecoder.dequeueInputBuffer(wait ? (infiniteWait ? -1 : 3000) : 0); + if (index < 0) { + return index; + } + + queueTime = System.currentTimeMillis(); + + if (queueTime - startTime >= 20) { + LimeLog.warning("Queue input buffer ran long: "+(queueTime - startTime)+" ms"); + } + + return index; + } + + private void startLegacyRendererThread() + { + rendererThread = new Thread() { + @SuppressWarnings("deprecation") + @Override + public void run() { + BufferInfo info = new BufferInfo(); + DecodeUnit du = null; + int inputIndex = -1; + long lastDuDequeueTime = 0; + while (!isInterrupted()) + { + // In order to get as much data to the decoder as early as possible, + // try to submit up to 5 decode units at once without blocking. + if (inputIndex == -1 && du == null) { + try { + for (int i = 0; i < 5; i++) { + inputIndex = dequeueInputBuffer(false, false); + du = depacketizer.pollNextDecodeUnit(); + + // Stop if we can't get a DU or input buffer + if (du == null || inputIndex == -1) { + if (du != null) { + lastDuDequeueTime = System.currentTimeMillis(); + } + + break; + } + + submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex); + + du = null; + inputIndex = -1; + } + } catch (Exception e) { + inputIndex = -1; + handleDecoderException(e, null, 0); + } + } + + // Grab an input buffer if we don't have one already. + // This way we can have one ready hopefully by the time + // the depacketizer is done with this frame. It's important + // that this can timeout because it's possible that we could exhaust + // the decoder's input buffers and deadlocks because aren't pulling + // frames out of the other end. + if (inputIndex == -1) { + try { + // If we've got a DU waiting to be given to the decoder, + // wait a full 3 ms for an input buffer. Otherwise + // just see if we can get one immediately. + inputIndex = dequeueInputBuffer(du != null, false); + } catch (Exception e) { + inputIndex = -1; + handleDecoderException(e, null, 0); + } + } + + // Grab a decode unit if we don't have one already + if (du == null) { + du = depacketizer.pollNextDecodeUnit(); + if (du != null) { + lastDuDequeueTime = System.currentTimeMillis(); + } + } + + // If we've got both a decode unit and an input buffer, we'll + // submit now. Otherwise, we wait until we have one. + if (du != null && inputIndex >= 0) { + long submissionTime = System.currentTimeMillis(); + if (submissionTime - lastDuDequeueTime >= 20) { + LimeLog.warning("Receiving an input buffer took too long: "+(submissionTime - lastDuDequeueTime)+" ms"); + } + + submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex); + + // DU and input buffer have both been consumed + du = null; + inputIndex = -1; + } + + // Try to output a frame + try { + int outIndex = videoDecoder.dequeueOutputBuffer(info, 0); + + if (outIndex >= 0) { + long presentationTimeUs = info.presentationTimeUs; + int lastIndex = outIndex; + + // Get the last output buffer in the queue + while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) { + videoDecoder.releaseOutputBuffer(lastIndex, false); + lastIndex = outIndex; + presentationTimeUs = info.presentationTimeUs; + } + + // Render the last buffer + videoDecoder.releaseOutputBuffer(lastIndex, true); + + // Add delta time to the totals (excluding probable outliers) + long delta = System.currentTimeMillis()-(presentationTimeUs/1000); + if (delta >= 0 && delta < 1000) { + decoderTimeMs += delta; + totalTimeMs += delta; + } + } else { + switch (outIndex) { + case MediaCodec.INFO_TRY_AGAIN_LATER: + // Getting an input buffer may already block + // so don't park if we still need to do that + if (inputIndex >= 0) { + LockSupport.parkNanos(1); + } + break; + case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED: + LimeLog.info("Output buffers changed"); + break; + case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: + LimeLog.info("Output format changed"); + LimeLog.info("New output Format: " + videoDecoder.getOutputFormat()); + break; + default: + break; + } + } + } catch (Exception e) { + handleDecoderException(e, null, 0); + } + } + } + }; + rendererThread.setName("Video - Renderer (MediaCodec)"); + rendererThread.setPriority(Thread.MAX_PRIORITY); + rendererThread.start(); + } + @SuppressWarnings("deprecation") @Override public boolean start(VideoDepacketizer depacketizer) { @@ -208,7 +365,13 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { videoDecoder.start(); videoDecoderInputBuffers = videoDecoder.getInputBuffers(); - startRendererThread(); + + if (directSubmit) { + startDirectSubmitRendererThread(); + } + else { + startLegacyRendererThread(); + } return true; } @@ -391,7 +554,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { } private void replaySps() { - int inputIndex = videoDecoder.dequeueInputBuffer(-1); + int inputIndex = dequeueInputBuffer(true, true); ByteBuffer inputBuffer = videoDecoderInputBuffers[inputIndex]; inputBuffer.clear(); @@ -424,7 +587,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { caps |= adaptivePlayback ? VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0; - caps |= VideoDecoderRenderer.CAPABILITY_DIRECT_SUBMIT; + caps |= directSubmit ? + VideoDecoderRenderer.CAPABILITY_DIRECT_SUBMIT : 0; return caps; } @@ -456,7 +620,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { for (;;) { try { - inputIndex = videoDecoder.dequeueInputBuffer(-1); + inputIndex = dequeueInputBuffer(true, true); break; } catch (Exception e) { handleDecoderException(e, null, 0); diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java index a760bb50..fb0f082d 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java @@ -26,14 +26,17 @@ public class MediaCodecHelper { private static final List spsFixupBitstreamFixupDecoderPrefixes; private static final List whitelistedAdaptiveResolutionPrefixes; private static final List baselineProfileHackPrefixes; - private static final List fastOutputPollPrefixes; - - private static final int FAST_OUTPUT_POLL_US = 3; // 3 us - private static final int NORMAL_OUTPUT_POLL_US = 50000; // 50 ms + private static final List directSubmitPrefixes; static { - fastOutputPollPrefixes = new LinkedList(); - fastOutputPollPrefixes.add("omx.nvidia"); + directSubmitPrefixes = new LinkedList(); + + // These decoders have low enough input buffer latency that they + // can be directly invoked from the receive thread + directSubmitPrefixes.add("omx.qcom"); + directSubmitPrefixes.add("omx.sec"); + directSubmitPrefixes.add("omx.intel"); + directSubmitPrefixes.add("omx.brcm"); } static { @@ -107,18 +110,8 @@ public class MediaCodecHelper { return false; } - public static int getOptimalOutputBufferDequeueTimeout(String decoderName, MediaCodecInfo decoderInfo) { - // This concept of "fast output polling" is a workaround for certain devices that are powerful enough - // that the governor overzealously reduces the clockspeed of the CPU enough that it causes frames to be - // lost. This (at least) affects the Denver Tegra K1 running Android 5.0. To simplify things, I've simply - // set all Tegra devices to use fast polling. - if (isDecoderInList(fastOutputPollPrefixes, decoderName)) { - LimeLog.info("Decoder "+decoderName+" requires fast output polling"); - return FAST_OUTPUT_POLL_US; - } - else { - return NORMAL_OUTPUT_POLL_US; - } + public static boolean decoderCanDirectSubmit(String decoderName, MediaCodecInfo decoderInfo) { + return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device(); } public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) { From 29a395f3f41e6fae68d026a14cc81cc2b36fc519 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 23 Mar 2015 15:57:29 -0400 Subject: [PATCH 069/202] Prevent updating the UI while quitting is in progress --- app/src/main/java/com/limelight/AppView.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index 6b1a6b19..18e2f695 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -55,6 +55,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { private SpinnerDialog blockingLoadSpinner; private String lastRawApplist; private int lastRunningAppId; + private boolean suspendGridUpdates; private final static int START_OR_RESUME_ID = 1; private final static int QUIT_ID = 2; @@ -125,6 +126,11 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { managerBinder.startPolling(new ComputerManagerListener() { @Override public void notifyComputerUpdated(ComputerDetails details) { + // Do nothing if updates are suspended + if (suspendGridUpdates) { + return; + } + // Don't care about other computers if (!details.uuid.toString().equalsIgnoreCase(uuidString)) { return; @@ -497,6 +503,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { NvHTTP httpConn; String message; try { + suspendGridUpdates = true; httpConn = new NvHTTP(getAddress(), managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(AppView.this)); if (httpConn.quitApp()) { @@ -521,6 +528,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { message = e.getMessage(); } finally { // Trigger a poll immediately + suspendGridUpdates = false; if (poller != null) { poller.pollNow(); } From 9c0960d03d48f76dcfa966b935e14982844ba433 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 23 Mar 2015 16:36:43 -0400 Subject: [PATCH 070/202] Add options to quit and resume streaming from the PC view --- app/src/main/java/com/limelight/AppView.java | 120 +++--------------- app/src/main/java/com/limelight/PcView.java | 92 +++++++++----- .../java/com/limelight/utils/UiHelper.java | 31 +++++ 3 files changed, 113 insertions(+), 130 deletions(-) diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index 18e2f695..fe33f193 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -21,6 +21,7 @@ import com.limelight.ui.AdapterFragment; import com.limelight.ui.AdapterFragmentCallbacks; import com.limelight.utils.CacheHelper; import com.limelight.utils.Dialog; +import com.limelight.utils.ServerHelper; import com.limelight.utils.SpinnerDialog; import com.limelight.utils.UiHelper; @@ -113,11 +114,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { } }; - private InetAddress getAddress() { - return computer.reachability == ComputerDetails.Reachability.LOCAL ? - computer.localIp : computer.remoteIp; - } - private void startComputerUpdates() { if (managerBinder == null) { return; @@ -308,33 +304,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { public void onContextMenuClosed(Menu menu) { } - private void displayQuitConfirmationDialog(final Runnable onYes, final Runnable onNo) { - DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - switch (which){ - case DialogInterface.BUTTON_POSITIVE: - if (onYes != null) { - onYes.run(); - } - break; - - case DialogInterface.BUTTON_NEGATIVE: - if (onNo != null) { - onNo.run(); - } - break; - } - } - }; - - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(getResources().getString(R.string.applist_quit_confirmation)) - .setPositiveButton(getResources().getString(R.string.yes), dialogClickListener) - .setNegativeButton(getResources().getString(R.string.no), dialogClickListener) - .show(); - } - @Override public boolean onContextItemSelected(MenuItem item) { AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); @@ -342,25 +311,37 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { switch (item.getItemId()) { case START_WTIH_QUIT: // Display a confirmation dialog first - displayQuitConfirmationDialog(new Runnable() { + UiHelper.displayQuitConfirmationDialog(this, new Runnable() { @Override public void run() { - doStart(app.app); + ServerHelper.doStart(AppView.this, app.app, computer, managerBinder); } }, null); return true; case START_OR_RESUME_ID: // Resume is the same as start for us - doStart(app.app); + ServerHelper.doStart(AppView.this, app.app, computer, managerBinder); return true; case QUIT_ID: // Display a confirmation dialog first - displayQuitConfirmationDialog(new Runnable() { + UiHelper.displayQuitConfirmationDialog(this, new Runnable() { @Override public void run() { - doQuit(app.app); + suspendGridUpdates = true; + ServerHelper.doQuit(AppView.this, + ServerHelper.getCurrentAddressFromComputer(computer), + app.app, managerBinder, new Runnable() { + @Override + public void run() { + // Trigger a poll immediately + suspendGridUpdates = false; + if (poller != null) { + poller.pollNow(); + } + } + }); } }, null); return true; @@ -482,69 +463,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { }); } - private void doStart(NvApp app) { - Intent intent = new Intent(this, Game.class); - intent.putExtra(Game.EXTRA_HOST, - computer.reachability == ComputerDetails.Reachability.LOCAL ? - computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress()); - intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName()); - intent.putExtra(Game.EXTRA_APP_ID, app.getAppId()); - intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId()); - intent.putExtra(Game.EXTRA_STREAMING_REMOTE, - computer.reachability != ComputerDetails.Reachability.LOCAL); - startActivity(intent); - } - - private void doQuit(final NvApp app) { - Toast.makeText(AppView.this, getResources().getString(R.string.applist_quit_app)+" "+app.getAppName()+"...", Toast.LENGTH_SHORT).show(); - new Thread(new Runnable() { - @Override - public void run() { - NvHTTP httpConn; - String message; - try { - suspendGridUpdates = true; - 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()+")"; - } - else { - message = e.getMessage(); - } - } catch (UnknownHostException e) { - message = getResources().getString(R.string.error_unknown_host); - } catch (FileNotFoundException e) { - message = getResources().getString(R.string.error_404); - } catch (Exception e) { - message = e.getMessage(); - } finally { - // Trigger a poll immediately - suspendGridUpdates = false; - if (poller != null) { - poller.pollNow(); - } - } - - final String toastMessage = message; - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show(); - } - }); - } - }).start(); - } - @Override public int getAdapterFragmentLayoutId() { return PreferenceConfiguration.readPreferences(this).listMode ? @@ -565,7 +483,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { if (getRunningAppId() != -1) { openContextMenu(arg1); } else { - doStart(app.app); + ServerHelper.doStart(AppView.this, app.app, computer, managerBinder); } } }); diff --git a/app/src/main/java/com/limelight/PcView.java b/app/src/main/java/com/limelight/PcView.java index 689a7fc7..03709836 100644 --- a/app/src/main/java/com/limelight/PcView.java +++ b/app/src/main/java/com/limelight/PcView.java @@ -12,6 +12,7 @@ import com.limelight.computers.ComputerManagerListener; import com.limelight.computers.ComputerManagerService; import com.limelight.grid.PcGridAdapter; import com.limelight.nvstream.http.ComputerDetails; +import com.limelight.nvstream.http.NvApp; import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.PairingManager; import com.limelight.nvstream.http.PairingManager.PairState; @@ -22,6 +23,7 @@ import com.limelight.preferences.StreamSettings; import com.limelight.ui.AdapterFragment; import com.limelight.ui.AdapterFragmentCallbacks; import com.limelight.utils.Dialog; +import com.limelight.utils.ServerHelper; import com.limelight.utils.UiHelper; import android.app.Activity; @@ -93,6 +95,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { private final static int UNPAIR_ID = 3; private final static int WOL_ID = 4; private final static int DELETE_ID = 5; + private final static int RESUME_ID = 6; + private final static int QUIT_ID = 7; private void initializeViews() { setContentView(R.layout.activity_pc_view); @@ -252,11 +256,16 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc)); } else { - menu.add(Menu.NONE, APP_LIST_ID, 1, getResources().getString(R.string.pcview_menu_app_list)); + if (computer.details.runningGameId != 0) { + menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume)); + menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit)); + } + + menu.add(Menu.NONE, APP_LIST_ID, 3, getResources().getString(R.string.pcview_menu_app_list)); // FIXME: We used to be able to unpair here but it's been broken since GFE 2.1.x, so I've replaced // it with delete which actually work - menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc)); + menu.add(Menu.NONE, DELETE_ID, 4, getResources().getString(R.string.pcview_menu_delete_pc)); } } @@ -477,36 +486,61 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { @Override public boolean onContextItemSelected(MenuItem item) { AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); - ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position); - switch (item.getItemId()) - { - case PAIR_ID: - doPair(computer.details); - return true; - - case UNPAIR_ID: - doUnpair(computer.details); - return true; - - case WOL_ID: - doWakeOnLan(computer.details); - return true; - - case DELETE_ID: - if (managerBinder == null) { - Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); + final ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position); + switch (item.getItemId()) { + case PAIR_ID: + doPair(computer.details); return true; - } - managerBinder.removeComputer(computer.details.name); - removeComputer(computer.details); - return true; - case APP_LIST_ID: - doAppList(computer.details); - return true; + case UNPAIR_ID: + doUnpair(computer.details); + return true; - default: - return super.onContextItemSelected(item); + case WOL_ID: + doWakeOnLan(computer.details); + return true; + + case DELETE_ID: + if (managerBinder == null) { + Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); + return true; + } + managerBinder.removeComputer(computer.details.name); + removeComputer(computer.details); + return true; + + case APP_LIST_ID: + doAppList(computer.details); + return true; + + case RESUME_ID: + if (managerBinder == null) { + Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); + return true; + } + + ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId), computer.details, managerBinder); + return true; + + case QUIT_ID: + if (managerBinder == null) { + Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); + return true; + } + + // Display a confirmation dialog first + UiHelper.displayQuitConfirmationDialog(this, new Runnable() { + @Override + public void run() { + ServerHelper.doQuit(PcView.this, + ServerHelper.getCurrentAddressFromComputer(computer.details), + new NvApp("app", 0), managerBinder, null); + } + }, null); + return true; + + default: + return super.onContextItemSelected(item); } } diff --git a/app/src/main/java/com/limelight/utils/UiHelper.java b/app/src/main/java/com/limelight/utils/UiHelper.java index 6eb22e3a..32827b4b 100644 --- a/app/src/main/java/com/limelight/utils/UiHelper.java +++ b/app/src/main/java/com/limelight/utils/UiHelper.java @@ -1,11 +1,15 @@ package com.limelight.utils; import android.app.Activity; +import android.app.AlertDialog; import android.app.UiModeManager; import android.content.Context; +import android.content.DialogInterface; import android.content.res.Configuration; import android.view.View; +import com.limelight.R; + public class UiHelper { // Values from https://developer.android.com/training/tv/start/layouts.html @@ -28,4 +32,31 @@ public class UiHelper { horizontalPaddingPixels, verticalPaddingPixels); } } + + public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) { + DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which){ + case DialogInterface.BUTTON_POSITIVE: + if (onYes != null) { + onYes.run(); + } + break; + + case DialogInterface.BUTTON_NEGATIVE: + if (onNo != null) { + onNo.run(); + } + break; + } + } + }; + + AlertDialog.Builder builder = new AlertDialog.Builder(parent); + builder.setMessage(parent.getResources().getString(R.string.applist_quit_confirmation)) + .setPositiveButton(parent.getResources().getString(R.string.yes), dialogClickListener) + .setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener) + .show(); + } } From 5c71f55993b6a990637ce0a5dfffc240a98dc76e Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 23 Mar 2015 16:51:32 -0400 Subject: [PATCH 071/202] Add another Exynos prefix --- .../main/java/com/limelight/binding/video/MediaCodecHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java index fb0f082d..cf5be898 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java @@ -35,6 +35,7 @@ public class MediaCodecHelper { // can be directly invoked from the receive thread directSubmitPrefixes.add("omx.qcom"); directSubmitPrefixes.add("omx.sec"); + directSubmitPrefixes.add("omx.exynos"); directSubmitPrefixes.add("omx.intel"); directSubmitPrefixes.add("omx.brcm"); } From 1876b30c1b210b494f5a8f3f49f84903edb3e5c5 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 23 Mar 2015 16:51:57 -0400 Subject: [PATCH 072/202] Forgot this file --- .../com/limelight/utils/ServerHelper.java | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 app/src/main/java/com/limelight/utils/ServerHelper.java diff --git a/app/src/main/java/com/limelight/utils/ServerHelper.java b/app/src/main/java/com/limelight/utils/ServerHelper.java new file mode 100644 index 00000000..7e0d2575 --- /dev/null +++ b/app/src/main/java/com/limelight/utils/ServerHelper.java @@ -0,0 +1,90 @@ +package com.limelight.utils; + +import android.app.Activity; +import android.content.Intent; +import android.widget.Toast; + +import com.limelight.Game; +import com.limelight.R; +import com.limelight.binding.PlatformBinding; +import com.limelight.computers.ComputerManagerService; +import com.limelight.nvstream.http.ComputerDetails; +import com.limelight.nvstream.http.GfeHttpResponseException; +import com.limelight.nvstream.http.NvApp; +import com.limelight.nvstream.http.NvHTTP; + +import java.io.FileNotFoundException; +import java.net.InetAddress; +import java.net.UnknownHostException; + +public class ServerHelper { + public static InetAddress getCurrentAddressFromComputer(ComputerDetails computer) { + return computer.reachability == ComputerDetails.Reachability.LOCAL ? + computer.localIp : computer.remoteIp; + } + + public static void doStart(Activity parent, NvApp app, ComputerDetails computer, + ComputerManagerService.ComputerManagerBinder managerBinder) { + Intent intent = new Intent(parent, Game.class); + intent.putExtra(Game.EXTRA_HOST, + computer.reachability == ComputerDetails.Reachability.LOCAL ? + computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress()); + intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName()); + intent.putExtra(Game.EXTRA_APP_ID, app.getAppId()); + intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId()); + intent.putExtra(Game.EXTRA_STREAMING_REMOTE, + computer.reachability != ComputerDetails.Reachability.LOCAL); + parent.startActivity(intent); + } + + public static void doQuit(final Activity parent, + final InetAddress address, + final NvApp app, + final ComputerManagerService.ComputerManagerBinder managerBinder, + final Runnable onComplete) { + Toast.makeText(parent, parent.getResources().getString(R.string.applist_quit_app) + " " + app.getAppName() + "...", Toast.LENGTH_SHORT).show(); + new Thread(new Runnable() { + @Override + public void run() { + NvHTTP httpConn; + String message; + try { + httpConn = new NvHTTP(address, + managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(parent)); + if (httpConn.quitApp()) { + message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName(); + } else { + message = parent.getResources().getString(R.string.applist_quit_fail) + " " + app.getAppName(); + } + } catch (GfeHttpResponseException e) { + if (e.getErrorCode() == 599) { + message = "This session wasn't started by this device," + + " so it cannot be quit. End streaming on the original " + + "device or the PC itself. (Error code: "+e.getErrorCode()+")"; + } + else { + message = e.getMessage(); + } + } catch (UnknownHostException e) { + message = parent.getResources().getString(R.string.error_unknown_host); + } catch (FileNotFoundException e) { + message = parent.getResources().getString(R.string.error_404); + } catch (Exception e) { + message = e.getMessage(); + } finally { + if (onComplete != null) { + onComplete.run(); + } + } + + final String toastMessage = message; + parent.runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(parent, toastMessage, Toast.LENGTH_LONG).show(); + } + }); + } + }).start(); + } +} From 5847fbb6b6ead53b221050d664103b6ae562b704 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 23 Mar 2015 17:14:02 -0400 Subject: [PATCH 073/202] Add TI decoders to the direct submit whitelist --- .../main/java/com/limelight/binding/video/MediaCodecHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java index cf5be898..18222762 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java @@ -38,6 +38,7 @@ public class MediaCodecHelper { directSubmitPrefixes.add("omx.exynos"); directSubmitPrefixes.add("omx.intel"); directSubmitPrefixes.add("omx.brcm"); + directSubmitPrefixes.add("omx.TI"); } static { From c5336009837fc9097f095e412f73ef1708368075 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 23 Mar 2015 17:26:37 -0400 Subject: [PATCH 074/202] Update for 3.1.3 release --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e7a733b5..640e77dc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { minSdkVersion 16 targetSdkVersion 22 - versionName "3.1.3-beta1" - versionCode = 57 + versionName "3.1.3" + versionCode = 58 } productFlavors { From 072a439c2d255205cb98693d7e14ae207d6872dd Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Wed, 25 Mar 2015 00:32:22 -0400 Subject: [PATCH 075/202] Update common and decode unit API --- app/libs/limelight-common.jar | Bin 956588 -> 956550 bytes .../video/AndroidCpuDecoderRenderer.java | 6 ++++-- .../video/MediaCodecDecoderRenderer.java | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/libs/limelight-common.jar b/app/libs/limelight-common.jar index 530e78dbc71e458b945c9a69cce646c44eb4852a..709e5a2d1a15abaf3b27531d9b034d74d8ba9c41 100644 GIT binary patch delta 21362 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 z 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 Date: Wed, 25 Mar 2015 01:08:23 -0400 Subject: [PATCH 076/202] Include the time it takes to get an input buffer in the frame latency calculation --- .../video/MediaCodecDecoderRenderer.java | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java index 5cdc51b1..13c2ad85 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java @@ -239,13 +239,13 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { for (int i = 0; i < 5; i++) { inputIndex = dequeueInputBuffer(false, false); du = depacketizer.pollNextDecodeUnit(); + if (du != null) { + lastDuDequeueTime = System.currentTimeMillis(); + notifyDuReceived(du); + } // Stop if we can't get a DU or input buffer if (du == null || inputIndex == -1) { - if (du != null) { - lastDuDequeueTime = System.currentTimeMillis(); - } - break; } @@ -283,6 +283,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { du = depacketizer.pollNextDecodeUnit(); if (du != null) { lastDuDequeueTime = System.currentTimeMillis(); + notifyDuReceived(du); } } @@ -421,14 +422,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { @SuppressWarnings("deprecation") private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) { - long currentTime = System.currentTimeMillis(); - long delta = currentTime-decodeUnit.getReceiveTimestamp(); - if (delta >= 0 && delta < 1000) { - totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp(); - totalFrames++; - } - - long timestampUs = currentTime * 1000; + long timestampUs = System.currentTimeMillis() * 1000; if (timestampUs <= lastTimestampUs) { // We can't submit multiple buffers with the same timestamp // so bump it up by one before queuing @@ -614,10 +608,21 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { return decoderName; } + private void notifyDuReceived(DecodeUnit du) { + long currentTime = System.currentTimeMillis(); + long delta = currentTime-du.getReceiveTimestamp(); + if (delta >= 0 && delta < 1000) { + totalTimeMs += currentTime-du.getReceiveTimestamp(); + totalFrames++; + } + } + @Override public void directSubmitDecodeUnit(DecodeUnit du) { int inputIndex; + notifyDuReceived(du); + for (;;) { try { inputIndex = dequeueInputBuffer(true, true); From eac6998e17512944f63df94146d8d4439a2a7184 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Wed, 25 Mar 2015 01:20:55 -0400 Subject: [PATCH 077/202] Update the latency message strings to be more clear that this isn't end to end latency --- app/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c871dcec..663baa35 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -44,9 +44,9 @@ Establishing Connection Starting connection Warning: Your active network connection is metered! - Average client-side frame latency: + Average frame decoding latency: hardware decoder latency: - Average hardware decoder latency: + Average hardware decoding latency: Starting Connection Error Failed to start From cf36c7adb1557a2ceb793ffb5985397f8a0c6371 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Wed, 25 Mar 2015 02:33:46 -0400 Subject: [PATCH 078/202] Increment version --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 640e77dc..f64805a2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { minSdkVersion 16 targetSdkVersion 22 - versionName "3.1.3" - versionCode = 58 + versionName "3.1.4" + versionCode = 59 } productFlavors { From 1148e0163ce3d121d524d4e0fadce92dbb4a6f71 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 29 Mar 2015 22:54:48 -0400 Subject: [PATCH 079/202] Only assign a controller number when a valid controller input has been received. Fixes misdetection of other input devices as controllers (issue #65). --- .../binding/input/ControllerHandler.java | 61 +++++++++++-------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java index 56ea6d56..765da10c 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -97,18 +97,6 @@ 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 @@ -138,6 +126,41 @@ public class ControllerHandler implements InputManager.InputDeviceListener { currentControllers &= ~(1 << controllerNumber); } + // Called before sending input but after we've determined that this + // is definitely a controller (not a keyboard, mouse, or something else) + private void assignControllerNumberIfNeeded(ControllerContext context) { + if (context.assignedControllerNumber) { + return; + } + + LimeLog.info(context.name+" needs a controller number assigned"); + if (context.name != null && context.name.contains("gpio-keys")) { + // This is the back button on Shield portable consoles + LimeLog.info("Built-in buttons hardcoded as controller 0"); + context.controllerNumber = 0; + } + else if (multiControllerEnabled && context.hasJoystickAxes) { + context.controllerNumber = 0; + + LimeLog.info("Reserving the next available controller number"); + for (short i = 0; i < 4; i++) { + if ((currentControllers & (1 << i)) == 0) { + // Found an unused controller value + currentControllers |= (1 << i); + context.controllerNumber = i; + break; + } + } + } + else { + LimeLog.info("Not reserving a controller number"); + context.controllerNumber = 0; + } + + LimeLog.info("Assigned as controller "+context.controllerNumber); + context.assignedControllerNumber = true; + } + private ControllerContext createContextForDevice(InputDevice dev) { ControllerContext context = new ControllerContext(); String devName = dev.getName(); @@ -287,18 +310,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener { LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius); LimeLog.info("Trigger deadzone: "+context.triggerDeadzone); - if (devName != null && devName.equals("gpio-keys")) { - // This is the back button on Shield portable consoles - context.controllerNumber = 0; - } - else if (multiControllerEnabled && context.hasJoystickAxes) { - context.controllerNumber = assignNewControllerNumber(); - } - else { - context.controllerNumber = 0; - } - LimeLog.info("Assigned as controller "+context.controllerNumber); - return context; } @@ -324,6 +335,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener { } private void sendControllerInputPacket(ControllerContext context) { + assignControllerNumberIfNeeded(context); conn.sendControllerInput(context.controllerNumber, context.inputMap, context.leftTrigger, context.rightTrigger, context.leftStickX, context.leftStickY, @@ -804,6 +816,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener { public boolean isRemote; public boolean hasJoystickAxes; + public boolean assignedControllerNumber; public short controllerNumber; public short inputMap = 0x0000; From b5ba59b4131a7db2f9137bd5ea8a06e48f134bb0 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 29 Mar 2015 23:06:32 -0400 Subject: [PATCH 080/202] Fix database reference leak --- .../java/com/limelight/computers/ComputerManagerService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index 1bd1c2bf..5c91e43f 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -81,6 +81,7 @@ public class ComputerManagerService extends Service { if (!pollComputer(details)) { if (!newPc && offlineCount < OFFLINE_POLL_TRIES) { // Return without calling the listener + releaseLocalDatabaseReference(); return false; } From d822980d5a0969fbf83b3b362dc6d23c01a3ecef Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 29 Mar 2015 23:25:00 -0400 Subject: [PATCH 081/202] Fix missing close of Closeables caught by StrictMode --- app/libs/limelight-common.jar | Bin 956550 -> 956717 bytes .../grid/assets/CachedAppAssetLoader.java | 6 ++++++ .../java/com/limelight/utils/CacheHelper.java | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/app/libs/limelight-common.jar b/app/libs/limelight-common.jar index 709e5a2d1a15abaf3b27531d9b034d74d8ba9c41..7368edaf155f4072cc342bff2467aa6baa83cb3c 100644 GIT binary patch delta 8539 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 delta 8386 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 diff --git a/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java index 1bf93276..0dfdd8e8 100644 --- a/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java @@ -10,6 +10,7 @@ import android.widget.ImageView; import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.NvApp; +import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; import java.util.concurrent.LinkedBlockingQueue; @@ -97,6 +98,11 @@ public class CachedAppAssetLoader { // Write the stream straight to disk diskLoader.populateCacheWithStream(tuple, in); + // Close the network input stream + try { + in.close(); + } catch (IOException ignored) {} + // If there's a task associated with this load, we should return the bitmap if (task != null) { return diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider); diff --git a/app/src/main/java/com/limelight/utils/CacheHelper.java b/app/src/main/java/com/limelight/utils/CacheHelper.java index 8a008a77..3cf26e91 100644 --- a/app/src/main/java/com/limelight/utils/CacheHelper.java +++ b/app/src/main/java/com/limelight/utils/CacheHelper.java @@ -61,6 +61,10 @@ public class CacheHelper { sb.append(buf, 0, bytesRead); } + try { + in.close(); + } catch (IOException ignored) {} + return sb.toString(); } From 2856617fb3bda0f25bda1fe2d9582fd936604c63 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Tue, 31 Mar 2015 19:58:47 -0400 Subject: [PATCH 082/202] Only release controller numbers if they were reserved --- .../limelight/binding/input/ControllerHandler.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java index 765da10c..e2ab24f7 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -107,7 +107,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener { for (Map.Entry device : contexts.entrySet()) { if (device.getValue().id == deviceId) { LimeLog.info("Removed controller: "+device.getValue().name); - releaseControllerNumber(device.getValue().controllerNumber); + releaseControllerNumber(device.getValue()); contexts.remove(device.getKey()); return; } @@ -121,9 +121,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener { onInputDeviceAdded(deviceId); } - private void releaseControllerNumber(int controllerNumber) { - LimeLog.info("Controller number "+controllerNumber+" is now available"); - currentControllers &= ~(1 << controllerNumber); + private void releaseControllerNumber(ControllerContext context) { + if (context.reservedControllerNumber) { + LimeLog.info("Controller number "+context.controllerNumber+" is now available"); + currentControllers &= ~(1 << context.controllerNumber); + } } // Called before sending input but after we've determined that this @@ -148,6 +150,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener { // Found an unused controller value currentControllers |= (1 << i); context.controllerNumber = i; + context.reservedControllerNumber = true; break; } } @@ -817,6 +820,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener { public boolean hasJoystickAxes; public boolean assignedControllerNumber; + public boolean reservedControllerNumber; public short controllerNumber; public short inputMap = 0x0000; From 88249ba8aa4acf733557733bbe5b073c506bee2f Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Tue, 31 Mar 2015 19:59:16 -0400 Subject: [PATCH 083/202] Enable direct submission for ARC --- .../main/java/com/limelight/binding/video/MediaCodecHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java index 18222762..0b0b48e6 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java @@ -39,6 +39,7 @@ public class MediaCodecHelper { directSubmitPrefixes.add("omx.intel"); directSubmitPrefixes.add("omx.brcm"); directSubmitPrefixes.add("omx.TI"); + directSubmitPrefixes.add("omx.arc"); } static { From 2160e87fefacba06855a6ff4417034082c63f9b7 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Tue, 31 Mar 2015 20:29:22 -0400 Subject: [PATCH 084/202] Fix division by zero in ARC --- app/src/main/java/com/limelight/grid/AppGridAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index 9e6d4eaa..3cc0b38a 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -40,7 +40,7 @@ public class AppGridAdapter extends GenericGridAdapter { dp = LARGE_WIDTH_DP; } - double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160)); + double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160.0)); if (scalingDivisor < 1.0) { // We don't want to make them bigger before draw-time scalingDivisor = 1.0; From 1d9cf715178a10a40d1de1fb8fed54c89232a3e8 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Tue, 21 Apr 2015 21:50:40 -0400 Subject: [PATCH 085/202] Total Eclipse of the Lime --- README.md | 18 ++++++++---------- app/app.iml | 2 +- app/build.gradle | 4 ++-- .../main/res/drawable-hdpi/ic_launcher.png | Bin 5522 -> 5283 bytes .../main/res/drawable-mdpi/ic_launcher.png | Bin 3186 -> 2979 bytes .../main/res/drawable-xhdpi/ic_launcher.png | Bin 7805 -> 7396 bytes app/src/main/res/drawable-xhdpi/ouya_icon.png | Bin 67489 -> 101544 bytes .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 13963 -> 13261 bytes .../main/res/drawable-xxxhdpi/ic_launcher.png | Bin 19888 -> 19217 bytes app/src/main/res/drawable/app_icon.png | Bin 7805 -> 7396 bytes app/src/main/res/drawable/atv_banner.png | Bin 37203 -> 67001 bytes app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- app/src/nonRoot/AndroidManifest.xml | 2 +- app/src/root/AndroidManifest.xml | 2 +- ...light-android.iml => moonlight-android.iml | 0 16 files changed, 15 insertions(+), 17 deletions(-) rename limelight-android.iml => moonlight-android.iml (100%) diff --git a/README.md b/README.md index 46e0c9d3..9ab01eb1 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ -#Limelight +#Moonlight -Limelight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield. +Moonlight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield. We reverse engineered the Shield streaming software and created a version that can be run on any Android device. -Limelight will allow you to stream your full collection of games from your Windows PC to your Android device, +Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device, whether in your own home or over the internet. -[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development. +[Moonlight-pc](https://github.com/moonlight-stream/moonlight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/moonlight-stream/moonlight-ios) and [Windows and Windows Phone](https://github.com/moonlight-stream/moonlight-windows) are also in development. -Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for more detailed information or a troubleshooting guide. +Check our [wiki](https://github.com/moonlight-stream/moonlight-android/wiki) for more detailed information or a troubleshooting guide. ##Features @@ -18,7 +18,7 @@ Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for ##Installation -* Download and install Limelight for Android from +* Download and install Moonlight for Android from [Google Play](https://play.google.com/store/apps/details?id=com.limelight) * Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC @@ -33,7 +33,7 @@ Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for * Turn on GameStream in the GFE settings * If you are connecting from outside the same network, turn on internet streaming -* When on the same network as your PC, open Limelight and tap on your PC in the list +* When on the same network as your PC, open Moonlight and tap on your PC in the list * Accept the pairing confirmation on your PC * Tap your PC again to view the list of apps to stream * Play games! @@ -46,8 +46,6 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de 2. Write code 3. Send Pull Requests -Check out our [website](http://limelight-stream.com) for project links and information. - ##Authors * [Cameron Gutman](https://github.com/cgutman) @@ -55,5 +53,5 @@ Check out our [website](http://limelight-stream.com) for project links and infor * [Aaron Neyer](https://github.com/Aaronneyer) * [Andrew Hennessy](https://github.com/yetanothername) -Limelight is the work of students at [Case Western](http://case.edu) and was +Moonlight is the work of students at [Case Western](http://case.edu) and was started as a project at [MHacks](http://mhacks.org). diff --git a/app/app.iml b/app/app.iml index 979dbef8..6dc028cf 100644 --- a/app/app.iml +++ b/app/app.iml @@ -1,5 +1,5 @@ - + diff --git a/app/build.gradle b/app/build.gradle index f64805a2..3a366e33 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { minSdkVersion 16 targetSdkVersion 22 - versionName "3.1.4" - versionCode = 59 + versionName "3.1.5" + versionCode = 60 } productFlavors { diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png index 9344efc0cb10991f5b6687f745e45e30b822b047..37702b9fb0ecdccd6ed18a18c4f48b9fc97a3da5 100644 GIT binary patch literal 5283 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 diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png index b6c9106c7a5bd357a6b6019edad2744b655cdf0e..754f626fd98b8186b3e6e39e8ebf5001321bc6d3 100644 GIT binary patch delta 2974 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 ZmjpNE-jEV0VPIfj@O1TaS?83{1OSE#zqSAX delta 3182 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 diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png index 781420050d7d0a63fd4a71c1c94621e1086c95a2..89fa60a169446a5b0cf542a23dbcfb3a06b6aace 100644 GIT binary patch literal 7396 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

  • #zqvGAYS3{@YySb_Mw3YXz+6#O+uN1oyL~XbHXxuvS;l=$uon3;8J`Y#?y|e$5 zas8$i>mGh~?aJR36Wgb&Kju+6?%j1J{n$~}qnm1vMZAoaoqOfpwS#<3kNCbD3&(P7 zz8!X4LiiAGs%Yx6Q?EYluiy5x`>NP#_TP={Ur!kI^vK?FcbUi4dg%SX*6;N~vXZuz zMoN?31{GfAdhN-yS;Ms6{n#3FhjVhr!;FNKYZ7WxmcH)D)74s7qP<#c<(^G{K1{#8 zuf}EZMTOg}{ox?<(Z*F+`i8yL1r#}!C6cn_}J+)=OyGu)Zzl2?rW%#*y zc2BE5SzcHEA~a{4_jLWh(9qJa2c6mLBSS+&a~18Ha`|ejk4efM-5hXAhC_K%-sxWz zE)fw$6%`f>7n~05x@mdI^1n&&4%xRMv9Z3>v$D>m$)By;vTD_H}hnuU!jUwPKCI)vH&Zzqz~r z`^ja?mRWV5d9EY3``bT<#YJKa*#ZYR=N0X|Q~qzyPksKc^1Bp_Y=U^~9Une(6Jj^B z+&yi|p(g}jLqD0tETjK-(2f!8C#=7Za6)3dSkKY)6R=Km>9AJ7R(4# zeqo#cZjb0byM6s_yz=iFS8@H|>YGx=p*%bLotr0bPECmEm6&~ZuHL%#>*Kjt>%g0A z>fFoJMAltC@|@%Otiaj4x#yzUIN~xhGe35I?96We!RyH_FQ_MFl)|BXWmjk^bBZn3 z#&xl~-L6hw<$FD9-P8H{cKfaD?d5M93dS&h-02)CD`tI-rCgLfqFCe)ON)-eB%3Ei zJFnj@KNt7x?d#tj9t9oZS-uB(xmq7hGYn6R%QBJL_A2mnWO)4bi}!B*I=TJ+`CJX}1}SlQSnEpKOMW|A?q z;Jb6+%vmQnHfH6T^l))gGqWJ)@ap_)d zxxl>KQAM%q)DFh*YfILgS+HP%Xr7mAmBWF6lNTBojtN@qWY?coU`-c_M7gT0*iP9o4Hs2i~r=DcgSw*!vWAc2;^k}G!d|_374o`d zSNGk!cdLtvi_1;S&5J`%D_FVzs9)V*`AvPA#l;lfHYF`BEfY<}%xyl3rzdo%ZVZ}v z<$ml-y@P_L8|H}=u|DQ`x23qxdPgYVXF=`Q#a9io*^ZwnS-p$5YVN+92_{osne4i= zh}*SVOG`^&e`CycH7oTj87|E`uVgM%*XYd16!Zl zUoO^kU_+8a#h*uq{vXcT{%_Z|#al{V?37k*%Bg&GEumq7jDmu}zq7G(^b+sSey@Ij zfA^f}>8S*^3_EmwEX6^aciocT55@jg|%$1s_h| zeSTq8=~HL(+01W_uCQ>57Oq=h(6u!^i?_veW9*~7|I`^0_$8`Mc0A>H`=;fM?~mvE otvwo}Ls={>=GN~${!f12`##Y*uh*<#U|?YIboFyt=akR{0J-}N!2kdN literal 7805 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%0AHpzLZemi}wBa9l-VvMFqOjvq)?iao$ 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 delta 51931 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 bkN=AUJjEA%W?99+z`)??>gTe~DWM4f`BXB# literal 13963 zcmeAS@N?(olHy`uVBq!ia0y~yV3+{H9Bd2>4A0#j?O@LGx(Tzm28MK*h;CHqdYXTftK*A@zSdr4)5b6N*8jaf z`}WQ^vF|4BJ{>1C@oDihpKoR9>HEJ|r`|hr>cOqDwd|`dFmKYT`F6{F{a1z<<`+^d zp|2XV8A4b7URV1=%Hg`iiXi?N<`rId%ikWp&cHOiZo%%ljk_FqR#SyKwVrfLQ0O>R9E+Ql%Fr{u&uH?`>( z(+^~AjQX%&HB4J1vgcV%_tjacMpBo3RF@r@q;$IV&Xqfl>(^OSD!pI6CrQxfhvf8x z*if#o`Rpe|O=LvA8GmUDxZ1mU_8kc+$)aYj4f-fd4R zHbqWU`Q4VUE4KP(j?wa&-oDd)j$N93OzHE(HCx{7|2L=X+4(j5H}x=i$Y`W*IO6s6 z; zXX!QyWgGsE%+h6De3e7Zpy_7TvHh32@2B5>`Q*)fCXo|!ghCcfTIrx0{MegWYUZ@R zOKc@#%$m~-X9g_f^6sA^Hto%h5~a+y>wfpoJ+}Ws<5|CePTz#LuitV-eaUC`;NjEk z`FpiEb+=jRtmAiCiRNB-qv?S|=Mr--Ua1R@ zG`AYrzDQ-eaPsR9Io*W~Z8uXi^>j6DBSIqfd_8sYiTpNx`x2Li&W1

    t#Lui#1qS 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 NJYD@<);T3K0RVrM$qN7g literal 19888 zcmeAS@N?(olHy`uVBq!ia0y~yU^oE69Bd2>3_*8t*cliU7(87ZLn`9l#i(V}&`-|bq*)MQX{q4e+d*izS!C0r*KDQR_W2wGyE z+4Lpx#pP_ZxAxDU-7Wb1EOpWu6@AGW33I+p+Eaf2=eeJLd!<&?URie_bPv<4)jw`! z>+daNT_DD=)u|wKy@M{px>vX3t3PgKSTVJ6browCV?pTsf492VvpTHhiMgsBz|Bya zzu*4#+mP>5*1hxp{JbsZuJ)7vS5IHRbp8Ckdy$+RkKSiBKFVhO`=eTy;ZKChO~psE zKE9t?=dSq|1e{9@^4d$iRmc1zgm&Py34Rm<7F{D`x9`S`7r`i(h$%g;V6m}d3no6vG$t~q?} z`}W?Mm%BYm{|49F$eor))%o?TN)$7%h|T`4uwPF}j~6X7SPOW&3xt2{4E*RNHUZ z#OuJYK<@Z|&;7+#bABwooVMLcFXD7=`r2uqe0Co#I}#L8_u$LA&%4X~|Nd2Z!Yp3% zGn`N8-|r{`kL>!bOb(srG&pVl_Sstd=U!d%k~ed&ujesoUbR~5=8IQCmR~G^_Fv6pm99%)T<<4+P*p`|5xCe`ZOB)RzUG!jby43yXd#I9xa_Tc&O&%X+QoH0SE0Wd|lj@B4T}KT^7^-__y4{8sha z85xpC-al>Fk)?C^DhosYpSj%oeuPTjR=rm7cSqS?VX5Qajc54Y^-W8*-Wxc3!Nr9u z7Hx?BINvpxYw0^6xdee zzHjN8KMh;YY_k0ya`2XKFkgt*Zxpu*`r0JAKYf{NbTr&&2!W< z<|M;}%QC48-K#FYd^_#zthnnh9^P1Tjd`Jb2S={y7j6yi!NFn$rMuH z6H+`Tc{pRz)vWW|M5eDfeOfDK*T<)|_F{5+TOMugn%4N6$7|oAsT*`A{VzQlkV&g3Da;`c)s}Af(PO;mfDL7R0};O z3Yng4_|LwV`7hh-%UPNm-{|cK_;Iu?GFtp;O{0VueZwjjg$cj8y*jr`?JhBuWw2wFKRn$x`p%^%{Dz(vnR(Ql zWxwz(bePP3v#&dR&$jR3S|_!ZWW09TV=Q>#Hv8(MRVyvFr~JHVs($}?!K+SfNe)v< zhD9I3FZ}SGw{rfSmlwC`tC{*pEHZvAZS}?Mgkx^~y*jbSUndIg{TS|W)yl8ivi;Yt z5D&N23%0&I7?)-}xmvCM;=J?x`^t;H{gkn9Sa5A6+uPzSC5AnjwQi-4)-gKIFaK>f z?~=hk{YF3SwsPJg{Ndg>JyXDrXPCI-vo$Frw|4X}m zw(d7vr?K44beh;z69$dUTD2cv-k7ZP+-z39wbEKmaT$MF_m`csa&|nh>E_fr#dy_@ zDVFh+_4Uh3oF*BwYOIW%6Tau)h4S{eo5uf^ypTVk?EdN16$ksUtBwfhh zSpK>cPwoG!>A-(GofR{zay-9V*_p3gc> zeo0hb_Fo_``{l8+Aj8x^xxa7Y?-?w~`)u{t=VIFxrdYWYOTVjDI~~Qj|KFQ1=ZQ&f za>sV{rlm)!zCGIL9b8xOVY(}q!i4ZSzrHhS$%tk$d{FySb>{Bt_rLbM@34A#at; zQ$x&;yHnJcx%-6tpLso1<(j`lT#k-Ws-=ID?!KjAKMw2s`SX~QMQu5!>=QMCV7cs* z*7NPtzphv-dGvOd_uha-9h_6$xENd>mbaT1e*DrquWxy>;)_EBj*-wYxXZ@;PxWX4O)gMB&K?>}I%j z&M8&6I+O3|(QVA^^}qi9jE}e8LoxQblt-{^HsrT9(9Ih_yUBG#x`+>)^ZD;IrYU;j!^I&$|!SEpT$8Y`DmK)+W zR~c{cu+D|Q%9exwB$yo681r3PC}|r1WS)Ih-5G!8?RuRm0xir8MeWlq z>=)ab@43Fx{c>Edefg0all?6FojbSf2>a2jJ5_7He51lGLFQi5vkuW~PTOw$d1dkb zJv-88^RT#d9(p9PnwMeyq5FkytNY&m+WK#a&9*hJYI7K7pNrD|qs5)Q!miI@!FK5j zLME)q6Mw#F+}iH`M)vRTdmappvls%pK2LsTUQ)Bz^>g;SJmYX__P2c&{Ee~k+J6=b z9V^(Y-{_ET?BMMyT_AF(@%}zrlbrWY7K_L}5?$(bb%~;V>{iAD-M8*+S^n^G;n(Wk zJ&EfK1MBa4zBSai8hdby`>c`^VzK)^P5rC;Bx%7pb^(q>ZwxOSxhQk$b56AR`Cq9w zX8vq_uKkhIsV*stvBA!L&5M*-^0yqmG|S)n78tVWH``-^-YSx=j_}s*x%x|&RjgK1LxJo&0@d6o%RcOG zzwUMD+%e%t-@i=dG^_f4BU^gCcZ-XyU!s9pV`wzr@3s4zu04pVYro3*miKzo!Nit5 zpJGmIe(Ka{%O7p=mP2Gp^)jmh73I?ty%#*-bToaJFu_!)(N<2oL+a+No80F_>oyfV zxOKfyLYUFvUY_KqNu2@u^KI^&Tx7fCZ+`Tx-!rXrC#lW5oIbNi8wpMFPPZsmpMNv-4#larN%bjb3wXT?6YmKP2k=Vj^W8QvNHyJsOTN=|v zti5DEc)nhG!FHi~glw3`CeZ^&KXn(}YufpKlaEDc8B^bMxklxs?GN_6)roeTlm3K> z?X1??MzJP`2|Xt_v28Osy>#(T;X0WeiQ5ubD{i`+_|560Te@f7{n&dBhwj%Wu0Q^2 z+ZuD@<28n_)*K7HC-AXTPw$J$hZR@nuGqRVanIJDQZK}=3-3Bo-p==N?t+CoC;Cg| zpTEs>n4ga+wkP}N+oOInE|+nZ_`iAjd29M9zr*{QY&WbkCX1 zuQiEy@-Fax!XKcVy2_Y$(&<;z;%tf%ue@3~Tbx7Tfh}v2edGDBoL?FOm>iz`z4Oxa z%Zz*5qA%Y&`+WLQ(d+iDd+x6+cH8zrxIDc_gmwOxpq*^DX54bRza)BL`6b^Aa;^7P zn6hnoce_}Sft~FU2lsT=4dPY_e%4zUJohmw)CKHhUMbR)==|>d2EFg<7Ack-2}i!F zNPAD|Ts^6VLn z{OC;oEoJsgogbQ8C`J94`C##d$=l3t_!QLE$V*&#P`E?yC7Z$PGojN&%o&nY8AL*P z86I&?xp_Jv`7!WF5mnjh(%n$NFZYCg&X&HN-Epl#@l^+ytkn7 z>((mn{;)RqO zMMEET}TrQopov_cs1*adT47 ze0^F`T`G|vyoZwd?YPccZ$G2@9kx^3mLw`+d{F&&uvdmRE(HoD!%Vz9N$r^H7k`A7JQb;d>HbKU*U28)}zi*k-ug($IHp>`TsVV zT}69=$<|xfm;V0yJ%QO_=C3QPpKXhOpS`@q;@A8Ovd))f5)bKrta)}L>dOWDk9ral z|JiKme*faT6I;=OD24~mOBN*P*G&2;A2Ij4>^iJ zWV^ka_)oUJ#*S_E$Bh`?Opa{eJGh-W=tjP3ttR+>x+(THI9;8*+X&ZZ+X zb6yt*)!P`Y;O~E~d|iF*{gYQj-L}qQm@Bk9%yr(XFS81m8s>kPw`Z=w=MyeBl5d_| z(|FsQW134yX6lNpBd)EpZp>;_bKhV0^n+=$lV@vF>CTwn@p`2WuP*2Ntrzsv6<#Lv zRnt5nLXT?ye3|P!#%L)`IoWe7`j5!Sirm_A?BHo%_rnL=oSjd7-FsJ(k0G}wge&k(!}&uD z3|97^c&E*a<9I)32BT}6(}koZ9ilH;osCv$%sRuR@YsKgn$zAxZSL3qSO4C7Q+w0O ztSQggDptB|&2DWfoY~cv|G(0_@4ETLe;-8x<>lJfJ!)ckni|xxdEZ(^pBz)>s_Q!~ zHS(jUnkO>+xO$oW$D63Hx(yZYr1k#ttNpQ)I`{g#i_3~B&l9DTMK{eY`BVK>SX!Mk zOd*4>u$1w@*8Lu}JZt}7S@YEN`}SY2w)}Kq?iNf}ZjJu^_*CGE$oO^bKi~d4omF<| zrvd}d?Z=*t&&s&;88U2F9DDJRnZNOD$HGgmyxdI1XILK5c(&}5Mn=fBFL(0e|NlGi zuwK?)skq~4*eR{rq*DFK+4YO;SeUfB4Dapxv21$|ug>9fQhvL))T>}S^D z!Vf8rmw#ky*Z+5%QNgY85HEwemHYg;za;Ki8MD5Zym$12Zh!QIo9DiJLUkGo@3|3>W< zFVtSJU1C9D=4NmAI*U&S^8Z_S{uVqT`$+3I(^Xc6+Y4Q<{QE2Z^=EbE-A(Fxv(C4L z{@y(+&|p@BvG&>}zc#*_qNTF(ier46LWUJrn|ZC7PM;;?gU-A2L^9&impCLcFmT?S zBP_pA*hV-fO{7k1s)H+I#L`TM-~d14h{H1s9)~T+i`0_PpY?pL+@^o|Twx90d0BdP ztYUHq{C)hpsp@yhtJTLlS0oqqX>9R(c;TPZ7IjgnL%Xk^bISXzedfT7@|}YDYWEVJ zB_vMzJ8_qDLe6cTHE(*jDju%-%*4lVk*S55A@y6(j4A`>eLHq=m_*$1YVzYc6p`0- zafb9ufqLCNUtW92cvh%s75(K}6##hK8@oo?;DN zaz3?bUuy(s-8dd^GM|}OHT;m^_vw%R7(Olh(9ieYasDoIWzUjBkAk^l6byEoNL|)p zVLHRQ=bj$(q7W8F<^y{ETqf(gcZn?!X|2D-C{lNZ(cq!_YY(x_XH@?*OWU_i4B%a} z|A+mL*Zag5-sm`55)voNcE3SW$0zEVZZ<1(s;*HuoC5rz=$g0W>F12q|`6Pngb5<<=`-IC=ZxhR9KA)D# zbo-q-pDa0Etz>Uno5EOjO2^^)(t^2Mb`8-#^f|V;M>VSlxi3(f@j&rUNw`L&S>=D} z4KEJaTyS0dj@x-r&x)Cg&v0f=E6IMDzqJ0wpHJ`Bvq*)cY6plibZoATSZjZAezf@4 zpC;xnAI&`)XmLwLW}$!Qub9>EndHl6Ki;~h@fOE@D~q$NOiTWq`QlyDp>yy^-{s~@ zUxFm(++`3jI`}z+fx+~&&kbv~JG17;TQAG<_z|D`d)M`Eep<6D@{Sk$xVlYSC#1vr zu;J2;6Q)Fe|2@BLO+DB38RdN^tJ(fsTh_?rP*QjI*8ZdNv$o__RsZ7Rsa4DNbYAiH z&!lheybB#)ZQgL(=*66n%EZ|mOqQ}zS94@|^8fjGIBxhV6r`|)K~AKDAwaz(F+k{_ zMX|{L;+wcYYGP9w!zki84)<&fa1C#(}$E@!}j|`;>d-U;a*R z7hU7vpm=c=LqofrC4Up0#l{5{EkIC}2?^ZH+k1dm@-SH2XkPe}w~(3P=udmshyRZo@|1cN*2!^7U)xZ0RzLns zc=>v_n&CPlD-|>FF z-|=~G-gIy_f7TA1&(>!$qx^1?NB_tD^X4}`IAAj4bS;ynkW=UMfTjby42?{!TnuuD z`a3-hnap}J{H@<_arc~@A~wHCeH&j}@hzRJD<|BR)Vt=ic#+!f@ckR!#oZB}yX#nm zN5~xYC)1U9SQxs_<$uqVeb=8Q|My_zXUS$Wuhi$^S_PM@^_su>{v>RHw9*SkaYa=$S&dY%&AaKre3 zLl0l?Yl#>E1BRT>OB}eXSi~88etci_A$ZGi71f;@=kiz1`|x?i1#Rp2q}bG~V8u99 zsaPTAlQ&DJt&!dL@w^c4q;{9Tr#M-d8oIuyizfa4v-rAydXjN;Kv?kA8}^y?SJEy% z-PrT1#A8hyvo-UILyN9#-*r7w8i(|tSN{m^w=E$Aqobn*&;v$jNZw+krX5X0?YLp&vCwsxA)7&Bq za$lC8Z{NEt_4>{qBJXPD)IV#rHCh)D-Pms~GVCm1R;^rK3l}Zcv+#IF<|1q1r=ZZt$7}v{>XTDX5lf_$N#fR z11EgjZjn&1_jrQWbIE=0f)CWEE8H;WwR84<@a(*htkm5i8Jnw{sw$cI7&fXFC7Loi z9Qyxq;?0QnDo1}V?GXEE=)dS${aWXO*7^FccHCW@__3AsM#TDgq6sD(zgc7~g106* z{_5lP&#PoOzufDhLOp|m+Wv}X*Kbs~JU089?;BaE`OeA4_-U5vewDZRI>v|o%=2F0 z?Uk|2Qh*~(iDQPUzzcz?LI2lY)sSdZSdlfafx#e!+v;Ew|ArOmS9KX$t?V5i{{Q_x z$a8bZ)sq?gS^okRh3c3Nu6r~0!;fi>5h)#u^+Go>*=V|%hLm!>7M=57^Y^^q`EwrC zGTq_&VH$Ehp5enO|L`~Sv+D1?`{z@5_>kt&!lM??<6QssGb|`n+kWIe*Z-G43q6dt z@vu~Mq1Yo7x`ET)I&ZxM7|$(C07TjbZ$dqOQs*Zk2vVHj7E!+7niKTG!&{{wDI zlM}BkxT5&MZTW%4&e12>Z@4fN=j+b0?7*)%&jJUj4bl_^rhM<+tihf7oA?e^QtG^=;xy-}ta> zmr8~04b2H$X8jwMNiCe8V9>arI6*;yf#>Fzj^k{S-Ru4w9~tNyF`Xn?-iU*kVpZNIv&{+^&2c1^n<*Ydu$`9H%4fjY&LuG{N_cE2=O(rPs;KIU)zn|khh z)*t;7j!xLOM2lm4?@z`R&)?4A_t5dvIuRFKkpFCs&nM<1T!HiJ8rBDkKK?)F|Lk4< zum5BOEXFw?*3B**Qv64ZO`n~REj-+w!$D;^`}u*BgE)0NM2*)_IKJ*2Y4w)=cVTE)w4{QmNckFC-}Ry+?+ zUbrp(7MpXuB*zt=rD1*zLJOA&oNWyhsJ43d^zHih|6&^1LSM1}m=wduQ2x^R!N@d5zoa#Ug(*m%nB>{i{~=!#03^(5-8(oGCO9o&XUrM>0<$z#e1U$!P6-lfOCzLtUS#`l%t zyo*-ed%@u>m*+QQu2uHw)3=RN7zET$Xm&N|GiV(9zkA>H)Ne~$ipyUKsr*|n#xUVC zZ~hOHqP$(_KG>dm%fxXcMUv~$>)d+{f@`(U3mY)xxG;E#oM;qz@mX4%oq_YedVBie zEzXl|v#LA3?P!e`zY&*uFz(>V6xs7NXKI{6GbUWo&N+X)AfvANefho}uO*8-KfEk2 zU187gA)`i}`@+k;-|jzCds?XguUj5gcMRUevfGgI4MxAf?&rj|b;xiXjJ zO^pt?)MsP|J*k$?yU&wpxNo}>Z;s~bvMuZ!JB+oSa!PRA(uihaYv48Fx{|v(dLL{p zK=e%}gNODe(lb7E7uG%cBdGFcOH<_0MGOHZ8VL+1xF4L@H|y4)m8tUS?9Uf%Iuf)uOHiTf--5%J58Pxh zySsVqtkYt5rcX}Z^Wg;3jGof(Vp5Fyywes<)NYpMZRCvAvO31naPYM)=f3Ro;};+1C(J)|VcIMA>YBCpzG_z=usmydVO4;lmRr-A%0RQTc4sFz z|F_x{$Ix&?d(8~~6+DX9Q=6{|0 z{I%SQf8}gV_f-Wr{<17-XIrkwv8$>n;q=e51|2#KVHYknYe`R^z`CN|lIuuIy*gL@ zwMD0&-t?&Ui8z10Wq;@sPW#x?lV8tkC}sU0tS|2{Q9-Bk!n2nOFLOTKFY~?OJUjPy zti*F}hEF^jcAlQnw?1Y?^~d$QQgq!KLbw_yu{!+wU-E172e<2-tGIXA%b##G;9IV@ z<2Pr=a$$}WLL3SS3=<+7R8tsL7D=;>2{_o@b zV9B{(=gQBJ%Yp{`qAMQ#`KR~d&f)n7rGFnYdb;A6h-J910F%OX#?sG0&)!t#{adT`GJc z;eX$j*YU$f?`rSOaMj_Y^P_aygQPq?u8#5 z6|&3>y$&IYoNhMyH|O!JejmlG`D}`SerCbFc@74>4h456^GUvO+p+iI+YO)InCCGn zHr$_FrT?ek8T*D;8`i~Tr7gVuBg9xHJi@tUewf~F9qC{7<=jtR6u3>{QZ-t~DW}J9 za7t}n8vXLz93q|9}tDy=)eE2zu*l~4#%!<9exobC&S-Tlfev8BYy z^zlL#rfu^%ZIAw(b*Y8>naQKcS3h5Oc({CbQfb7)zV~_Ui?=DSGC94QaY8@CiN2fF z6<01+t1@Wx`to!zb$S})F)o^QK4;r1ot;~HZ!x@(yUjf9>;L?-6KWpnoHUGUP`u>% zd*ghE_DUwZxR`ZSOEgS#-z09Dolx_e^Fu^Sn^o=ImiBOamfPLiR`M|v`(0U>ytwaj zfySJ&S9yI8n~Oy!Cr*uIuw=NP%UnEJ()Zf5(y*$ zge|}Lru*Uk^y!I9m+;5+-1@)pN_(Np&jb_J6uzuWM|bJmz7oIuz`RGcuYNK_@Hu{U zIF|GHRtUj3{$T$|4e4IVVGd1`8K^e);dUG>LQ0N9z|Rs!B3jFZ9`c@+L^8{dCkkty2wDo zt+7PBbor)Z>32nf7?${D?Kn2^QO2?~Ifm~q8vQoTO^8u1Y1-Mw)AWtwXSBzwqn&;q z(%qjQI#sM)yPbPcf3bga^@EdQGkjImBNjC%+FwtoX<+%GS$%<1@7Ckn5BU$JcU3av zZu|D*=Ew9EAq`oL6^=0zbRFDoX+_IcXJ0is7$mpAy@o%c&iyQRbCB)-H_a9sbMO0X zc=a)^qfc@B{zYx>+36c36H3|xKlt2!<-35psV4NLF58+aj%}hVB-o}J9sK3J>u{uG zqzD_Y7Q>yT7oIRL>Gh9pWO%W{`$!c3DmLEIUC~Sq-pSrS9`2LhAhY0^%Apk-a{oNz z-0(u>+P4I?*52SmF`jhIK=HW@46pK(^k>SXPUAStaBqT|b3%~*f2p^tRaPoi1n#`c zwSD~#*}f^Or*JIpkQ6=q)RKX5gL!t)wc3MaO|}dZtn_j>?(N&VeA+6w`o4KkP{N+blUHPtD=(q!5x>kU#N<(}K0zL}!qv!EiP!K-`%kA>j17hm1x zPg@@GJ9F|1sSs5Lg;S-eQ4yu@zh5x3{M7w$#)te_4BY+|VWn9UruyyAv)ba5!Px)D zL}2#u*qRfaM+@Tb>=NmFBHwBleY_z!jMHk1X48XTvl2P^p2@UW@7pO{-XFM9!cVIA z(TT0eN@q5F`?BS-eCR^x*qe1cQIMg7|Sv{d)|YYqkz5%1PMgM0io`|sQASX!7e z-8o+DU#my;gi8;E0xOw!ruc7{ViaC~zTs^8_5+9QZJPFO*8BVTNYTC2?e&a%gQ^d; z_)32G_&$hHwqDoou=K5xj9!L=vx2i9eBA0E^NyR3@$94gm-j4%+LLc`GKl-er|+Hl zQ|O6|1l#rH-K~IbUnQvzmQh=lq?2aE9#q)t3YgGc;s->rVV8 zW9<@aaQ^I{<15WsxE%x)47oxc{?T6S@W>~i+_L1B_sjZ?SG0ex)?@w5utj&0)a<0m z$~`vuDW@k+DE!MZ-&|3KeZq(OE#+or8~s;l{8_8dZ9n(+GUGUg3m2{mzhOK0uuEaa zg;X1x+lEDpoh4_h9a^Kjc|(ZZ7u|_9pL5hB8*;2?=*{>)@oS#i+sUV+m5xR!9Mv+a zXFQ)-INo-HD? zr1J`^+noQILjUH?JF6qZAmW-V!m(K543h^#L309k6cbb917V2*li#mn-IO`zeYI@V zli~h)<9q+%TRAH=?T)A`oMYV-zsdOv>*v)o-=}~2*imr#hh7N<(p_Q{jZdj^pI<{!Shx1Nn?Q^e}Y>-+~TVeUu@nOBQ+Mhd2 zJU{MDX42h%S(rhFWwIrQh3uj4W)Yk$$q6YeQBCW*s}4jyj|@Gg{7Gd7^X3EJyv=q$ z?D)6w^ZfbuS0ph0m~f{-Q-Ga8zumyuN=21-?dQeo=B;B~%@L|B?y%hVwvuA_J-ZBU zldkTbgAq?Y?Aut#q+{X6w_fDA`+`}yYBO#qGtbNTzvIz@s7(ya`zA9!Sl7jnaEQkp?j@e%kQXw z%l;MedJpCKx%;>l`pvrCe8_&K(8KcRhJ5+7i9%{mINm%h*JgHLIatBc*y^D0*8hOa zW3e5D_j@P*ndK$>N;JCgi*!ZV-(sCTuk-$JUToi`?C^pi*m=*}Id8A;`F~ncfwMl~ z*rE@UGWzE#eq>%W>3_(3W?$wV%}<#!_AQNiEO?P$;^rgC870YX9n-Gv->&*le!Js^ zKXX1WK5e@BdCB@&^Y{N=w&v^yg-C{z>+Q8r8TATmu1Lsw| z4VISw&^`F0SJL41i=5E5q@79*CL-MPC74R*HHTXM-}QlMonfm(vzdgqp-9yGqgSry zmgXo&s&u>;kj!$Q@rU8c2HC|{2Trl?=eym)GW|pNl|Le{YL=b7Y_M|s`<=Et3>l0^ z3|>r6Vbo$|*v|6UJmtU}XD-E{EiA!R?dOF8p1hi;|Gs_iQVY4cvl45j*8X0ey+`h8 zAHzdFLlMSH@t4l;`f%@N<|NjDSL|VHR(>s=CEE5_h;`NXispmYN^Z~A5fWJ+7in@) zA$FzMwELe~W2UeA$0qsY!@gg}$A0!#9Q&y$_V&2 zjV!sJmaY77G2f!xW1f13rO3Tqnl_b9Yty$MY?#{LV9RBEAgi<~QF6=T2AKqF38Sy; zV`tRoZRBB?`i=Q=)i0)o8QcZ!PRYMiw-}aOu3x!&b^p~#$~u;|St>^~32F(3a6FgIM3&y3p9Ec2jyo6>8(+s#L_KmQc-tZQ}n%j$5{ zV*=-ykLOj_U$}mCd+ATciVmsYEuK>s$|l`9Y|gyT>3F}JTKm6)E%o1XW^8iiD&?9p zpYuQOzs$Oe>=h5B{kgte-TN<+;h$Uzr`q2jK^74u+3UQ{OXgl>c3ioZ=UwbShBw;B zd_RTE;qsMCc=Uf;_qJwM`Db_9&&;25e?i^^MXN({a}z(FSN$6t?q4rI)n!AE!CMi@ zEPICaeXp7}BnUmbZ^!=2e6Q(~Oa6!cD{}q6@^)Tfom=%SHoZS3hxQAwMky|K_^#V5 zUGszCQ}kTe;o*1%L7T ze=19E1{75sUpJfKbN%-xe?*?ozoX6ERvOax`L~lQ;{!&P26om}Obk1cZOY%*R3vYn zqAuXHq<^~m`?oyJf9^N(ZTPmQtmfPt?&6RB zH7bkl&rbc#&9KPvQH#aopH;=#$^9?%FLE8(^e^EPbI1yw)I+V@30tHRf7b4ZPRTNl z{}i@#e>KPc13y=5|2e}nXBvZo$l+R7hKwiG(><1UFBDkG&e0-}xx#V7-#2^`8vh_>g^TpB&ueo4TJ{tU>%>X!Y!nsQjJEXS|eNbed#rT(@tZ z_GjiBbq8NvP{^9Jym8avC_BFV`$n6+nBs1B&-m~^Ch+s*!@r;PUS7TC=g;&ahK{Wc zR~Z{JIOA?>Jmq{c?ZL7O5l!LOdYX2+Kikdrx9!Hy)a#y8H(p_G_|pDk|KB~i>X+p2 zu_!c%#t}(4+GqqFL0??Jh~_u$9Mform%^? zl&IJ(>2jCkcpc~dky@SQhNjuiRTfd9-!F=~=33qn! z{K(j!ZDMU-uFmwG)32Rp-*>}rrT_lC-ydt$a_&9DiR)r3f_@)nP~bgWDx&n)@!xxk z{Mo@4>iq6m)hY)nI87KAT$(AhPeXvcVTM5QbT@UIkmqLU$<5v&86VT^Pt}L~cdYrApY3*d&tap7>h0U_UN%@*o^VkvKm7>9hpEej|6FCh%Fa+L zXL(H6JYn90hSP@M7#v?O{O~=xX}Wqv?%UTNH1j$Z8m?OMSMyKj#M(vnI{)V9ZqM|~ zj63oF{ZjdlUPpy8=2tg=tQO{A&cH)+x>5KB8@wKnIDF0y7&of*{ERzy{ zGJ7u!{@ZueiJ|-3n>oCXxBDdh{;=s?$)!IUf0P41_w!HsaM^Pgqe4`J0%JpSf>2X} zSR@l4lZc%9u8zr7eeKV8h1t98dj2O);=@%3jVod+m;b%_x8N=3u^0Z^-c~%^bEkR% z$A8W53_oNRJ?3Y~@b&zm{fzkne@CrWVVjxj{|=Al6)k#CJPob1(^%&iEcjUaVgBxa zy~XLP!;zRYb_th&O7P8u;aCUAB*OBcSo zuwz~*`@wSeu!9})^A6W`Pwf9Ot4#LB+YePH#dVf{7ryY{cJ|$wqJKLxD)s!NR~xW4 zO?UV`XXW$ttOB1IS(wTrS*)TjZah3`ngYW*RjDU=lS48SB}5OcaM-2DyRW7~)5J65 z&G$)@5C8u4|H4DtmO1f7Qk`Kl3+?`ktg)!w6nyp3pM8D@co`N-nTRkH7`o2!+MaIL zbJ**w(XIIn?~{8RVlSUql})5ueDX(uk!12 z_QgAQ93_70A57kp+bLqg5aQLmsO{K)*ZAun+?O~-$h1c$u3`_=RzI*(pv{$idj-qA z{h#OWd(!_mR;2D?xmG#wOhck2IXZ;UuIG-au%|7CP?5+(B-~Weo{wv6OJpI4}$9un+&-|LVEbN(| zq4!qJ8_H^Dxr=&PK3+fYqnc}~jQi&1Q>7a|J$!m$Z{l^Y`x7|--}-3qUEtjTy*8_f zpa1WD|7AjEU0o0l!~dy_4;EfvblAdDAKm_2;`Act!?zy1oBDUdlw0kpH|o;V?`+Ot zxX~*S8>0B~DVsrgo#wCRbNXrZpHF`i;op+GvE!uvJ%;z@e^+WT9GRaJ$ z)n4Zpf8}@}?5!M9+_qEHL7>F{4Oh3^AD&5zCy6*d5cZg5I+SeNNQQE)m=MwuyrUTNvNuQg1z8}Az>vMU3 zvx&#~If=KJ!`GfMi1+E@S~#<2)mwuZm9IrEWZEuvdh%i4C*?G~`pIdfT%mJS{s`xB z?K`}>WIiXucfHSB81CE1L~$QD-z@XE(|&PskHUoMLHxVZIQG=uKEw4jw5(m$?XL-+ zovlUq)S~plvlY*#Z*yAutbB?6wof{@PfuIVy>BXmf?wmhE&sz`#<83Vd}4CK)`0tL zmd}~w&hCp`B?5C3L)S9IDh4X9=1coBmr2=*zc}8w`&-U`j$?nD3hU%{B`*tQK3^)s zC~4jsy!LdCa^M1exlbop)>k~)UywXuvBSM7I;|eYb@Q0y-yP*Faf@M|_O`5b%JMq~ za`WE5vn+Z#`EGIImtBPk<|}+^D_#T_)CK;2uN^nRT;j9wQ6`3p&(#iJmTQSMFdXn! zt}yAitPp(Vz>f5iuRM!q${$$M@PudeOKy+ZnJN~K6TaR3-xxjnZcm`^^bh{GRQf-! z*8j8Rg|xA`BEt!$9bfn}w~O}cI5Wh&S2tYoFY%N@L2ry?y=|L^s=%L;KlLBZY(MY3 zZ&|?B+WkMS?sT4i!}zS0edNo{rEV`T9l9B}#a_SmL-)TBRt6+F=tBRJ_cS+?yo;w0@arEK9ibx>A~jTX$O~Gj^0qo(i-31bx8JkIp_bD z*~eLJ&%O7yF`xH|so~fw#)fkfFE0FlQit)Y@C`v0^Pa}n(qCKD!+TmTi}6(c+I(>S zdT#y)?|Xi4_Eb3Wcj=k+#@FLQz9i?_%hVQmF8h1*QhUQULEb4$g%>nJJ2o-xT;VRf z?7@ROj?>pN)Uo99FIgt4u-40}A#d3+2D5hFIZ<;ib}B5X*mN^7sr1uf>vpws?_b*q zGrUvfPMD^{kY>JLk$Zs=!;G&7>x;|Ir?WPsMrA(XdK>@XLPOl@b$^L8@h;oU9nKO|X|7<9faak$UJ!_RO>;YO=Mg#Sq+o;4k_ z%L`tvT(#uKB{r`5a`!rm@5guApR=i6mzsYv;IfamN=FFNuf08uAB32G%1{d!S+rQrMcgXT} zZCqclddaN%Wq(!Q^$RMj`|J8}I{*H!vwyQQGu->da^{!cuW8H9ZTxJ?xog+-TE?lH z4|6dXB;I)+?K7n`^TJB5D$NT0M1}~Z%?!8)1vhgcZOuZbJ~ z&U-gMe}Bf!*e^SB``>o>{678qUnn!f+L;nUs_i%Ksyl=o_>yoZdzV$)@|pS$PX%*| zTqIdIB;FPLb^`|tfW^M}`-nX~&ZVy;nVI$+yv%ebMapn~zk<&FOj#@E&G`b|&Y z$;6t-z))VZ@^Lq({Zw0l@B35!-uRUIaQ37R-KGCUzH(Z8(Y&|rRm?I8p{Ih1OpF&5 zgiTt11)3+tDL7uA@sr8KvCpSYLMG`ut9NTL=PV0}`{_K3oj#>RHf)MndiXAbfJVR$ z1_w{Qq9*mV^Z9=I@szGIxOU|BqMfqwJX{uqkEGB3ad}m|szmwNU;hJ@HFJY5S07k- zK7p~}Ui%_P7M7&mMWPA&8b02;q5ehl*Y&NPRdd_eGL9+r$J>d&`>D zZ}YcqH1GWsWfrLV^4nuqY5%H?48B)*n}aG3UzwkNDE+s1lW%^w?KyqDsk46>92cJ( zCsXnC#O9m1H@9Uzy)SZha-jU9e=)x|e`u3Gy{^KeREJ^j!bze#VzhEwUaVl`EZ$(Y zf_=7%Z%D^4)JYq-O-f}`PWq;=vM zf8m7Nt+IM+<<)K-&TAL>oPMQ!tM#re_SgR9Xvfa8m)X4U)1>EF#r1XZtGF31a-HaY zGDEb%@7J@yx5>|yL|2{PbayNA4*B(4H<3BXRZoR|hC29w!lpg)A#Bk^D&UCp4 zx7{zY2QV_I7x^Fh@-_ePCg=FJ*w>$ru5_qf@l5@G-M*6giv~B6t<^67+w-O|c)nJ4 z{knszs}JuKeV{ve9UfAJ42!Pt*#hnhzCofOZS&D+dKC^ z#bK>?C-@ACZ1v9;hf8QqxM^PMfqp;4__zdA6z#1mc-6mTqi|;eP1TJ zg5PT2j1Nv+DYqlGZFx{xBR=t0$K`uWT9xbG7rO4)rSRJ0M-v&09-+E?5>sJ)FzgRqzHR6RN+uyQ`4~zvL4k^yq z``9?5s6bNWMrKv>lCRCFzyD4yKDv0uhs(2qcQW_?TJWu0?1ui7ciOdLbz5cDwEPly z`_b~_!TFcD#TZ<)70wCldLxjWaiu2wO@MFLL*9}OQz=dJ18JuMLOh;EDX#B6JbfzL zhDDovt{gwyVD|lB@?19O?OXI5XS%*(Yk0uI&dm_=bC+_qxXz8u64T_P7%R=BqFTdG zpO$W_JpOm0f&TFw+O~z;gpdB2t9ShR%QM^j^nS17Xy3TYd7jto*aC)-$2>9myx)pG zY|tq0_{leU-JJ%(MLzs0b$z$BN)_T8d?zbw9np4E=49Do74%nNX_U{8_tVsB6J4&W zFYv!Cy|H$~tHn1t8On7(Y+<->8)nNLkfq!FRo(26y7lvevnIa|s0zq)ImmzX?z%-k z3?HfsF7xv5`jxO;{PDk|=W3^||0H*G1^@kL9wK#dAHvpdSpH{9GcUtM#jq}}nI(G{ z?UK6G`##xEq4g)TNmT3?b|qGpnx!t?4d%YtmmQ8wxXLub;e{d5?LQ=^LJxo%^;ItmR`c`s{bO z&v&dZ5hyC1G>9**B`-EfV-21OQJo@PGja{cU?TvWT=BeMk_JG;h zZL2))Jo1?HQ9rDj&G(1%PM3QtCP=d^+x@6bnYT)Eathltg~d9CnMVrLPyS(S6H@Tl zoS3sHFvmF~dOPnH&rH^{lV&?jK5}e=*nF<9$*&#vSoTGB_o|y8j_ux{I_ZymQq+m& z{nAak6Md^T-kisp+FG#h1yhRXoYu*e%O6ynFX_ZKhi!Y?##EKL)@nOMRgBYPkYbx{J}@Bsq+*U+WooXl*nkYVUKHT zjDkVa$qM%xwx9*=7r8QSs&KwinEa|W@T4VYr(C zYnJe;-Qjb$22`w#{!s~Xl~jI2LivLkpHrW%<}#Y_qNCJq(Km7Dd5%~AE!@KJXYWs+ zK-GE%ZdHYICGLNA&(es^Ph+*cb&9Io=S7BhgRzu6Qci2fi(EOjCPcRB%WB zcuRES@#_K$zb~*|!m}hcHsVM-r+uC7*8B}?v?@3k|81>c;oMtuh*|vK0@vCn+l{ZB z)(lc~T`uvvLwd$8^*!ypUv!L)Se-Z_w83ZN=`+_Y1VY%Pgv#<)$KAZo#G1q+^M2va zl_z>z=3DPw+E+X6?N9xr`uCQeAEvh0Ydqzfs_VLfQNgWskL6Ld7a3WJS)yO_-^gUQ zbs9|%?wPnyTZ~0eVa`IkR<1++5B-X&3@^IY?mMo#fBHYcZ&7_!f^|DDOP9P-mUE@EZB!vkcDq zABuluk!u_46u-@TLx`WPigI86vcE1kxl~-WxD*erz z!s#O55;aeur%9|+{;gk8;W__V@sIxg*m|Pu+NY*k@0(?;=gzOT`FX5N@$uhByIN;4 zowWCUC-T{6|UrkV`1m9wdNcQQHV4VL~Eke?v7tmEH(#x?%$TLeLJx8+4Od6=VQMQr!u}M zwq$sr{gdg_EX6pR^p@=2x4yoM?;YCX5L}+p@sv|9y+?Ub*A;uook{&g-1eRN=RXKN zeJD8J=sD-<&P(AFKfezbsI$A=9;f^5+bX3$F$!1rS^jHY(7bZz#+@&Hd)T&KKEiQP z(Q8`Zj1aH?*Q1!W<}ZBx`OZ1L-s#H?)Ke=ew=9>)^xyf};iuv0><#CgVq0kT) zK_@?=#Xg2XK(}#{|AChiPU~l|eD$6CXnE4CFI6RdTj%gGx^}P#T!~`->hQp^a>?56 z1|{1$-$blsX1u$Xax>UK%x`lh-<;Q{Uspew6K{Ir5;6LE=>*t4-EE88|&?0P6AQp3y6)GZ*W zyQqe7e!BnHl&tgL_}2YteS6nti=FB=gHy~d6f~{AuY!&pv4BrdZT6CSYKXQULuTF2=v9JA_etDX;*PG2b(p}<@4d;B);I!v2?t03*%6iX4J_bXp zC#UrvY&pSjUE;RHteJ5~Wqqm^*e`fm$M~5yV|AFA=ArG+j0>_F11zsTRJ|i|%Vp!z z+QShYlV9*Hw^_=^o~NN?KL2uED6ZMRqtNvTXT3_G!pCJDl}v^{THD!b zs@25wOz#Mp`$#XXWU@Z>%3wdkOM%baOODK(BX+gTS6_c;^yAIb{A-GK9Ttv0@?G^- z^T~DIeL>G{Ue9uxbzO6sfi~wpq5J(DXXQQp3R0(aeiyzFUioasZ#Ef@4Bir7-m^(+ ziQhxiMQYYcGqA8}tx)cE*vGgxSl6^jb<>Uu-nw&F+>DiySGMta@v@WQztWe%HVa)VU5aE=7&ZM9bYHs7dW30$nE)NSTgBy z#JUCshnvY~Bn=cU{5vYc!DPBiYE4u7Hub_MA?+;jT1m`ux6>IEw4R(O60sJ$QKuQF zQdk@K;ps!)z}KsTF1^`aytp7i#q!|2b&HL>RNA~Iy zNdXQ9o%{!@G!NN+iTTIKen>PbuKl*R& zSBHA@@teJOVX5%cJHYT513rEV}+AHzeu0( z>5H>sf4YN!ql10$jVT8XG(31|a6M)9gV5WPuccmk6@zud{hKXF~pCR{L{)m*=A(YEOo zc9*}1JW6TmKg!Mb{MDz+)BGPMSZ>wsikerizy86R!l?Lj8?Rn^bnusqYIcC6fQ!M4 z1$rt{w~q3;Se2+}%sp#bwf0=B*TF-}f__Y?VcAy5qIT(t^YSCc2Uc^5D~PbpbUYJecS%r%dC}{x`>V{msvb?h5xlw|2$d zzZZW9O5WyoxqOX9eaEKPAJcfBI6P?5zqI7^{_N(5mrXu1OErIU6L~UihwH9sz9DJ` z1^*kShj)DG_^wqzsqM`z?E749Si?$X-%PNY6;!s=45r5>!Q%bTmFF#ExEo>L^p`3 zNy1IEaiR0{$n|gKuExu+e&3#7r+ZE%tD$dmUnGN=p_2E5s$J=Cl{xN&HAb_)^toYT z&c^VEHQaPx2%osB-UpTMi{?$W*xSBs`ZtpVnRuV1i|5O>aPpZ|KM#+dn7cN)S~Igc zaL3--^R4%yBPKH|2((HU9{;sVR@Cyuy=1DnKtl*_@>#poAx6(8ccmqzH%_*8IVK`|`!)>h- zo>J?5rmmLT{%m?ipU$MiJN7R48#%Yn`^L-fsT>!I{n{T}w3%(c?Kk}|le^^Jhlg~y zwhD3Vm^R(L6{MxVFBrre?Lw z7QW{JAN&))Je%-#QYybp+Q%xr4MmM&rn~-B7xMLMe0UTyPrK=zM&m;-HirFimZnAB z%TidMBm{_6tZX`QIy>h5Ocujk&z@xqh`f~W%WY6T6D+{x$tUxzW0H*X&Wt}pZ-^U9gt1atvuRStw6!{V(@;k>TY_4x>^;wA)eGsJ5hDz+E5ocdAqO%eBkl)@kHr~KGH$zOrJ-Nt3*m(A~2 z@A2wi!Rqk9JTAsql`;I+ZlUMjzn=7(;&JZ!RFmJK&rke{IuRwmz)Jhc|LoepISx~O zQ^g*xDvSBMUv*zU&zEIC>W<%1ZdzE7wRMMAQ3yA~RIy#(l9h^Cvls{PVyvQ-3l7B}St=5Ep=n#z+J8G1{E1qR{Z`9OUCQUJ?B5ns6KD|y!!n1`sXo% zfA8dJvAURleQUpeP956=HT|afj6Wn997P`NPVi}}+S_UVdG3W?IVE$wKaYf>>((B> zUH#^o%klk%lDhlv+|RR2$Wvul)5+V^z5mCf?iDXyy!hy~)F^IYfXnHppA&5|~e^4;mRR7+1^-~QtPX8w?f0}Kj# z9d};rUVZ#`o*Bb6m+5;xpR;b9tnR<>`0d-)-YY};3X6-m&1d=;O+Lw@6u)1`<-Mig z#7Zrti7UP@4PJfqs@Bw3CDql{-yR-r-(UIb<#OrH69w80g_{{39O+7Vd1>i9?`b-b z0umBC4qU!myvIZ36Vt|CH)Z7=+ENaJ-A8#OCZD`=@#f9SMH@F3{(g3L_V)Z+TUKrs zmsnS!`ra1x!Yr|4i`Cny*REY#@9pJf z7qg=vaV0B9L0k5O=Y}(-SsAuJTDrUJ?I{V%q9>c}?Ci>gCZ3t9J@{l2uDn^K>K$sF7+eegT48iUUr9(lW%ZKbcTxgS1sC~k_^(tm8s z7Jshg3U#)8sj4mZ^*x*2EvEbH$ET;Kt+)l6m8P+AGkn-u^7q%*!0rBf|JusS&zE#t zJn`eN{a(V|M~`jWw(YO|zb}s`Ylp9Ekv!e0I^FN!Rfd91adjUa9DF));zWHf6`}po zOJicaBrTMZ3?+Hmrky%<>iRO@+1naBwye9xyN;!ybIz10B1ez)+g0gWn3OC%nK5bC zEDiI;_7fwPtY5!2cK@4=$K!N#bgt|OQrXGH@I^UxOUA_W#^-G|U##!=A2a#nlU4ij z+0Opn6RaXMbJlv(_f@~&Zl9iech}alN|WYX<6fsAr2OyG)6>lI@gIMPRcu^Wv>=c3 zR5UMBpUj?@{C0m5KDO~nKk#+dw_t7B%wX_HKXQ}GkL~_{%lfBJ55Kfu`=g}{gOj_v z`^ETse^0+TJKOxaHFuLSNMXc=1K)qz+x(x}-+x}@V!cP*aTm3$>}=~V$K>zt$p8KA z?b>q<4>TBj9v(Y-RMYIG>J8w&rZl*j~t_9)Fz)SdFu6c*P7d# z;}n$6SqW|7lVxP!o0|0f-QE42{k`?EDndK2Tw!_mh0XS5X;snp0(E_T{ZF61DVdvZ zmlFxX1uPL9q_&Fa6gYnSzX<2#Z4>fI}~l|EiUomZaDNi>@I<>=C*`e$Yu zKd;wS;ON@O!OHOcbX?ufr$2r_`~OINY0yh`jy!4e<_}hL=XHwP+uL(VNF50K?a<2@ z5R}BGptp{n;oA8|?OE&00=>Pt9eqxwtYdXr8nm@$>tp%g>uVx|dk)MKIPIaqaH*kz z;pw@#*5!O(<*j53g_qAh$&x?0q{{Xu`;Xv+0ELe)#TllE%x3K9-peidZ+ZyEG+f(W5>wAISz`xQ|k)h$%C)xP7KYX2>4#}$ubza^m zyQb3YUX&$YjP)4MVH>fJAIukTL#_g#DaoafqIKbRV>uD|pB z@IHU%X6NV02hN!^Up(M*^u&r2cBiK1?`K%>!^TA~uqJ7b0^f$Z_NcYXPIY;uJ@Pto zt}{eu#jRhrWR2g?_-P-%niMzq{_$6wS%yyLRsU`1~tl9Pef$C9O#| zHnu-5?LJ@2)3As;jbW0I3>(Aw^3t-vP*+vs=()LBTcb)}TzFWiJwNK_WLJ@!JfR;L z8BWLUeE+-lwv)3{y5-?j0@fR@pFLTqeDuh%39}h)=K9Jo6g-?G+0YW$$GjkH<%%_1 zdR&wYKg&Ldx3E5bYuBy4#_#9syl{xsOmvei!;M$oMkTDyy^LL>C+=Og(n&YhQvnt zsx3aLDX?qTu9Ew;pJT6iuirZ*y`TFH&&f-F9*gX z$xQw^VRyiSq>UYgE+siV2?7iUemWU5bZQ=AG!Wk3v$kUK#VhGKMKjMNzV=vsImk8j z>HTMalkRy{?>ZpHaOTrYv)o%3!sF{qub;o3KO_0stj;S^ysj>;6Yfu_kFuVyJ5^QO z^9V;m3X4FJ*P#|2tLP&P9$tP)%XID^iwscMcQRwij2#ngzXYD%Dz<9X3MKQ_LqUQk zx2J!ujVRDvs~a8{^*QL+!DjaLw>EX;Ffv%*&wcDJJzrw3)h~aij%3@I4!PFZbCQG~ z{W`;>5Tmp6M3U)+kd2%h_c$gnh|Du)o?z5v&Y)2DvNcF-yX%_F6#YlV%{qGnoTRp1 zOAKuJK1b8y^Y7Gt-Rk)rSEhxe$xGc}VmNjE@$S3#-%80@>6{iknj&T!<)*N6QT<2O z!jP(cEdp;`B$!w}e%Z8MnBl-qCS!)hK|dKU+`O2{C=(dGD5BfrO@3{*X%w5mk#)** z`diC)e)IYkTl{Vow{y0_`w9hy8J}YJR1|*sw6tIL`{&P}g^sm6+nt!l#=^h!VdCLj zyS@!fI#LBp%!+o`^f-7L{%lTTxHRc)5rcAFp=LL0;?u)LuVfFrwmos$bo$Db|5hD- zmg(*7eZ0+A-uK3)RPL;v2eRJ{J=vKWI_LMB*ZfKG3Oe;#PVUg#QWf1VRkEu6eu?vz z?PV)u*(NT?kzz7?n%NU828-%t6H2?wZ|uI#vtfo$O3I@N{rpVFx!zCkN`2z7Y31tG z-CW&V@f9UM7r*CKX=7tz5GlUDuk?2M#lVZjvV{QxH^1=9I4LU~dVBHTulu{LtY?<8 z2q-LhJoyp3%9>TXXLEf%BQs$VA78=y@3su9cC5HD=Zxa!P2U9)zQwItwd~okY@H^p zEsji6!l%5C+K}&AWHYHZRweF;Hv0^P6$O=EcokU0yd%sc-C*}g6KZ~FJfAajf`i-AWnd;$M zH(F#f85XX1Ad_6JJTtdMC3TH7gN99L!n1~rD{C1XjM{!mv2dSu@m#t1&8a_)$GAQ& z4b7Z!QfJH7En8lcy}4&tdUsc8!Kn$RuNSiaVrIw>-1qfYbieP36BWmA-;RD^9hqZq zqBF&*t(Bv0S~Y{$<{vSJ%#4eeS+-33%&_19lS7t1!;H>T3@vBO1OKe3*zI-qn#1dL zR|0c=bx$jDa49?_+gLbC9caN(W;cocR5Ie*W3Mo(`W0o~_u9NgN3N(WKkD~>3_f0_SW`~g^Sj(uQr#+$oLS@)zev``ZqG3 zVd07e9WE#0JXMdp-E{DsSo1vx4R${%mPe28PEDO!vh|;qC##s!ftjn*?6;VHgO8i!;#dZO=C60RrAA-uc$nzi!gS#3 zcLg3p30}pNif4O|AN7hZ+2wdUv$LQ@`Kyb>^`)mxuZ!u$%}8Nf(5`<$p51_9Tiv&( zKR;|JuwSUe5$LW_H`orW%=*aK&Li(|YLdyJv{XSy-EO&E_JINn(f@u` zCZ@Pt30TPBz|O+Yy=cLO1Vz@QJ?Tqdg#}l7&U9soT)}wdsLRDd!zCr}BNv_6ojlvT zYsJEC^Ioktu~@>awOVVP-oLNSb2uIyXRQC7@wxNGiWde8(==XLHMg;Dj#$+2TcxqKxyw;L~Ji6;O*8jYk?0xL> zQ-&D}_etOTZD4JuXSc3AaLwN7%6nftet&vTSh$&64a@x*heQo8f1aZ%Wu;|%GiQ>N zuaBX6zh|K($A@*dXGO1{e&q99X(7L(sz+5%&yQg=o-$pCjlr(|=VR;pD(cI7J{O2j z<8BagQ^-5hVpwxL-0`p&nut%gQM6%^0}Pdxp`-2*feU!i%D;4m|&+k_m1p| zWFC_lmyi3;W-ely>JVNoKG!z7;?wr}3aLJceB&#>&xYUsp<`;AJ8k=PgR(TQ8!MJ; z8ZWz{sxx1D!itmjGyUE6?p(uKDBq&=;eWrp)v;YO3{sW6Hzk0G;mJB88-M-0nxEDqxxb|2 zzCdDr%i48ems{3!>exN@$FGOktVlgK`m25^35~`wX~Ux4$eIsid@Zp=Uqg1 z96f&h_{_u&cKzw{#VdL~MV&T1Wm}tc`Ih-LC5iSq4QB&b*iJPS?yr8dQdC$yZJ(Ts zh|Y|+3uG2QTBM<)bmn2F@uTQ4W2Wq6Qj@nR58o`8l(0CNFJHSYG6}kwNab z>A8l0;815#RZ*Xt+?fK$zw&-9c@uGtyT0!KUH+eJk0UIq?#-!8em}4F`}aV>i9riB zJ@?$We0;K7zrVQAz1QEHb}TRQGYV9i)N%9F``nuk<}J3DOFe(5?00Uh3-1&&i-Rc! z%?4)0a}Gaxai-JeRnBXXJpvCU!a_n{?Z5l^?Of~f_clk(WzGs2xjgxP?bh}_m3}Y2 z_xg9!XKzvqYp%cj#k1+ASj$=Kx>;YWm^)RKq~=b%x8C(IgGI#=o&#Gv{|ZfSoG{zn zt+{iF5$95W31tbEj?0VL`y#&SaRf8y#N4W5@7 z9sX~15pZALKUZmP*m(;@TjjYme}24X5h;vaXYyWA;8s*r^ouJmgR8Iq%X~ZOUk$^T zbYso^pE{@d_sZMWm1cOXc%UF+{kXTsr1F=@Gn1Ry3U)rWX$L)yOfj@PyiCYF<;#i2 z!(t3-w|VTDTkag*sZ+%zkf8LLk@4K!hs^B}S-~rQc1i@~#_G-+Wn+A5m; zo*Oq!IdkUBj^EGjeNV2*+0*{u%#5W5+D2xRk{%`HT#|ppVrJ{o8lkmpqww)trArtN zJp7x+pmu%(FT=Y=3#-yMB9dR-)NVbnzOkBDOIvgCsi5QEcol@hVxsPRy}E9F-uaU! z4~EIzOVyOn*FMuESMjd={9B3Xc6m121UX|_?<{-ed0CBDLaj~1vhs_Hu%M9JpF*95 z$?F*#oPYCeyuIwqoI~1-%`K-^Caa%ZU~VL6{?1I}**{nDL-*YIX>#t<8*iw!_XOH#al& z8eiDH7{$dOCm;L#EsoLQ8~@EU=hs+os$guWei%C6qNr($lYyM}SN?FaVuR7c2Z??c=v!>=m>Y6{D8=kVZ@G%_lzn?5FaPf-u@=t6G-xmixyy0)zBfKF0 z0OvJ@E7z|XzxLE?*Hzk+_}=m0&Zc9&^K)mFy}Y#abf3ucsBXpkq1s`4t<7rRG#yy@ z=i7&Gaz@tA9a9#aS(#QXd*t-VbBUiMR=TdKS=cA^>qp`H{x_l9&+m#AV5ng0le1u4 z>GNXlnKeGw+HW4T_TpH}C2IR(`din1w>*}A^2?3?{&3fFoooHhPhZA#E_yCoerKX# z?WZs6AG;m%Z2irWX3^uutZ6%CO;i)>wk=j}e|kJ6n>zddF&sF!mp$_SEzBs}79 z+4--(DVOg{7~KO<*>acer?`riq6leC{_AMm~XjbTUJvP}u^nr5!l zTv^}S-ydF4b>qH6V)h4*TM9K7=6`x@ohfqf-T!ku2YPlhG&n~~tb488V%nk>UDdyJ z^Xkc6-QA|Ft*ra+?aTgc7kz4a^))$$9sRk%tl~ENa*i&05~yk1*2-6WLg`T)*U{JS zRxT-=`S|UvN6d^(7d|(>|2Et5!8*y>uJWHuoO~8Di<@>?hx#a5-%9uyvvRLf`gzfJ z{}Ud!d#?@KAACE+P_B=A&!$bA+|rk%=g3brImP~vbD^m4waJGq9cCDu;*!;T#LDn{ z|4}97S%u$y+8G=!>YkF`s8FzO!eZV;67nvE<1|#e<4| zok}HVl-#CC`1;iF+%`U={)d;L?#5r+#Rp}J-#uYvxVh)y5z8u?4&qTgN zhR3@aZ{$CJ(b@2$U>u`crH-?!IJe7MMX=l6|5(+_>d#>TuLHGJbN)$qAi!%gQ(6mX?+`3=dWA)w5mKwpe)igjzBEh&@_1Nk=Y!RDOK)#*v3Tb{paX1-vyr z9MwN1-Q3<95Ks^xZ!FC4=5G~$b;-58n}0Jf7;jy;ukqFTD}txE9)>(L5pkch`d

  • #zqvGAYS3{@YySb_Mw3YXz+6#O+uN1oyL~XbHXxuvS;l=$uon3;8J`Y#?y|e$5 zas8$i>mGh~?aJR36Wgb&Kju+6?%j1J{n$~}qnm1vMZAoaoqOfpwS#<3kNCbD3&(P7 zz8!X4LiiAGs%Yx6Q?EYluiy5x`>NP#_TP={Ur!kI^vK?FcbUi4dg%SX*6;N~vXZuz zMoN?31{GfAdhN-yS;Ms6{n#3FhjVhr!;FNKYZ7WxmcH)D)74s7qP<#c<(^G{K1{#8 zuf}EZMTOg}{ox?<(Z*F+`i8yL1r#}!C6cn_}J+)=OyGu)Zzl2?rW%#*y zc2BE5SzcHEA~a{4_jLWh(9qJa2c6mLBSS+&a~18Ha`|ejk4efM-5hXAhC_K%-sxWz zE)fw$6%`f>7n~05x@mdI^1n&&4%xRMv9Z3>v$D>m$)By;vTD_H}hnuU!jUwPKCI)vH&Zzqz~r z`^ja?mRWV5d9EY3``bT<#YJKa*#ZYR=N0X|Q~qzyPksKc^1Bp_Y=U^~9Une(6Jj^B z+&yi|p(g}jLqD0tETjK-(2f!8C#=7Za6)3dSkKY)6R=Km>9AJ7R(4# zeqo#cZjb0byM6s_yz=iFS8@H|>YGx=p*%bLotr0bPECmEm6&~ZuHL%#>*Kjt>%g0A z>fFoJMAltC@|@%Otiaj4x#yzUIN~xhGe35I?96We!RyH_FQ_MFl)|BXWmjk^bBZn3 z#&xl~-L6hw<$FD9-P8H{cKfaD?d5M93dS&h-02)CD`tI-rCgLfqFCe)ON)-eB%3Ei zJFnj@KNt7x?d#tj9t9oZS-uB(xmq7hGYn6R%QBJL_A2mnWO)4bi}!B*I=TJ+`CJX}1}SlQSnEpKOMW|A?q z;Jb6+%vmQnHfH6T^l))gGqWJ)@ap_)d zxxl>KQAM%q)DFh*YfILgS+HP%Xr7mAmBWF6lNTBojtN@qWY?coU`-c_M7gT0*iP9o4Hs2i~r=DcgSw*!vWAc2;^k}G!d|_374o`d zSNGk!cdLtvi_1;S&5J`%D_FVzs9)V*`AvPA#l;lfHYF`BEfY<}%xyl3rzdo%ZVZ}v z<$ml-y@P_L8|H}=u|DQ`x23qxdPgYVXF=`Q#a9io*^ZwnS-p$5YVN+92_{osne4i= zh}*SVOG`^&e`CycH7oTj87|E`uVgM%*XYd16!Zl zUoO^kU_+8a#h*uq{vXcT{%_Z|#al{V?37k*%Bg&GEumq7jDmu}zq7G(^b+sSey@Ij zfA^f}>8S*^3_EmwEX6^aciocT55@jg|%$1s_h| zeSTq8=~HL(+01W_uCQ>57Oq=h(6u!^i?_veW9*~7|I`^0_$8`Mc0A>H`=;fM?~mvE otvwo}Ls={>=GN~${!f12`##Y*uh*<#U|?YIboFyt=akR{0J-}N!2kdN literal 7805 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 Mp00i_>zopr00x655&!@I delta 20881 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 N@O1TaS?83{1OSD-O(*~W diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 3604f32d..c5df69a3 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -102,7 +102,7 @@ Impostazioni Interfaccia Lingua - Lingua da usare in Limelight + Lingua da usare in Moonlight Usa lista invece della griglia Visualizza applicazioni e computers in una lista invece di una griglia Usa icone piccole diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 663baa35..bf6935d1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -102,7 +102,7 @@ UI Settings Language - Language to use for Limelight + Language to use for Moonlight Use lists instead of grids Display apps and PCs in lists instead of grids Use small icons diff --git a/app/src/nonRoot/AndroidManifest.xml b/app/src/nonRoot/AndroidManifest.xml index dc81b05e..3fe824bf 100644 --- a/app/src/nonRoot/AndroidManifest.xml +++ b/app/src/nonRoot/AndroidManifest.xml @@ -2,5 +2,5 @@ - + diff --git a/app/src/root/AndroidManifest.xml b/app/src/root/AndroidManifest.xml index 5e340b3d..8988f226 100644 --- a/app/src/root/AndroidManifest.xml +++ b/app/src/root/AndroidManifest.xml @@ -5,5 +5,5 @@ - + diff --git a/limelight-android.iml b/moonlight-android.iml similarity index 100% rename from limelight-android.iml rename to moonlight-android.iml From d8822392f1bd0183915b9820a8e590b9aefc18dc Mon Sep 17 00:00:00 2001 From: Michelle Bergeron Date: Sun, 3 May 2015 23:36:19 -0700 Subject: [PATCH 086/202] Link to site --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ab01eb1..70e8cb1d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ #Moonlight -Moonlight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield. +[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield. We reverse engineered the Shield streaming software and created a version that can be run on any Android device. Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device, From f1230d46f37844af2ada4a4478f1e7d5d1ea5fea Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Tue, 5 May 2015 20:02:53 -0400 Subject: [PATCH 087/202] Android Studio 1.2 and Grade 1.2.2 update --- app/app.iml | 8 ++++---- build.gradle | 2 +- moonlight-android.iml | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/app.iml b/app/app.iml index 6dc028cf..ca76e7ea 100644 --- a/app/app.iml +++ b/app/app.iml @@ -1,5 +1,5 @@ - + @@ -12,8 +12,9 @@ - - + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 833eee9a..01de6eff 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.1.0' + classpath 'com.android.tools.build:gradle:1.2.2' } } diff --git a/moonlight-android.iml b/moonlight-android.iml index 0bb6048a..a859e19f 100644 --- a/moonlight-android.iml +++ b/moonlight-android.iml @@ -1,9 +1,10 @@ - + @@ -15,5 +16,4 @@ - - + \ No newline at end of file From 9878902a896a7d2fd0e429fd51c4127a6201de99 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Tue, 5 May 2015 20:08:58 -0400 Subject: [PATCH 088/202] Use IDs to track controllers instead of descriptors. Fixes #64 --- .../binding/input/ControllerHandler.java | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java index e2ab24f7..01d4429a 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -1,10 +1,10 @@ package com.limelight.binding.input; -import java.util.HashMap; import java.util.Map; import android.hardware.input.InputManager; import android.os.SystemClock; +import android.util.SparseArray; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; @@ -31,7 +31,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener { private final Vector2d inputVector = new Vector2d(); - private final HashMap contexts = new HashMap(); + private final SparseArray contexts = new SparseArray(); private final NvConnection conn; private final double stickDeadzone; @@ -104,13 +104,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener { @Override public void onInputDeviceRemoved(int deviceId) { - for (Map.Entry device : contexts.entrySet()) { - if (device.getValue().id == deviceId) { - LimeLog.info("Removed controller: "+device.getValue().name); - releaseControllerNumber(device.getValue()); - contexts.remove(device.getKey()); - return; - } + ControllerContext context = contexts.get(deviceId); + if (context != null) { + LimeLog.info("Removed controller: "+context.name+" ("+deviceId+")"); + releaseControllerNumber(context); + contexts.remove(deviceId); } } @@ -135,7 +133,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener { return; } - LimeLog.info(context.name+" needs a controller number assigned"); + LimeLog.info(context.name+" ("+context.id+") needs a controller number assigned"); if (context.name != null && context.name.contains("gpio-keys")) { // This is the back button on Shield portable consoles LimeLog.info("Built-in buttons hardcoded as controller 0"); @@ -322,17 +320,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener { return defaultContext; } - String descriptor = dev.getDescriptor(); - // Return the existing context if it exists - ControllerContext context = contexts.get(descriptor); + ControllerContext context = contexts.get(dev.getId()); if (context != null) { return context; } // Otherwise create a new context context = createContextForDevice(dev); - contexts.put(descriptor, context); + contexts.put(dev.getId(), context); return context; } From fc2f5cfe4d998f14f51d8dce89033e03717394e2 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Tue, 5 May 2015 20:20:37 -0400 Subject: [PATCH 089/202] Manually pass through Samsung capacitive buttons --- .../java/com/limelight/binding/input/ControllerHandler.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java index 01d4429a..6c6e99fd 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -306,6 +306,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener { context.leftStickDeadzoneRadius = 0.07f; context.rightStickDeadzoneRadius = 0.07f; } + // Samsung's face buttons appear as a non-virtual button so we'll classify them as remotes + // so the back button gets passed through to exit streaming + else if (devName.equals("sec_touchscreen")) { + context.isRemote = true; + } } LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius); From be126acfd12cae92f455af00b5901bf2068b1919 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Tue, 5 May 2015 20:52:53 -0400 Subject: [PATCH 090/202] Update version info to 3.1.6 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3a366e33..19517e46 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { minSdkVersion 16 targetSdkVersion 22 - versionName "3.1.5" - versionCode = 60 + versionName "3.1.6" + versionCode = 61 } productFlavors { From 381d0d5e81d4dca10b3f4036e0bc53c4a8155ed6 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 10 May 2015 00:02:04 -0500 Subject: [PATCH 091/202] Add support for multi-window functionality on Samsung devices --- app/src/main/AndroidManifest.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9f8cdde7..a0076d6c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,6 +16,10 @@ android:allowBackup="true" android:icon="@drawable/ic_launcher" android:theme="@style/AppTheme" > + + + + + From 2a18ffcdba554b9feb8d06a38d308203f1275b37 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Tue, 19 May 2015 10:10:18 -0500 Subject: [PATCH 092/202] Update to Gradle 1.2.3 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 01de6eff..88d246d4 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.2.2' + classpath 'com.android.tools.build:gradle:1.2.3' } } From 7c8a108e286bf34bb7d3bd120130232e803fdb6e Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 29 May 2015 23:18:56 -0500 Subject: [PATCH 093/202] Use the leanback feature on API 21+ devices --- .../preferences/PreferenceConfiguration.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java index c5ad6055..52513a46 100644 --- a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java +++ b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java @@ -3,6 +3,7 @@ package com.limelight.preferences; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.os.Build; import android.preference.PreferenceManager; public class PreferenceConfiguration { @@ -69,9 +70,18 @@ public class PreferenceConfiguration { public static boolean getDefaultSmallMode(Context context) { PackageManager manager = context.getPackageManager(); - if (manager != null && manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) { + if (manager != null) { // TVs shouldn't use small mode by default - return false; + if (manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) { + return false; + } + + // API 21 uses LEANBACK instead of TELEVISION + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { + if (manager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) { + return false; + } + } } // Use small mode on anything smaller than a 7" tablet From ded9c9140db7a30ceb47f98bc966eae076333f50 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 29 May 2015 23:20:04 -0500 Subject: [PATCH 094/202] Handle being online but not having a known reachability --- app/src/main/java/com/limelight/PcView.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/com/limelight/PcView.java b/app/src/main/java/com/limelight/PcView.java index 03709836..0106d7b3 100644 --- a/app/src/main/java/com/limelight/PcView.java +++ b/app/src/main/java/com/limelight/PcView.java @@ -306,6 +306,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { else if (computer.reachability == ComputerDetails.Reachability.REMOTE) { addr = computer.remoteIp; } + else { + LimeLog.warning("Unknown reachability - using local IP"); + addr = computer.localIp; + } httpConn = new NvHTTP(addr, managerBinder.getUniqueId(), @@ -431,6 +435,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { else if (computer.reachability == ComputerDetails.Reachability.REMOTE) { addr = computer.remoteIp; } + else { + LimeLog.warning("Unknown reachability - using local IP"); + addr = computer.localIp; + } httpConn = new NvHTTP(addr, managerBinder.getUniqueId(), From 6371d364e1a9340f077a1413898cfa12a9c56837 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 29 May 2015 23:22:40 -0500 Subject: [PATCH 095/202] Lint warning cleanup --- app/src/main/java/com/limelight/AppView.java | 7 ------- .../com/limelight/binding/input/ControllerHandler.java | 6 ++---- .../com/limelight/computers/ComputerManagerService.java | 2 +- app/src/main/java/com/limelight/grid/AppGridAdapter.java | 2 +- .../java/com/limelight/grid/assets/NetworkAssetLoader.java | 2 +- 5 files changed, 5 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index fe33f193..ec372401 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -1,19 +1,14 @@ package com.limelight; -import java.io.FileNotFoundException; import java.io.StringReader; -import java.net.InetAddress; -import java.net.UnknownHostException; import java.util.List; import java.util.Locale; import java.util.UUID; -import com.limelight.binding.PlatformBinding; import com.limelight.computers.ComputerManagerListener; import com.limelight.computers.ComputerManagerService; import com.limelight.grid.AppGridAdapter; import com.limelight.nvstream.http.ComputerDetails; -import com.limelight.nvstream.http.GfeHttpResponseException; import com.limelight.nvstream.http.NvApp; import com.limelight.nvstream.http.NvHTTP; import com.limelight.preferences.PreferenceConfiguration; @@ -26,10 +21,8 @@ import com.limelight.utils.SpinnerDialog; import com.limelight.utils.UiHelper; import android.app.Activity; -import android.app.AlertDialog; import android.app.Service; import android.content.ComponentName; -import android.content.DialogInterface; import android.content.Intent; import android.content.ServiceConnection; import android.content.res.Configuration; diff --git a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java index 6c6e99fd..10978cda 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -1,7 +1,5 @@ package com.limelight.binding.input; -import java.util.Map; - import android.hardware.input.InputManager; import android.os.SystemClock; import android.util.SparseArray; @@ -52,8 +50,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener { deadzonePercentage = 10; int[] ids = InputDevice.getDeviceIds(); - for (int i = 0; i < ids.length; i++) { - InputDevice dev = InputDevice.getDevice(ids[i]); + for (int id : ids) { + InputDevice dev = InputDevice.getDevice(id); if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 || (dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) { // This looks like a gamepad, but we'll check X and Y to be sure diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index 5c91e43f..8c1bb1d4 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -639,7 +639,7 @@ public class ComputerManagerService extends Service { if (cacheOut != null) { cacheOut.close(); } - } catch (IOException e) {} + } catch (IOException ignored) {} } // Update the computer diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index 3cc0b38a..d2635c63 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -27,7 +27,7 @@ public class AppGridAdapter extends GenericGridAdapter { private final CachedAppAssetLoader loader; - public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws KeyManagementException, NoSuchAlgorithmException { + public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) { super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading); int dpi = activity.getResources().getDisplayMetrics().densityDpi; diff --git a/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java index 6114f4ac..c43e51fb 100644 --- a/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java @@ -26,7 +26,7 @@ public class NetworkAssetLoader { InputStream in = null; try { in = http.getBoxArt(tuple.app); - } catch (IOException e) {} + } catch (IOException ignored) {} if (in != null) { LimeLog.info("Network asset load complete: " + tuple); From 0c73e3d0ae97ce10c7285cd73bf8b564cd9e9d96 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 15 Jun 2015 10:28:09 -0700 Subject: [PATCH 096/202] Only propagate a decoder exception if it happens at the beginning of a stream --- .../binding/video/MediaCodecDecoderRenderer.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java index 13c2ad85..69b9b6dc 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java @@ -134,11 +134,14 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { } } - if (buf != null || codecFlags != 0) { - throw new RendererException(this, e, buf, codecFlags); - } - else { - throw new RendererException(this, e); + // Only throw if this happens at the beginning of a stream + if (totalFrames < 60) { + if (buf != null || codecFlags != 0) { + throw new RendererException(this, e, buf, codecFlags); + } + else { + throw new RendererException(this, e); + } } } From 1cb7727dc78fb465ee8106d1b010d4a582fe36b6 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 15 Jun 2015 10:28:31 -0700 Subject: [PATCH 097/202] Update common --- app/libs/limelight-common.jar | Bin 956717 -> 956926 bytes .../computers/ComputerManagerService.java | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/libs/limelight-common.jar b/app/libs/limelight-common.jar index 7368edaf155f4072cc342bff2467aa6baa83cb3c..d4f623326d184a1896967712ca5ed2128dd7b615 100644 GIT binary patch delta 23326 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 diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index 8c1bb1d4..d9bd17a9 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -393,7 +393,7 @@ public class ComputerManagerService extends Service { private boolean fastPollIp(InetAddress addr) { Socket s = new Socket(); try { - s.connect(new InetSocketAddress(addr, NvHTTP.PORT), FAST_POLL_TIMEOUT); + s.connect(new InetSocketAddress(addr, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT); s.close(); return true; } catch (IOException e) { From bd6ff3560328fea2350d6f50a8d497c58925c39a Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 15 Jun 2015 10:37:58 -0700 Subject: [PATCH 098/202] Update to 3.1.7 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 19517e46..8c14f625 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { minSdkVersion 16 targetSdkVersion 22 - versionName "3.1.6" - versionCode = 61 + versionName "3.1.7" + versionCode = 62 } productFlavors { From e02a0096359ad9d697bf25bd3e569401a6880e1c Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 18 Jul 2015 00:46:25 -0700 Subject: [PATCH 099/202] Add support for the Razer Serval controller. The start and select buttons are manually handled for devices without a mapping for them. The back button is ignored so it can be used to exit the stream. --- .../binding/input/ControllerHandler.java | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java index 10978cda..0613b294 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -273,6 +273,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener { } } + // Ignore the back buttonn if a controller has both buttons + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { + boolean[] hasSelectKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BACK, 0); + if (hasSelectKey[0] && hasSelectKey[1]) { + LimeLog.info("Ignoring back button because select is present"); + context.ignoreBack = true; + } + } + if (devName != null) { // For the Nexus Player (and probably other ATV devices), we should // use the back button as start since it doesn't have a start/menu button @@ -296,7 +305,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener { 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; + context.ignoreBack = true; } } // SHIELD controllers will use small stick deadzones @@ -304,10 +313,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener { context.leftStickDeadzoneRadius = 0.07f; context.rightStickDeadzoneRadius = 0.07f; } - // Samsung's face buttons appear as a non-virtual button so we'll classify them as remotes - // so the back button gets passed through to exit streaming + // Samsung's face buttons appear as a non-virtual button so we'll explicitly ignore + // back presses on this device else if (devName.equals("sec_touchscreen")) { - context.isRemote = true; + context.ignoreBack = true; + } + // The Serval has a couple of unknown buttons that are start and select. It also has + // a back button which we want to ignore since there's already a select button. + else if (devName.contains("Razer Serval")) { + context.isServal = true; + context.ignoreBack = true; } } @@ -347,8 +362,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener { // 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) { - // For remotes, don't capture the back button - if (context.isRemote) { + // Don't capture the back button if configured + if (context.ignoreBack) { if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { return -1; } @@ -392,6 +407,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener { return 0; } } + // If this is a Serval controller sending an unknown key code, it's probably + // the start and select buttons + else if (context.isServal && event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) { + switch (event.getScanCode()) { + case 314: + return KeyEvent.KEYCODE_BUTTON_SELECT; + case 315: + return KeyEvent.KEYCODE_BUTTON_START; + } + } if (context.hatXAxis != -1 && context.hatYAxis != -1) { switch (event.getKeyCode()) { @@ -813,9 +838,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener { public boolean isDualShock4; public boolean isXboxController; + public boolean isServal; public boolean backIsStart; public boolean modeIsSelect; - public boolean isRemote; + public boolean ignoreBack; public boolean hasJoystickAxes; public boolean assignedControllerNumber; From 1e70e1d329c0ee61f1086df16bb6cf6c665c90ba Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 18 Jul 2015 17:06:41 -0700 Subject: [PATCH 100/202] GFE 2.5.11 update to fix black screen on Fire TV Stick --- app/libs/limelight-common.jar | Bin 956926 -> 956909 bytes .../video/MediaCodecDecoderRenderer.java | 44 +++++++++++++++--- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/app/libs/limelight-common.jar b/app/libs/limelight-common.jar index d4f623326d184a1896967712ca5ed2128dd7b615..e92cd7098a91ffef8d9fef86e8692a4b3661632e 100644 GIT binary patch delta 9761 zcmeyj+3M|PE8YNaW)=|!4h{~6tsS2x^2SY0VLURuN0n1xV)aXAm7GtTwV4{2z>JA% zYnZ_d2Q4=iFk>ByB3NYdDpQT={55<$i`h8DK{Auyw`xq^U&E)cIZ%ENSjlDqdb*G#kV=8zzE`=52=RJ<68IxH&^7X28(QdUsVWp)aHqW zi4exu(gcW9MEMl3fz#(lF=|ZSuz_#$oV72Rm{oH=ft>&rk)FoNfhrO??Y; zVXy}_yPn)H0MfDjo(f|zBZxJ*=Z?u{>sx*h`JOwA!IpqU(;j>SE7=_XNJ+5%@K=Mg zQuR*ePi=jU^1nwVi~?`%znv!51QX)RM|Wsv=8)SA1gVy^P;*^RF&f7*tGmNzVlEvc$Y6hHcj zd)JDs4?MGOJKwb3vN-P#EBlK@O?kU^%+{0tuUk`c*;I9<>=eCX)s;UZ*D;sG?z^{B z+In+Ped_(*Plmf@=N)^dcPm-He*ODTH-3E5jo7!=k9pVP?sp=2OEU6wE<`<47uRhQ zo6G(72AeKN(=CIk7b>^3EP4>AUAeM%){41$?h|W;=dQQhBxS?W%Gr0v=e~OXwYJ%& zU%mF^t)ooi6=7aqpd#54q>BUXbo0p14Ig><@3mTh$GRyc6T6 zU(C7XTKV8j(XL6WUTWQP2zO>*-}tXqdd^XyXo5reM^}OdPxB)4Yy@Y zbx)mt^2JB4w8J>)g+hO3{ujXczOI6;3v_Yi4_EadR10esqkxb#{4a;m)40TZ^0P_w4FkowCSkUrhHwH=b2p z^A-j&T;O7lZry2hX6lYjG3O_|JTq+vSC>hTZ|t6(3a3Tu?<;*+^Mf@xc6;!zy1X|F zw*0+w=jcN|Bkv&YZ^a_-H*GfExVihb)rsb#B6|{#|BS6EbX3py%hvOVZy(R9%g+6e zGr6DUXoy_wKiIll;9Pw@*XA0jh0i1<{(k20)xRKBC3xtkW$vQz7cy0%dVi*iERJWq zC0!tpf9d;&JyY*3+WjKep^ksU{pIQ}wl{oH^sAflR z*whl%_?5Ch)Ve-}RunTXV!T+#eW<=EXzq5tw|#g1FU)Edzjf))9JPnW9UJQ-wv@g8 zs5)`$Bti4F20x6-dF={j*Svg?!n&Pd84GX0hg;1xv8jqnHVM^xSN%xy_`mz^p5+X? z>b=eHGkz@(fAsfV`<5hm|II(;+S`M-9IIi{vOoI!{)TVm84v6i%$cA0>+{x`qG}?$ zCcHmv_aw=rxBkR;=G+(B8Z`|0P758gR@Wc=zN6t)^PctBXIe~rp`)P7bNBAyxei73 zj^_4v&a$ss_IBaQMQgvZ?{Js9u=@C}pC^UF*?q75?6R$|N{ZX^cIKpz%0D^$OH-GY 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 M%XY4ckAZ;!0OxT2fB*mh diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java index 69b9b6dc..e5a286ce 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java @@ -486,19 +486,49 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { LimeLog.info("Patching num_ref_frames in SPS"); sps.num_ref_frames = 1; + // GFE 2.5.11 changed the SPS to add additional extensions + // Some devices don't like these so we remove them here. + sps.vuiParams.video_signal_type_present_flag = false; + sps.vuiParams.colour_description_present_flag = false; + sps.vuiParams.colour_primaries = 2; + sps.vuiParams.transfer_characteristics = 2; + sps.vuiParams.matrix_coefficients = 2; + sps.vuiParams.chroma_loc_info_present_flag = false; + sps.vuiParams.chroma_sample_loc_type_bottom_field = 0; + sps.vuiParams.chroma_sample_loc_type_top_field = 0; + if (needsSpsBitstreamFixup || isExynos4) { // The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag // or max_dec_frame_buffering which increases decoding latency on Tegra. - LimeLog.info("Adding bitstream restrictions"); - sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction(); - sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true; + // GFE 2.5.11 started sending bitstream restrictions + if (sps.vuiParams.bitstreamRestriction == null) { + LimeLog.info("Adding bitstream restrictions"); + sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction(); + sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true; + sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16; + sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16; + sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0; + } + else { + LimeLog.info("Patching bitstream restrictions"); + } + + // Some devices throw errors if max_dec_frame_buffering < num_ref_frames + sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = sps.num_ref_frames; + + // These values are the defaults for the fields, but they are more aggressive + // than what GFE sends in 2.5.11, but it doesn't seem to cause picture problems. sps.vuiParams.bitstreamRestriction.max_bytes_per_pic_denom = 2; sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1; - sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16; - sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16; - sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0; - sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = 1; + + // log2_max_mv_length_horizontal and log2_max_mv_length_vertical are set to more + // conservative values by GFE 2.5.11. We'll let those values stand. + } + else { + // Devices that didn't/couldn't get bitstream restrictions before GFE 2.5.11 + // will continue to not receive them now + sps.vuiParams.bitstreamRestriction = null; } // If we need to hack this SPS to say we're baseline, do so now From f1d7f556fd4f142b27316eb00343681cdc605b9e Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Tue, 21 Jul 2015 18:03:37 -0700 Subject: [PATCH 101/202] Bump to version 3.1.8 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 8c14f625..33867643 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { minSdkVersion 16 targetSdkVersion 22 - versionName "3.1.7" - versionCode = 62 + versionName "3.1.8" + versionCode = 63 } productFlavors { From a4f48876476a043139a72a7bff53d3b751d41af2 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Tue, 4 Aug 2015 23:46:03 -0700 Subject: [PATCH 102/202] Upgrade build tools and libraries --- app/app.iml | 17 +++++++++-------- app/build.gradle | 6 +++--- build.gradle | 2 +- moonlight-android.iml | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/app.iml b/app/app.iml index ca76e7ea..e93367af 100644 --- a/app/app.iml +++ b/app/app.iml @@ -12,10 +12,12 @@
  • #^)s)q>*CodPo0>CQ z?t74M>4xO8SAqUF7(jO-egZ# z`#q!3Y_hpo#v%0zj@_>=-ItPGRa&@e=7l=<-AmW*c)=X;vFmPLX;9CNxcNCp)@Kz= zi8H8Oa9H1={%KhJ)RVqNnL-}({*LkL%f2(yP*FknVo1=Zx3apb^5tKRC)-q{T^2Di%wF|wcH8%tp6}Bi z*1BE#?X%|OqR5QRHObd%(=W^Xo%7mebLPCdbIVyP&P|rre`r}RzgusS%$n$)?V%N? z>Vq%OTdZna{5$hzambG~ab6_I?Oa5S>Hq`ft>^82Lu>zV|#4R_6FTF}?kdMkJ1juV!Pn;)3Yuy>hm!WXck zf7?^>Zx%_vn=|L8twXr@m&;rKe(9XK{ei#z-LOgboM&DNpHx3@ z{#&U9)}Owf@lQ@G+jZu5&!3ss{O2EiVz*2D@OSf9Gx@(=bXLlXnzUo>lSM)^oTd5h z^e6K=%u%Rv+S17vV0rX}_go3vj{UJwcX9&+ZuoapPmojVw|mtbWwJEd{estJ-N3dt z6W!b|PUoAIJ@=Yn&wS@sCfmHsJ9w>fE)@O!U$5-4d`Z4#&V#@z9+UkIJDq+PRynOU zY}vNr<2%QyO}j2!_1|mp>3Me7C;tlz7A+F~VszQ+y1-txuXUjz*Nkp|Re5G^HSO7@ zJjVy!D`YOmyE}ahv#x*Qq|0;aX`nBk>l!V~Ls} z{;~gEUiM-33-d4088wPu{>NLSir(Ey3)on(v5c{`uFBT9wh+`*}a@vb(1tD}Jnf`-7!S`lhb=mqY|Qn5Ji4C>Q+Y6?1c)`N@(h z?-*9sS#8_{JAr4&Y5xZ zxUTJ!g9%bmGakNw9N?&*vX}3{XAbuTGZhy&)E}|jqFwT&iY@Ag(W1MX4Sqa!c=7y! z-(QC>j0t}f?tE~)<*d*6Jf1CQFV}}^-YaqKulF(v*U6ULKeBN@>#6%JZ}Tr|e=)1t z-@X04y5r;9=QMY`{cu+q>ud0+(!KSoy~E5;H5J6KRM{V|K$B9DZM5A7HgBG{9=}`IPhS*|3#f<_GL;N zPUH(7bKgo%LRQl`I^UE?Pe^&R-a(`LzwNw!kL&ez6~B&u`lGM&Z?D=;k1)|A zfp7R{thp>ayYbDJxD)pLf1WjENAI5cK{)BdIlWoOXKtBcyJ4^92BpbiA5Ef88?Wfu zyh3QhtEV@ww588@9>ks=Y<|97PyB-O9*^w$Jz>xHy9-I#PrC3!(EV}u!sqrMt#4H8 z#y;IL?V?G<^Z5%u8tt0%|4_Z_>PdQ68Bc#Z`T69G=cX?@FK=M6PLI@k_LpU%#nRI+ z4S%S+ygzCAmt+0&HEcZp{f%C!E8TJ1!n)%wYviB(4%-4BoO`a;xZ7H>C%U<_;rIO? z?>$3gGp_HfpVm<&Hs`@Yi#6Xr+q(tFuUk3$TTJ_|3pP`mo~gB8_E{J?p((;qX!T6a zFk|60``U$NyvwX_?JCpUuyF4#ffN5sdRGYUkPuq$OUFJ#w&qknR)bDFlk6gAzuY(@Op4VQO)SrqrOe*V!f4_?Nrq!vs4 z`%&{|Yu~0y+ozvc4K9>!ndCkBL4Ik|?yFhbp6Si)x^gqCPjB|JRXeq$gN|%AsLq+k z+)=C|Jx^@Aj&tgSg+4FSOzjJGCLOg5T|M zP&;XKRcPUYb5nIMojVkvw&G5Gv(4^R4xe&fyr?|FKQ~?cl}+{M38yk-0(Pn%{#kZw zSC8DUw};ff#@R1@IH%g~-`hj`nOl{M-Z{-J7FR3E+dXyZv{w(=H^n+$F}iJnXMexAen;oMhiQK4-J5Tm|GZYS@Eq z(bKgvA6Tx5WvjnbxH@>pnn!7+mK&^_t5$lRZIhNP*tho;*H-bhTw2#_^WH{Ta!P#` zGWoYaHvCyU=Lxa$Xy<2{2lg-QmfQU(a79C0^X)V9H!4LxT03js5BU zE_f^QU3+lK2UDwJ#Z$6#53a7Y@a{eISoLE~t&q_3V-q9nep$}btY2x(In8q4%{ApR zO4E-WmXgg+f5o&WK61rDw$SYNGjBLB1*o=$Ho8<-iF>cQ_^0+^#-81Rj6c&PF6VA( zwD-LIqioe9>q!@O9D7*K{LDilb%}!{-@T)O=iYTLmRWQ;r{g($W1yz|RZFiJtG3^( zk7*QMU$I+dA9u%}n<~C~z8OaK3vM0HDV@A{Z*TH8F24M!OAqb(v#B=3wXIZL^HYEO z`M~QfHTph3?4p{VSM9U+be9(1u{%&&^KzBM;tcPJ?(4$ln*5ENv2#I6n zFU@_mQ1mDFwKEGYU(eHvm~gLb4wtx8`?Ysdw!QYMw9fvuHM_jd>0{HM>GGC;n-%M$ z`dY$DzXjQ(+q!27)k(+|wfNY1ZT=zg^n-Fp&v))07nd9eUAF7$agR-(gi1}XDOv<< zv|>rss<>NI+WYNH^0r;~l`jWe{}iP0eZ#K2oC8N4ca$8kwRGEiy+7TBwXRuCf8^d4btQzY{}{ma)9&eyU%w1L)uytn;rJd}&l>saYlW88tlybV zZSgD@_O$GJ@>E4U{(Zrm*B?%sP+LkMmcqf7xT$y#CSBSFgp> z4kx|&Z)Sa->-Rz4lKsDqwcf0mazE_;k~9C=1H9QePAp9AV~}NFsIy~W2!PM@b23G{ zF(AO^*3X~8^V-uvQ}WXb_!(KJ|F7i}*}Ui5H6hS+?c~bOy3=Fp_~f=f;ABh(D}~Ie z*YT-upUK0x1|rm7tFgU+pK&>uHyykfpn#uIWP6wpV=#FBcl#PK#$rZ6H0v4ECnqXN zfvw%{FTt4446<_jL21UbV1u?#lw%Z!uvRNDPGP~&ZaSUOnNeYTfhuDrY#Li*dO;%} z+w=leMuF-6O?+(I6?7T*@q*2LXvJs^wqf!{BZM-p>731cJlm7)7<<7k1uruIn~~7W zC%E0nnK1?IgvkYa!J62oe~@Nm*?z{A@j6(1`ax+%^~CNHC*0cxHV2zevWH;CXb=LWt=LTKUAcYbG!zfh8w{ zmRu}oJmxzBn-BX#rH1VSXaXr%D0Fq+6|Gprwi8dd4ToB)bdF)-TFNJ zVJ)A|^rf|YGV&mQM#WTq+9Slka7>+n!2pr*85kJuOn*?zXRiQKwB+8hYF{P>22U0S z1}hXr-#Di?Ht|V&;nc4RmUgIw3-JZb)b&V7;Fmbkrh6x% zp*Z+yFhuEsRz8X8A+3DEV5eub@<}trN5Q4E!J_9tqTUH`QSs^0ZO~L^(8edt^gazP zB{aRFjZX?}cgQlux2L!m82H4|vyFBJME&VDK2@+%`G1EuDswR~927wh*WhA^QmJA_ z@#zljP|y0e^GP#Zs)b0+tYws(J`X0fww+I!amMtM?R@@>Yo<$f@R>7io*vl2=g5?s zG<`}3pCe=E^lKe_4oqg^(x+XnYcn-4G5d9gZC=Ns$P8jkF0j&= zE|bS6w3&y)Rt%(Oa(^vYglBVu+y!Qk$aH^eMu^bl204z+{YsvUVBth(Mvdv_`F!G= zrBt>O3$+ep`CiexhO|LKJ&oR^Umn3t=9@|!mV6@c|k?yuF}>=(L} z9W1@kNPDthDEH+53GADVV<$t*fC_H@pD>jbUDb5GGCuarx6_`0m4bt8vO+1}E1ABFea)qyaZwN*IwlZ88LaI z5m=4T<|Vy?aHoQFNpAi#VshIlNndrZDw9y4hiwv&Cl6D=4?*i#Rit%T(Eb)0CQw-)a30K zOs21^;bY&Nd(NE+B(iz?g;mTT23RVtmXCe&{44Jn!6Fl{S=V#cggo79|NOK{xlxP* zheqPW2F6WNku07KQcg;XJr{U#u!YD>nw^qjWPC=*9^v`pa_bvYypDX|T z_4~drMuKYt@}-u{5Zm5fRjm?%KfYXq5v`1pj3*)oF8hif1b>s(!q$LzF+f;CQ+s&M_v5=} z>%_ZrAK^Rdn2@C-)PHOhbEA~DP;&uySyuhaZ`mIgU375m?%`jpXfDzpll1lLk0zhb zZs(64_~28%ed~TJp^sl3?^|bo*tgF10spUL=^2~4x}q1Zn-$PL)!XO!j4AG$?`&ad zue|q3R6Bg`{;IRA*{jX3<*Zu!`GS#_*@}!Kmbck6gE*%e`F6Hu?Wz?_*A5T-cuq<& zD(@1LRsHPDs=f9ZrpKzwq9=;of7hU@CYbe|HL6x{{~qhPm4{~C7ynRyMCh*R@fyP~ zaRDX^Ctgwiovl>+Fd`#c<;v^#4IhrKWm}sv%X*(0uhsmOAHImi?YQH-I-}Q9{6f^S zIi-C}(|ylO-z%2Wp|r~9)bgEdYtv^L-z${-vhd`_&4-+&>sLhGxOt-F=8_#h8l*(7 zna?!oe#>5dTdy~V3DmzPQSa-j0nYb zJ9&@i1Zn58o0wF*e!aK;?A8N)zGr>7U#rK2s2Xjbxuv2cBIbqivBZoX{{5SSMbVZtft`qggovkQ@ZTPnA6=$>BU_b^@Rha{ujx`^%a zGlDH7jSqeLCTMo38C_APxX9BcJ^wU$Dj69?$n|QJLa3}rHIwb!u0J-}l@q?wukC2$h6#pOQnpL`?&{2AtNM8;QM+SC%)HFo zhOvCNL-Tft*2L>gvCF&kcGGriu}#nGD;7-kpHU*GTYh=2P|(>g9T%5eK+}W$m)y47t3W+5 zwZ9cz@jAQdSiz_IX-jMhbz(wHHoCbiTz|3L=a*hBKTpf!i+RhI&McF@t*Tw0aB2S~ ze#Jry5xHKe%*+4IE|EWN5`JUTw~jydrZ4YL{gM;eTwGswr83Nu(NKx; zsFFHotWnjcT^rx+T6k)KJyV^#*UI3lo-Ipx{^ehoKiPx7sBF3K73caz%t?m-vo9_F zA~A(A!pX@p=jN?zvrWT%%Qn|9kUP1-X6NJkFPE?^FQ2$3?Vg(!`&0L?hSrZa96Yln zOUYvIis1Ago}2v-PMf`XM#F=fP4(AriI;6%?9e!KW8$XS_l_kV`m!o{LQ1(!+RIbt z4$GPG&THD37`gZ0jMfVYpF-xb`EYbktXLX#S|WdMo}F`%a<%t^aFN2J%*pdOT?^#S zEIlz>)93JoH>N>uj5iNNZx(IzuWp{ZFi_6&VB-gw8Rc5%%zrS=UFgw&b>rHVcDxs+ z*ME$e{QP6hN-x(px^tJ8SEe(YFXosXD|mIy&x5zx-#of=f1}*8qpWKBkB+XG>U4s) zL@QH$&5doUQ+jev8=cssoUAeXrmAK>|C@tdIh&H~_B$lDPTt*@F1T3CLEU?%*Zt#M z{Q3(gcm(Hs%CWnAcIl*U3(loqrgt;%)va1%@YBYz-ca1)-N~n_dk#IBIz?GPZN|Ch z%k%XA$jZeX&gR{x@bayc__o~7c}6=YspgftIoW#3iuTptTUi>q>0#AmbAwa1;WqEy zdpvylLB>4%@a5{7w?;dUJ@Gv>m;JpT)8n)$$!%>0@59XVul-~$5u8%~q_1Y<$>|1N z>tf~{J!WUktR-1LfA__kTXSw_d=hKomtfrM8}Z97r)ct*oyuYU8fle>^W1o!PAoaY z7rXP$sp(RC{+j%=H>%h6S-0IMZ*upvIcu-pTXxIWWBI+~e=itJ)q42IXtKU|)VyQ5 zy1R3#ln&}mUO(mCor0omKKF8qcHi5v?pAdC?vrOH8CWSU;XXBaYOGp)`D9M@{v*wf zIn{!{_R8+85oPDOcX-R6T8Zq1%DMc^F2&nSu2oBCPi)*L&#&pzwX5Z@+Ivx_n@z6M z4i;>0J*=L4fA6eFp z_!nER9}5Fpz546NvyErnR(TQ1xwdZd{FE!TH+L@; z-e-4i>x9kQyqfM`R*OH&{$$Gjgio7$lFmDbhCSxKamQzijd5WlSKhqG&w1^*%BA!B zPU?R!e3SOJ{p6|Ayn{RL+~Q)p$Y_`ML1ES2&Sx|KsLDBUKJPa>e*3oI7OTgMv4`FY zFzL_yV>G|vvGdzwmA&=FW{*Nr@2*zczpPu*)0+36*QX`{$-KGt38rmHr3))IBwOe{ z*1ybXzMu2(Wa&*!9c#BV`6Vp98W{Otq54aP$+Zuq>yMh`GiBwuU94>T%(PkCp^sDV zr}ZD(=Kb87Zf#6Ezpda`{8SsiZdJ@At1i>Ot55QOa-QHj=jZ)NmD^*Dw*Gz)Z}4aS zhyBujE*Gw=z5F%r)A=9IIaGH4PMQ|$U@&*;8V~nSF}Z(&bC2-!dPdeu-=!zlYd0u;UmoZk@vC?Ce+EmgQ|2Nbclq>={jM=SWaK7d z6@4kuH7Pgluf)gIGBVd1H(l*=Db4*{KjD%%-}hVZzkPMq%AEMB@R3O6os#3u-#=Qb z{qOZRinY<(vL}Djx4%ifb6I1T1bc)}uY2Obt7clM#xS>H`>{A35s&(yQz4P3Sf}1N z^6Kv+uHvpspPyFM7xZ6MR1TG!y7*3;(WKN*D++_3YMu1{d&0h^!!2mz(%?^4e?x;e zyftz5b#;-j)^hvm?7UDUrX)(zCTrKM%vCm;Dl&p9XW451i58qte`Q$Xx%1o^9nnt$ zJ}aF$6aB5zg?2fAoIml#F3&Z0>lNZXR6n<#T*10**79@L=1)*7&Hl*PtD&u9&T8E5 zUzr^LY2T8JH-GO`RGrHGR?Zrjc%nDyu*!5_KjjTNTsuWioz?jrA-FLy=jbx6z@^{J zpI60~d^zj5=| zlkwo+_zUeN6)i=EoCJ08}&w9eYSQcv`#1S7Lh`;px%14V^ zLbr0>iW}VBY4~{h(p0Z~Q#2D|%PyXi+9`1Q+UqCpdChLs*DKUZG?ZUCd1P#sChdRI?4xb}SL z^;P|nT&#%~Q??sk{rZZn(Ac)`oL~KQmim>K=9uaBU7o(@z~a21?Gmq^ecYdv8j-px zdh)UTckc_nF4Gq2+Y_I0H*uB!i5YjEXtdZ>_w!PQhD@w}z4m*lkcws|G0j?UO#1iK}rvUh+I%R@7E2?T*>J(UI~7+w0642rn>UohW~aci*Gzi zUdFXTZhIBeqMN%P|48|-?)$ZJXSU&&e@yNE${+Nvmh)cy$XB0{ec<|a1$LcBH3viL z&v#iAnd&fqU749z>f%-VQ>m5f;J)t}+xsQcjVn(#tJN@hY*_vGSMq%4t(Kmq;V9%4s^S4Z? ziM@J0=F@|j{=p}XtXa7$U+UiS<$LEhewWOAZ@f{hKW*XJPZPtx&-F}y`CR7tO#kAq z#d2ql7q9#Cbwb^)OhwUzyGna>j|cyoJ?pED?!3qu3znV_zFAy#bLWq*4?MTbimBlb zeG&Jwd|sT|>HYO=NlMq&30?QzvPd_(BW&wbo(r#@C5LFnzFv~;9PRm%A+}@REX7q* zKXyxb#Vt6S%DJmzw@KQ?Ym2W;S>WycMAvb;>|*VUyNbl#Ui&M1m}hMRU&pG2yL&Hq znwAt8s_bgv`WWLjy>Y8{rtZ%beYaj7zTy}@VX374hMP_vGl?%OuAmbs)f zv&nAO0_n7SP4ijqhRwX|zw=V~%-Biqr50FweL3TwlvcOx%QKzWO`;$-EA86snxI%=;F^d-R0&UWvQy_y0!SIU68w z!@qNPznn_H-K*Ltlcmx83#TrPUc_@#xm(UH-YjEXQB=EOo!QFZ8(Oy&%w8;TJpJW= zzrfrn`%*VFy1z8MP$N^^`b~JnW!GNCEv~)g{Fj1xQ`fP`9-O%ULPT8A95&IZ^6Ee8 z?`K^X*vs~{&Nbwk?rw#)JEF8{Nd_SM--K5hOIR=4f$^ac6( z@0TB4bmRTviTb_rd!0^ueGTDP``hx8L(YEk+qB&=?f$ZllxOXGu|v?!rNs0e-vQC} zjjv@}ykoh`clJ%Y^fkoC{DRu9&RH+gvX(|?n%}KwTf9p)t(e{K;pB)y^|U*>^(P$5 zFYb-{FP*dV<@7J|QUA*?*DqpE=-v6>_5WJgKWe{3|4K>4GyMI}f9_`M-KHZzP-Bc{hY&2Mt+TRWt#sj@PCl$Z1?nHcEKyj%Xc>O)(YhJ+j9KyoEG-tisG-y zKkmnSvAxJh|E^<5v7k8F5; z=E}9t8V-*P4?KJv;`TSwXG->6VK?y)I%{7ZHruSelBcq%=CjnasrBcJQtqY{7g- z3Cp(aZ55uk8T40+H{6;1U{TwpO*VRxPd2g4ukM+C`nJ~Z+zH|Gk4rBVCKgpF$7OxI z6!A~Zm$6Z!V5MNK_`>y^pjnD!S4d^ zUGukeuY7K}Z_)fSr`}vz{Kh#tt!zn}+~ad~pArnXb@E5vY>ZJ4U`!*ljJ}0=~vb4-&3%^9Ry+`)mdiLst z*uCWo3)NjK?W6PpOm_6!Hwjm_Zkl^7@$HLu-&dS`plYTyZ;A0o=_9{2IlnN?IH}0! zrJ8>yD!bsw-5zZyh_%Rd;{u=lEm9hTcAohx_{NyQg_%On7DTLoQ-pOfcW>mCYZy4{Z69`{bZvorI5F(5wgN zRey9I`}zH{&Bpp`+U!3xD-W!j=lNlxSGfJzi;4H%-JkPHJlp$@|ISY}A3Of+GirWs z=krXUe&^bfhIo5J7yoUYkDq>&7IIH*lXYPKv0B}&^~`;Rn16By{;{Xn*Dq08kfix{ zX4s|p7fueh(;Om||1vi|lRfaWI&(s;!P4o?e`Y-1%lql^3x_!wMG3Wo=BL3&73pFV< z|9Ly<(og0;&By9atuA>PnJ4(O{pUJi=Rb?hUc5cd{HNPXTp+;dd6MG9`MJ!KeEz6% zJpEX&_F3@dulLT&>vYclKltc=edZ+Y)X+?!qjmi!t!y_+o;YvE^z(VseW!xl?CpGY zPCWN+x)xhb7u(3b&73tXG4%-;2p=d(;#vEA2?HZO|oY!7g6 zJvO`K-|qmg$NWo8{!Li?(e=V-`JcIO_I9l+it^5sTJzceLWODUx%y}O7lgV`f7MW* z`uoYxlrvwYzC6*4XqvlONBDEyB#$|lo_>+|BfO~m>72Tj>zl(F+Wyb#`?Zo|J6|qi zUU{b6fBgx$B0D}?Wg3_j`e+B7%KH=tw-T@r?c1Y zqz3P_3ZA?4bp5iGS$(J4MNaCRk(>RR;ZfuyUVrbXX$y-^1S~7rJZrj@*vaH~k~0-v zHXpN`u6E_mMfT4N1v!JfinA8VouBHhHou-O|&%fC*XSdJu zJE<$@+|GNtQnl*Gznbe4X7%Y#Qw<5UznixFv9iXOT`9*66g;LrG|}!&SvGIE)|ZyZ zpKdeHU7KjDa^+LqEfbsM&^F1{bGO`|w=wMe48tV1(3yU}&2=0u*{7GCQ=Dby;=kqT zN?ztyhlCG#lrFvK@B8yuVd!e#`Zbs4zV};kwta!Y!IaX@1DaJ*0$Dj0Q@2ZM7EIl~ zmh-C4gq2aPe$ta|^n_2>Xx%r>nZ>WTN$9%5uDqu6SDB9MExNf!vhcdM_m3p2>H3Uq zW(>~KncDeFwSOGmGJCe>3EdLj{Ke6g)_2e4mWaP!aCpyTxu>UJa^7=w|KU|Nwf0NB z?mgfAPs*xhTU*T5J(p~M{wZ7B3EP+xj_V$9tbeAl-t}1U>E8yM=C5a-cCTsQ?jKUQ zO>>LH8BcHJ78idsWr{XWT>dr9%}m>Ccg37!I-mH~XhPraIfqg$%U7IkylYv~a_ef& zj){!VZ!cXJu=~QQrBZjFUt1SCVUeM^>*MdY3RC~pPi@F4+&(pB9@8J`T`x9j%e8X7 zFx_!>kL1yxLity}xc*4f4uj4kpRS($=;L|UbHkaBXZiL%7T9SbXZEVXI#@h`Gj$=GbV#hb-*GmfhAPc9 zUaGe)TSr`Kc=o>jl-uLGy&NaZ56{S3y!B7vtuxz$ZSPNPJJ!p$Kikm#kcKkrslpF> zokeB-LQe(era9Z#bM$!qTyZn-(Imq=@uqH_x83imevEZ^;=eqzsPphlcRl^NE1*FFe>CZ*cKU^t{dpR5=ijZJa!O2sJuG+fw%7GumDb6>+{1Hs zJAV8#X}Y}S*Jj12wkd(E{Q*YWHqo2v_KBNS$j>}ck^WIp%VAq|<9F^K2e(`ZSo>`i zhv&WMm5Us$G4tX0!}oWFh&%E+33%ie5>W5{&AO!Z?gR^>;8 zSH8;nb56V^@VbZU&nBxG0p_p1*WV6$zrDZe|B|}>+F$F;6{H?sn!5D&`>ns<+pm*L zacOk_YSw>y?%%3o!7HbpU$XYd&eVr@q)vanB0l%mh0br=l+G{J6nHH->)d>15u2_X zza+MPdd#p={YBNg<<>tvtJog9|M*-Gy8AS_hi>wYVqUkR-JrGDje)JFG=T~&_L zW9pA5wjM7zsj8KycwKW>*Cv%~tFk3or?A|uEvldJ>hz}HKSW{;c6M5`|6m$7HT0{7Rhbx3!N;`qZZ@(@SphvMMDO+DE84SqMdm+OKb`n>mD{Q7 zSEiTxO*uxvTVr+#-*KXF> z{(y(^GK2@3V1K~FC^~(8JsfBcN?U~%x2?Dh>pjOpN+*X>f`jK$!&+U*l080UcJ zUO^M-)AijLxu>f(@(FD(kzworYlVrkZP%4&dTLgqV@&%(xh0Rl#13?fPzvk03n8)nN79 z)B9WagtkX|GD<+q0nIw=yD@@o;@ZB(hfx)5&h$jknt_G{M$YNst$cjjg##JyfDM|S z6UFGT-6(|796Z~+JtvCsC3spI1OGS|3PjC1^xDf4936U z*=~sW2^oy6+k1-{{lQ|}@7FRO2J3EL+QQhrw1ugCX$y1v(iWEXr7f)OOIz65m$tCC zFKyvyU)sXizO;p_eQ67K`_dMk?MqvDUp{9}?Twl~F^6vh$hzq+xqNP*g%i_zbNRx+ zjAyxgAz&%nJia6_V^JPo44A=~&ld@1#OCuQgBgeO`J%xL#R9%~Fr&GEF9*!{RKOPp zW&{^Pb*(Ri>Jlx2T9H%4mjPCDy@)Rp%y2H|ivlwi6!V3G89!hmp(T7NV39o~P=_g& z^2LEgnqZ8_FosJR)Xj@w45o6vII!a6a;WXQ%Ap1-RzMAGtKf?Rt9cG%cvM2oSzgJP z3YOxpf{Nr-LA`td#xSae`eI%+Up81VPYquzXuZJnoEpARFyjb}Aydm2$`{!i<%YeJAVrq?veyK>Bs33pely zfvuV}OTLGnk%3_fBLjmaiq`pJAfTcoEmHu5=v?R8;{JGz6Bfx&J_GYC^AY-|KA98CtnkvG?S?+NJ<`5 z{zyze*$R~^1WBFOflCQY-`fNYjZ+8j=-g*wU~pn(U@%0n<%$(VJzp~)B>D`S`J|bI z>>#4|R2jLZmpAiCfc^Gy28W0j69dCPW(Ed56jPo#LDcs-wbuoJCIR5t{^Fu z>G~~vGT@N*YvGe-n&|-%?Qh|e1B-3|iT?BkiGuybHGPc_qwsXjR%kfonNOX2h>3xr zofX~g$bjh+TlthgC7~2J)oefFDJRCp!0?5afx!&LndQ?bwDP%v{qmZ1yKx6A149Eh zx}uMfAoC2TtF`e7gPms6#wX2mHy$KvFuk*l4_q}(p9d0cOaqB(Pk+|NCj~Y(WSQdI zQ(O!TeB$WYWN$hsZAnf~$Y7M5u9pW5qQrJSX{M(|a4FX5y~T_|)7P~_<@bW*Q)(gd z>~)Ni(|J0eQqmoK(u`Wu%{%z~8SSRGckr1rx=!EU!RN^IKXE!!C!Zsu^mLa_J_n`~ wV$<6?`D~fanoK|1$!E{VF`c!G&w|lwx?LBaAJggV>3v;%GHfC_d<+Z>0O6X-xBvhE diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index b4e87227..6a87fba6 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -2,10 +2,12 @@ package com.limelight.grid; import android.app.Activity; import android.graphics.Bitmap; +import android.util.DisplayMetrics; import android.widget.ImageView; import android.widget.TextView; import com.limelight.AppView; +import com.limelight.LimeLog; import com.limelight.R; import com.limelight.grid.assets.CachedAppAssetLoader; import com.limelight.grid.assets.DiskAssetLoader; @@ -26,6 +28,10 @@ import java.util.concurrent.ConcurrentHashMap; public class AppGridAdapter extends GenericGridAdapter { 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 final CachedAppAssetLoader loader; private final ConcurrentHashMap, CachedAppAssetLoader.LoaderTuple> loadingTuples = new ConcurrentHashMap<>(); private final ConcurrentHashMap backgroundLoadingTuples = new ConcurrentHashMap<>(); @@ -33,8 +39,26 @@ public class AppGridAdapter extends GenericGridAdapter { 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); + int dpi = activity.getResources().getDisplayMetrics().densityDpi; + int dp; + + if (small) { + dp = SMALL_WIDTH_DP; + } + else { + dp = LARGE_WIDTH_DP; + } + + 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; + } + LimeLog.info("Art scaling divisor: " + scalingDivisor); + this.activity = activity; - this.loader = new CachedAppAssetLoader(computer, uniqueId, new NetworkAssetLoader(context), + this.loader = new CachedAppAssetLoader(computer, uniqueId, scalingDivisor, + new NetworkAssetLoader(context, uniqueId), new MemoryAssetLoader(), new DiskAssetLoader(context.getCacheDir())); } @@ -102,7 +126,7 @@ public class AppGridAdapter extends GenericGridAdapter { } @Override - public void notifyLoadComplete(Object object, final Bitmap bitmap) { + public void notifyLoadComplete(Object object, Bitmap bitmap) { final WeakReference viewRef = (WeakReference) object; loadingTuples.remove(viewRef); @@ -117,12 +141,13 @@ public class AppGridAdapter extends GenericGridAdapter { return; } + final Bitmap viewBmp = bitmap; activity.runOnUiThread(new Runnable() { @Override public void run() { ImageView view = viewRef.get(); if (view != null) { - view.setImageBitmap(bitmap); + view.setImageBitmap(viewBmp); fadeInImage(view); } } diff --git a/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java index 172b05c2..3635040c 100644 --- a/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java @@ -1,10 +1,12 @@ package com.limelight.grid.assets; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; 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; @@ -12,21 +14,34 @@ import java.util.concurrent.TimeUnit; public class CachedAppAssetLoader { private final ComputerDetails computer; private final String uniqueId; + private final double scalingDivider; private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(8, 8, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue()); private final ThreadPoolExecutor backgroundExecutor = new ThreadPoolExecutor(2, 2, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue()); - private final NetworkLoader networkLoader; - private final CachedLoader memoryLoader; - private final CachedLoader diskLoader; + private final NetworkAssetLoader networkLoader; + private final MemoryAssetLoader memoryLoader; + private final DiskAssetLoader diskLoader; - public CachedAppAssetLoader(ComputerDetails computer, String uniqueId, NetworkLoader networkLoader, CachedLoader memoryLoader, CachedLoader diskLoader) { + public CachedAppAssetLoader(ComputerDetails computer, String uniqueId, double scalingDivider, + NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader, + DiskAssetLoader diskLoader) { this.computer = computer; this.uniqueId = uniqueId; + this.scalingDivider = scalingDivider; this.networkLoader = networkLoader; this.memoryLoader = memoryLoader; this.diskLoader = diskLoader; } + private static Bitmap scaleBitmapAndRecyle(Bitmap bmp, double scalingDivider) { + Bitmap newBmp = Bitmap.createScaledBitmap(bmp, (int)(bmp.getWidth() / scalingDivider), + (int)(bmp.getHeight() / scalingDivider), true); + if (newBmp != bmp) { + bmp.recycle(); + } + return newBmp; + } + private Runnable createLoaderRunnable(final LoaderTuple tuple, final Object context, final LoadListener listener) { return new Runnable() { @Override @@ -36,7 +51,7 @@ public class CachedAppAssetLoader { return; } - Bitmap bmp = diskLoader.loadBitmapFromCache(tuple); + Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider); if (bmp == null) { // Notify the listener that this may take a while listener.notifyLongLoad(context); @@ -48,20 +63,24 @@ public class CachedAppAssetLoader { return; } - bmp = networkLoader.loadBitmap(tuple); - if (bmp != null) { - break; + 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) {} - } - - if (bmp != null) { - // Populate the disk cache - diskLoader.populateCache(tuple, bmp); + Thread.sleep((int) (1000 + (Math.random() * 500))); + } catch (InterruptedException e) { + break; + } } } @@ -95,7 +114,7 @@ public class CachedAppAssetLoader { } private LoaderTuple loadBitmapWithContext(NvApp app, Object context, LoadListener listener, boolean background) { - LoaderTuple tuple = new LoaderTuple(computer, uniqueId, app); + LoaderTuple tuple = new LoaderTuple(computer, app); // First, try the memory cache in the current context Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple); @@ -125,15 +144,13 @@ public class CachedAppAssetLoader { public class LoaderTuple { public final ComputerDetails computer; - public final String uniqueId; public final NvApp app; public boolean notified; public boolean cancelled; - public LoaderTuple(ComputerDetails computer, String uniqueId, NvApp app) { + public LoaderTuple(ComputerDetails computer, NvApp app) { this.computer = computer; - this.uniqueId = uniqueId; this.app = app; } @@ -150,15 +167,6 @@ public class CachedAppAssetLoader { } } - public interface NetworkLoader { - public Bitmap loadBitmap(LoaderTuple tuple); - } - - public interface CachedLoader { - public Bitmap loadBitmapFromCache(LoaderTuple tuple); - public void populateCache(LoaderTuple tuple, Bitmap bitmap); - } - public interface LoadListener { // Notifies that the load didn't hit any cache and is about to be dispatched // over the network diff --git a/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java index 219ca8a7..2da573de 100644 --- a/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java @@ -11,20 +11,21 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -public class DiskAssetLoader implements CachedAppAssetLoader.CachedLoader { +public class DiskAssetLoader { private final File cacheDir; public DiskAssetLoader(File cacheDir) { this.cacheDir = cacheDir; } - @Override - public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) { + 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"); - bmp = BitmapFactory.decodeStream(in); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = sampleSize; + bmp = BitmapFactory.decodeStream(in, null, options); } catch (IOException e) { e.printStackTrace(); } finally { @@ -42,13 +43,11 @@ public class DiskAssetLoader implements CachedAppAssetLoader.CachedLoader { return bmp; } - @Override - public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) { + public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) { OutputStream out = null; try { - // PNG ignores quality setting out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png"); - bitmap.compress(Bitmap.CompressFormat.PNG, 0, out); + CacheHelper.writeInputStreamToOutputStream(input, out); } catch (IOException e) { e.printStackTrace(); } finally { diff --git a/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java index 4fa26fca..995a4f36 100644 --- a/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java @@ -5,9 +5,9 @@ import android.util.LruCache; import com.limelight.LimeLog; -public class MemoryAssetLoader implements CachedAppAssetLoader.CachedLoader { +public class MemoryAssetLoader { private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); - private static final LruCache memoryCache = new LruCache(maxMemory / 8) { + private static final LruCache memoryCache = new LruCache(maxMemory / 12) { @Override protected int sizeOf(String key, Bitmap bitmap) { // Sizeof returns kilobytes @@ -19,7 +19,6 @@ public class MemoryAssetLoader implements CachedAppAssetLoader.CachedLoader { return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId(); } - @Override public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) { Bitmap bmp = memoryCache.get(constructKey(tuple)); if (bmp != null) { @@ -28,7 +27,6 @@ public class MemoryAssetLoader implements CachedAppAssetLoader.CachedLoader { return bmp; } - @Override public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) { memoryCache.put(constructKey(tuple), bitmap); } diff --git a/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java index 0150b230..b80f09ee 100644 --- a/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java @@ -2,109 +2,44 @@ package com.limelight.grid.assets; import android.content.Context; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; -import com.koushikdutta.ion.Ion; import com.limelight.LimeLog; import com.limelight.binding.PlatformBinding; import com.limelight.nvstream.http.ComputerDetails; -import com.limelight.nvstream.http.LimelightCryptoProvider; +import com.limelight.nvstream.http.NvHTTP; +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.security.Principal; -import java.security.PrivateKey; -import java.security.SecureRandom; -import java.security.cert.X509Certificate; -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; - -public class NetworkAssetLoader implements CachedAppAssetLoader.NetworkLoader { +public class NetworkAssetLoader { private final Context context; - private final LimelightCryptoProvider cryptoProvider; - private final SSLContext sslContext; + private final String uniqueId; - public NetworkAssetLoader(Context context) throws NoSuchAlgorithmException, KeyManagementException { + public NetworkAssetLoader(Context context, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException { this.context = context; - - cryptoProvider = PlatformBinding.getCryptoProvider(context); - - sslContext = SSLContext.getInstance("SSL"); - sslContext.init(ourKeyman, trustAllCerts, new SecureRandom()); + this.uniqueId = uniqueId; } - private final TrustManager[] trustAllCerts = new TrustManager[] { - new X509TrustManager() { - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } - public void checkClientTrusted(X509Certificate[] certs, String authType) {} - public void checkServerTrusted(X509Certificate[] certs, String authType) {} - }}; + public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) { + NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context)); - private final KeyManager[] ourKeyman = new KeyManager[] { - new X509KeyManager() { - public String chooseClientAlias(String[] keyTypes, - Principal[] issuers, Socket socket) { - return "Limelight-RSA"; - } + InputStream in = null; + try { + in = http.getBoxArt(tuple.app); + } catch (IOException e) {} - public String chooseServerAlias(String keyType, Principal[] issuers, - Socket socket) { - return null; - } - - public X509Certificate[] getCertificateChain(String alias) { - return new X509Certificate[] {cryptoProvider.getClientCertificate()}; - } - - public String[] getClientAliases(String keyType, Principal[] issuers) { - return null; - } - - public PrivateKey getPrivateKey(String alias) { - return cryptoProvider.getClientPrivateKey(); - } - - public String[] getServerAliases(String keyType, Principal[] issuers) { - return null; - } - } - }; - - // Ignore differences between given hostname and certificate hostname - private final HostnameVerifier hv = new HostnameVerifier() { - public boolean verify(String hostname, SSLSession session) { return true; } - }; - - @Override - public Bitmap loadBitmap(CachedAppAssetLoader.LoaderTuple tuple) { - // Set SSL contexts correctly to allow us to authenticate - Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts); - Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext); - Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setHostnameVerifier(hv); - Ion.getDefault(context).getBitmapCache().clear(); - - Bitmap bmp = Ion.with(context) - .load("https://" + getCurrentAddress(tuple.computer).getHostAddress() + ":47984/appasset?uniqueid=" + - tuple.uniqueId + "&appid=" + tuple.app.getAppId() + "&AssetType=2&AssetIdx=0") - .asBitmap() - .tryGet(); - if (bmp != null) { + if (in != null) { LimeLog.info("Network asset load complete: " + tuple); } else { LimeLog.info("Network asset load failed: " + tuple); } - return bmp; + return in; } private static InetAddress getCurrentAddress(ComputerDetails computer) { diff --git a/app/src/main/java/com/limelight/utils/CacheHelper.java b/app/src/main/java/com/limelight/utils/CacheHelper.java index 4bbebd46..581c26fa 100644 --- a/app/src/main/java/com/limelight/utils/CacheHelper.java +++ b/app/src/main/java/com/limelight/utils/CacheHelper.java @@ -38,6 +38,15 @@ public class CacheHelper { return new BufferedOutputStream(new FileOutputStream(openPath(true, root, path))); } + public static void writeInputStreamToOutputStream(InputStream in, OutputStream out) throws IOException { + byte[] buf = new byte[4096]; + int bytesRead; + + while ((bytesRead = in.read(buf)) != -1) { + out.write(buf, 0, bytesRead); + } + } + public static String readInputStreamToString(InputStream in) throws IOException { Reader r = new InputStreamReader(in); From 7b12fd1ad2607566e8930adf3a2376e24c0fd9e1 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 27 Feb 2015 01:33:33 -0500 Subject: [PATCH 046/202] Immediately show the PC as offline if the first poll fails --- .../java/com/limelight/computers/ComputerManagerService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index 06e3be56..217976e7 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -124,7 +124,8 @@ public class ComputerManagerService extends Service { @Override public void run() { - int offlineCount = 0; + // Start with an immediate offline notification + int offlineCount = Integer.MAX_VALUE; while (!isInterrupted() && pollingActive) { try { // Check if this poll has modified the details From e081ab5239c66f88dcda8ab4c2d7e1609c063573 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 27 Feb 2015 01:43:24 -0500 Subject: [PATCH 047/202] Code cleanup and Lint suggestions --- app/app.iml | 22 +++++++---------- .../video/AndroidCpuDecoderRenderer.java | 1 - .../computers/ComputerManagerService.java | 2 -- .../com/limelight/grid/AppGridAdapter.java | 9 ++++--- .../grid/assets/CachedAppAssetLoader.java | 24 ++----------------- .../grid/assets/NetworkAssetLoader.java | 4 +--- app/src/main/res/layout/activity_game.xml | 4 ++-- build.gradle | 2 +- 8 files changed, 19 insertions(+), 49 deletions(-) diff --git a/app/app.iml b/app/app.iml index 87814d7a..426a9c9e 100644 --- a/app/app.iml +++ b/app/app.iml @@ -12,9 +12,9 @@ =&kPFcsWDprJwU2e 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 W7oRWF#N6q9U3@ZZw{rOy7#INg^-XF3 delta 19231 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 Lp(%%tfq?-4x*#{- diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index 6e263d4c..4276180e 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -432,7 +432,8 @@ 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, app.getAppName()); + intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName()); + intent.putExtra(Game.EXTRA_APP_ID, app.getAppId()); intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId()); intent.putExtra(Game.EXTRA_STREAMING_REMOTE, computer.reachability != ComputerDetails.Reachability.LOCAL); diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index 0e327ae1..d4670926 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -89,7 +89,8 @@ 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 = "App"; + public static final String EXTRA_APP_NAME = "AppName"; + public static final String EXTRA_APP_ID = "AppId"; public static final String EXTRA_UNIQUEID = "UniqueId"; public static final String EXTRA_STREAMING_REMOTE = "Remote"; @@ -171,17 +172,24 @@ public class Game extends Activity implements SurfaceHolder.Callback, wifiLock.acquire(); String host = Game.this.getIntent().getStringExtra(EXTRA_HOST); - String app = Game.this.getIntent().getStringExtra(EXTRA_APP); + String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME); + int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID); String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID); 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(app) + .setApp(appName) + .setAppId(appId) .setBitrate(prefConfig.bitrate * 1000) .setEnableSops(prefConfig.enableSops) .enableAdaptiveResolution((decoderRenderer.getCapabilities() & From 85d95b2d8ed33d0b8e32b34cd272d58675a3529e Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 27 Feb 2015 13:52:17 -0500 Subject: [PATCH 049/202] Revert "Immediately show the PC as offline if the first poll fails" This reverts commit 7b12fd1ad2607566e8930adf3a2376e24c0fd9e1. --- .../java/com/limelight/computers/ComputerManagerService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index 26667028..d2621807 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -122,8 +122,7 @@ public class ComputerManagerService extends Service { @Override public void run() { - // Start with an immediate offline notification - int offlineCount = Integer.MAX_VALUE; + int offlineCount = 0; while (!isInterrupted() && pollingActive) { try { // Check if this poll has modified the details From 3d398ef6dd3e22656ce4fdf28e80cdae1322ff24 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 27 Feb 2015 14:22:35 -0500 Subject: [PATCH 050/202] Update common --- app/libs/limelight-common.jar | Bin 953352 -> 953516 bytes app/src/main/java/com/limelight/AppView.java | 2 +- app/src/main/java/com/limelight/Game.java | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/libs/limelight-common.jar b/app/libs/limelight-common.jar index c1b93feb18a64e63d15c76568fb54f53e0d6c9c6..5ff38374009fdec43afafab5a299efa1a03d3318 100644 GIT binary patch delta 18852 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 Date: Fri, 27 Feb 2015 15:13:43 -0500 Subject: [PATCH 051/202] Evict cached bitmaps when closing the app list --- app/src/main/java/com/limelight/grid/AppGridAdapter.java | 2 ++ .../java/com/limelight/grid/assets/CachedAppAssetLoader.java | 4 ++++ .../java/com/limelight/grid/assets/MemoryAssetLoader.java | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index e2fb87cc..381d4def 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -75,6 +75,8 @@ public class AppGridAdapter extends GenericGridAdapter { public void cancelQueuedOperations() { cancelTuples(loadingTuples); cancelTuples(backgroundLoadingTuples); + + loader.freeCacheMemory(); } private void sortList() { diff --git a/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java index 7c813c32..611a2aed 100644 --- a/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java @@ -30,6 +30,10 @@ public class CachedAppAssetLoader { this.diskLoader = diskLoader; } + public void freeCacheMemory() { + memoryLoader.clearCache(); + } + private Runnable createLoaderRunnable(final LoaderTuple tuple, final Object context, final LoadListener listener) { return new Runnable() { @Override diff --git a/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java index 995a4f36..47831a75 100644 --- a/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java @@ -30,4 +30,8 @@ public class MemoryAssetLoader { public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) { memoryCache.put(constructKey(tuple), bitmap); } + + public void clearCache() { + memoryCache.evictAll(); + } } From 0dad2dc64bc7bf81a9e76f50d2a26c17be8198ea Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 27 Feb 2015 15:15:01 -0500 Subject: [PATCH 052/202] Only close the app list activity if the PC is offline not unknown --- app/src/main/java/com/limelight/AppView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index 7c1df194..0c47d8e4 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -129,7 +129,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { return; } - if (details.state != ComputerDetails.State.ONLINE) { + if (details.state == ComputerDetails.State.OFFLINE) { // The PC is unreachable now AppView.this.runOnUiThread(new Runnable() { @Override From 067be54715924f62b6cf4351d34f42cdf01db5d3 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 27 Feb 2015 18:05:02 -0500 Subject: [PATCH 053/202] Show the discovery in progress view if no computers remain after one is deleted --- app/src/main/java/com/limelight/PcView.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/com/limelight/PcView.java b/app/src/main/java/com/limelight/PcView.java index 376e0b6b..689a7fc7 100644 --- a/app/src/main/java/com/limelight/PcView.java +++ b/app/src/main/java/com/limelight/PcView.java @@ -517,6 +517,12 @@ 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; } } From 4affc3c4cec5f9102175d91b58fd980387cbd4af Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 27 Feb 2015 18:12:49 -0500 Subject: [PATCH 054/202] Update to 3.1.2 release --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7eb77625..716e7b3a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { minSdkVersion 16 targetSdkVersion 21 - versionName "3.1.2-beta1" - versionCode = 55 + versionName "3.1.2" + versionCode = 56 } productFlavors { From fc8ce5e4b930de7f47f15cf1c2f9a0020f268352 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 2 Mar 2015 16:13:54 -0500 Subject: [PATCH 055/202] Quiet down disk cache misses --- .../main/java/com/limelight/grid/assets/DiskAssetLoader.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java index 2da573de..e9c0709f 100644 --- a/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java @@ -7,6 +7,7 @@ import com.limelight.LimeLog; import com.limelight.utils.CacheHelper; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -26,6 +27,7 @@ public class DiskAssetLoader { BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = sampleSize; bmp = BitmapFactory.decodeStream(in, null, options); + } catch (FileNotFoundException ignored) { } catch (IOException e) { e.printStackTrace(); } finally { From 896288a40b4a32c0178c20fd9ac6fcabd5c00ef2 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 2 Mar 2015 17:03:08 -0500 Subject: [PATCH 056/202] Use AsyncTasks and attached Drawables to track background image loading --- .../com/limelight/grid/AppGridAdapter.java | 146 +------- .../grid/assets/CachedAppAssetLoader.java | 345 +++++++++++++----- .../grid/assets/NetworkAssetLoader.java | 2 - 3 files changed, 269 insertions(+), 224 deletions(-) diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index 381d4def..9e6d4eaa 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -1,7 +1,7 @@ package com.limelight.grid; import android.app.Activity; -import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.widget.ImageView; import android.widget.TextView; @@ -14,27 +14,18 @@ import com.limelight.grid.assets.MemoryAssetLoader; import com.limelight.grid.assets.NetworkAssetLoader; import com.limelight.nvstream.http.ComputerDetails; -import java.lang.ref.WeakReference; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; -import java.util.Collection; import java.util.Collections; import java.util.Comparator; -import java.util.Iterator; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; @SuppressWarnings("unchecked") public class AppGridAdapter extends GenericGridAdapter { - 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 final CachedAppAssetLoader loader; - private final ConcurrentHashMap, CachedAppAssetLoader.LoaderTuple> loadingTuples = new ConcurrentHashMap<>(); - private final ConcurrentHashMap backgroundLoadingTuples = new ConcurrentHashMap<>(); 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); @@ -56,26 +47,20 @@ public class AppGridAdapter extends GenericGridAdapter { } LimeLog.info("Art scaling divisor: " + scalingDivisor); - this.activity = activity; + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = (int) scalingDivisor; + this.loader = new CachedAppAssetLoader(computer, scalingDivisor, new NetworkAssetLoader(context, uniqueId), - new MemoryAssetLoader(), new DiskAssetLoader(context.getCacheDir())); - } - - private static void cancelTuples(ConcurrentHashMap map) { - Collection tuples = map.values(); - - for (CachedAppAssetLoader.LoaderTuple tuple : tuples) { - tuple.cancel(); - } - - map.clear(); + new MemoryAssetLoader(), + new DiskAssetLoader(context.getCacheDir()), + BitmapFactory.decodeResource(activity.getResources(), + R.drawable.image_loading, options)); } public void cancelQueuedOperations() { - cancelTuples(loadingTuples); - cancelTuples(backgroundLoadingTuples); - + loader.cancelForegroundLoads(); + loader.cancelBackgroundLoads(); loader.freeCacheMemory(); } @@ -89,14 +74,10 @@ public class AppGridAdapter extends GenericGridAdapter { } 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); - } + // Queue a request to fetch this bitmap into cache + loader.queueCacheLoad(app.app); + // Add the app to our sorted list itemList.add(app); sortList(); } @@ -105,100 +86,9 @@ public class AppGridAdapter extends GenericGridAdapter { itemList.remove(app); } - private final CachedAppAssetLoader.LoadListener imageViewLoadListener = new CachedAppAssetLoader.LoadListener() { - @Override - public void notifyLongLoad(Object object) { - final WeakReference viewRef = (WeakReference) object; - - // 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.setImageResource(R.drawable.image_loading); - fadeInImage(view); - } - } - }); - } - - @Override - public void notifyLoadComplete(Object object, final Bitmap bitmap) { - final WeakReference viewRef = (WeakReference) 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, CachedAppAssetLoader.LoaderTuple>> i = loadingTuples.entrySet().iterator(); - while (i.hasNext()) { - Map.Entry, 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); - - // Clear existing contents of the image view - imgView.setAlpha(0.0f); - - // Start loading the bitmap - WeakReference 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); - } + public boolean populateImageView(ImageView imgView, AppView.AppObject obj) { + // Let the cached asset loader handle it + loader.populateImageView(obj.app, imgView); return true; } @@ -222,8 +112,4 @@ public class AppGridAdapter extends GenericGridAdapter { // No overlay return false; } - - private static void fadeInImage(ImageView view) { - view.animate().alpha(1.0f).setDuration(100).start(); - } } diff --git a/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java index 611a2aed..9247677d 100644 --- a/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java @@ -1,148 +1,318 @@ package com.limelight.grid.assets; +import android.content.res.Resources; import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.widget.ImageView; import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.NvApp; import java.io.InputStream; +import java.lang.ref.WeakReference; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class CachedAppAssetLoader { + private static final int MAX_CONCURRENT_FOREGROUND_LOADS = 8; + private static final int MAX_CONCURRENT_CACHE_LOADS = 2; + + private static final int MAX_PENDING_CACHE_LOADS = 100; + private static final int MAX_PENDING_FOREGROUND_LOADS = 30; + + private final ThreadPoolExecutor cacheExecutor = new ThreadPoolExecutor( + MAX_CONCURRENT_CACHE_LOADS, MAX_CONCURRENT_CACHE_LOADS, + Long.MAX_VALUE, TimeUnit.DAYS, + new LinkedBlockingQueue(MAX_PENDING_CACHE_LOADS), + new ThreadPoolExecutor.DiscardOldestPolicy()); + + private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor( + MAX_CONCURRENT_FOREGROUND_LOADS, MAX_CONCURRENT_FOREGROUND_LOADS, + Long.MAX_VALUE, TimeUnit.DAYS, + new LinkedBlockingQueue(MAX_PENDING_FOREGROUND_LOADS), + new ThreadPoolExecutor.DiscardOldestPolicy()); + private final ComputerDetails computer; private final double scalingDivider; - private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(8, 8, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue()); - private final ThreadPoolExecutor backgroundExecutor = new ThreadPoolExecutor(2, 2, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue()); private final NetworkAssetLoader networkLoader; private final MemoryAssetLoader memoryLoader; private final DiskAssetLoader diskLoader; + private final Bitmap placeholderBitmap; public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider, NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader, - DiskAssetLoader diskLoader) { + DiskAssetLoader diskLoader, Bitmap placeholderBitmap) { this.computer = computer; this.scalingDivider = scalingDivider; - this.networkLoader = networkLoader; this.memoryLoader = memoryLoader; this.diskLoader = diskLoader; + this.placeholderBitmap = placeholderBitmap; + } + + public void cancelBackgroundLoads() { + Runnable r; + while ((r = cacheExecutor.getQueue().poll()) != null) { + cacheExecutor.remove(r); + } + } + + public void cancelForegroundLoads() { + Runnable r; + while ((r = foregroundExecutor.getQueue().poll()) != null) { + foregroundExecutor.remove(r); + } } 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) { + private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) { + Bitmap bmp; + + // Try 3 times + for (int i = 0; i < 3; i++) { + // Check again whether we've been cancelled or the image view is gone + if (task != null && (task.isCancelled() || task.imageViewRef.get() == null)) { + return null; + } + + InputStream in = networkLoader.getBitmapStream(tuple); + if (in != null) { + // Write the stream straight to disk + diskLoader.populateCacheWithStream(tuple, in); + + // Read it back scaled + bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider); + if (bmp != null) { + return bmp; + } + } + + // Wait 1 second with a bit of fuzz + try { + Thread.sleep((int) (1000 + (Math.random() * 500))); + } catch (InterruptedException e) { + return null; + } + } + + return null; + } + + private class LoaderTask extends AsyncTask { + private final WeakReference imageViewRef; + private LoaderTuple tuple; + private boolean loadFinished; + + public LoaderTask(ImageView imageView) { + imageViewRef = new WeakReference(imageView); + } + + @Override + protected Bitmap doInBackground(LoaderTuple... params) { + tuple = params[0]; + + // Check whether it has been cancelled or the image view is gone + if (isCancelled() || imageViewRef.get() == null) { + System.out.println("Cancelled or no image view in doInBackground"); + return null; + } + + Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider); + if (bmp == null) { + // Report progress to display the placeholder + publishProgress(); + + // Try to load the asset from the network + bmp = doNetworkAssetLoad(tuple, this); + } + + // Cache the bitmap + if (bmp != null) { + loadFinished = true; + memoryLoader.populateCache(tuple, bmp); + } + + return bmp; + } + + @Override + protected void onProgressUpdate(Void... nothing) { + // Do nothing if the load has already completed + if (loadFinished) { + return; + } + + // Do nothing if cancelled + if (isCancelled()) { + return; + } + + final ImageView imageView = imageViewRef.get(); + if (imageView != null) { + // If the current loader task for this view isn't us, do nothing + if (getLoaderTask(imageView) != this) { 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); + // Show the placeholder by setting alpha to 1.0 + imageView.setAlpha(1.0f); } - }; + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + // Do nothing if cancelled + if (isCancelled()) { + return; + } + + final ImageView imageView = imageViewRef.get(); + if (imageView != null) { + // If the current loader task for this view isn't us, do nothing + if (getLoaderTask(imageView) != this) { + return; + } + + // Set the bitmap + if (bitmap != null) { + imageView.setImageBitmap(bitmap); + } + + // Show the view + imageView.setAlpha(1.0f); + } + } } - public LoaderTuple loadBitmapWithContext(NvApp app, Object context, LoadListener listener) { - return loadBitmapWithContext(app, context, listener, false); + static class AsyncDrawable extends BitmapDrawable { + private final WeakReference loaderTaskReference; + + public AsyncDrawable(Resources res, Bitmap bitmap, + LoaderTask loaderTask) { + super(res, bitmap); + loaderTaskReference = new WeakReference(loaderTask); + } + + public LoaderTask getLoaderTask() { + return loaderTaskReference.get(); + } } - public LoaderTuple loadBitmapWithContextInBackground(NvApp app, Object context, LoadListener listener) { - return loadBitmapWithContext(app, context, listener, true); + private static LoaderTask getLoaderTask(ImageView imageView) { + final Drawable drawable = imageView.getDrawable(); + + // If our drawable is in play, get the loader task + if (drawable instanceof AsyncDrawable) { + final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; + return asyncDrawable.getLoaderTask(); + } + + return null; } - private LoaderTuple loadBitmapWithContext(NvApp app, Object context, LoadListener listener, boolean background) { + private static boolean cancelPendingLoad(LoaderTuple tuple, ImageView imageView) { + final LoaderTask loaderTask = getLoaderTask(imageView); + + // Check if any task was pending for this image view + if (loaderTask != null && !loaderTask.isCancelled()) { + final LoaderTuple taskTuple = loaderTask.tuple; + + // Cancel the task if it's not already loading the same data + if (taskTuple == null || !taskTuple.equals(tuple)) { + loaderTask.cancel(true); + } else { + // It's already loading what we want + return false; + } + } + + // Allow the load to proceed + return true; + } + + public void queueCacheLoad(NvApp app) { + final LoaderTuple tuple = new LoaderTuple(computer, app); + + if (memoryLoader.loadBitmapFromCache(tuple) != null) { + // It's in memory which means it must also be on disk + return; + } + + // Queue a fetch in the cache executor + cacheExecutor.execute(new Runnable() { + @Override + public void run() { + Bitmap bmp; + + // Check if the image is cached on disk + bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider); + if (bmp == null) { + // Try to load the asset from the network and cache on disk + bmp = doNetworkAssetLoad(tuple, null); + } + + // If the bitmap was loaded, recycle it immediately. We can do this + // because it's not loaded into any image views or cached in memory + if (bmp != null) { + bmp.recycle(); + } + } + }); + } + + public void populateImageView(NvApp app, ImageView view) { 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; + // Show the bitmap immediately + view.setImageBitmap(bmp); + return; } - // If it's not in memory, throw this in our executor - if (background) { - backgroundExecutor.execute(createLoaderRunnable(tuple, context, listener)); + // If there's already a task in progress for this view, + // cancel it. If the task is already loading the same image, + // we return and let that load finish. + if (!cancelPendingLoad(tuple, view)) { + return; } - else { - foregroundExecutor.execute(createLoaderRunnable(tuple, context, listener)); - } - return tuple; + + // If it's not in memory, create an async task to load it. This task will be attached + // via AsyncDrawable to this view. + final LoaderTask task = new LoaderTask(view); + final AsyncDrawable asyncDrawable = new AsyncDrawable(view.getResources(), placeholderBitmap, task); + view.setAlpha(0.0f); + view.setImageDrawable(asyncDrawable); + + // Run the task on our foreground executor + task.executeOnExecutor(foregroundExecutor, tuple); } public class LoaderTuple { public final ComputerDetails computer; public final NvApp app; - public 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 boolean equals(Object o) { + if (!(o instanceof LoaderTuple)) { + return false; } + + LoaderTuple other = (LoaderTuple) o; + return computer.uuid.equals(other.computer.uuid) && app.getAppId() == other.app.getAppId(); } @Override @@ -150,13 +320,4 @@ public class CachedAppAssetLoader { 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); - } } diff --git a/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java index f1d09d13..6114f4ac 100644 --- a/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java @@ -10,8 +10,6 @@ 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; From 56c8a9e6fe0694da9d4798b3474a16fdd9c0dceb Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 2 Mar 2015 17:05:45 -0500 Subject: [PATCH 057/202] Use the regular serverinfo query to update the running status of apps --- app/src/main/java/com/limelight/AppView.java | 53 +++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index 0c47d8e4..6b1a6b19 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -54,6 +54,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { private ComputerManagerService.ApplistPoller poller; private SpinnerDialog blockingLoadSpinner; private String lastRawApplist; + private int lastRunningAppId; private final static int START_OR_RESUME_ID = 1; private final static int QUIT_ID = 2; @@ -143,13 +144,23 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { return; } - // App list is the same or empty; nothing to do + // App list is the same or empty if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) { + + // Let's check if the running app ID changed + if (details.runningGameId != lastRunningAppId) { + // Update the currently running game using the app ID + lastRunningAppId = details.runningGameId; + updateUiWithServerinfo(details); + } + return; } + lastRunningAppId = details.runningGameId; + lastRawApplist = details.rawAppList; + try { - lastRawApplist = details.rawAppList; updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList))); if (blockingLoadSpinner != null) { @@ -356,6 +367,44 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { } } + private void updateUiWithServerinfo(final ComputerDetails details) { + AppView.this.runOnUiThread(new Runnable() { + @Override + public void run() { + boolean updated = false; + + // Look through our current app list to tag the running app + for (int i = 0; i < appGridAdapter.getCount(); i++) { + AppObject existingApp = (AppObject) appGridAdapter.getItem(i); + + // There can only be one or zero apps running. + if (existingApp.app.getIsRunning() && + existingApp.app.getAppId() == details.runningGameId) { + // This app was running and still is, so we're done now + return; + } + else if (existingApp.app.getAppId() == details.runningGameId) { + // This app wasn't running but now is + existingApp.app.setIsRunning(true); + updated = true; + } + else if (existingApp.app.getIsRunning()) { + // This app was running but now isn't + existingApp.app.setIsRunning(false); + updated = true; + } + else { + // This app wasn't running and still isn't + } + } + + if (updated) { + appGridAdapter.notifyDataSetChanged(); + } + } + }); + } + private void updateUiWithAppList(final List appList) { AppView.this.runOnUiThread(new Runnable() { @Override From 899387caa1cb3af61fb811a0435ab9ba297ec880 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 2 Mar 2015 18:34:21 -0500 Subject: [PATCH 058/202] Use a separate executor for network loads to avoid stalling cached loads. Optimize background cache fill loads. --- .../grid/assets/CachedAppAssetLoader.java | 124 +++++++++--------- .../grid/assets/DiskAssetLoader.java | 4 + .../grid/assets/MemoryAssetLoader.java | 2 +- .../java/com/limelight/utils/CacheHelper.java | 4 + 4 files changed, 73 insertions(+), 61 deletions(-) diff --git a/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java index 9247677d..f4438cd5 100644 --- a/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java @@ -13,16 +13,17 @@ import com.limelight.nvstream.http.NvApp; import java.io.InputStream; import java.lang.ref.WeakReference; import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class CachedAppAssetLoader { - private static final int MAX_CONCURRENT_FOREGROUND_LOADS = 8; + private static final int MAX_CONCURRENT_DISK_LOADS = 4; + private static final int MAX_CONCURRENT_NETWORK_LOADS = 4; private static final int MAX_CONCURRENT_CACHE_LOADS = 2; private static final int MAX_PENDING_CACHE_LOADS = 100; - private static final int MAX_PENDING_FOREGROUND_LOADS = 30; + private static final int MAX_PENDING_NETWORK_LOADS = 40; + private static final int MAX_PENDING_DISK_LOADS = 40; private final ThreadPoolExecutor cacheExecutor = new ThreadPoolExecutor( MAX_CONCURRENT_CACHE_LOADS, MAX_CONCURRENT_CACHE_LOADS, @@ -31,9 +32,15 @@ public class CachedAppAssetLoader { new ThreadPoolExecutor.DiscardOldestPolicy()); private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor( - MAX_CONCURRENT_FOREGROUND_LOADS, MAX_CONCURRENT_FOREGROUND_LOADS, + MAX_CONCURRENT_DISK_LOADS, MAX_CONCURRENT_DISK_LOADS, Long.MAX_VALUE, TimeUnit.DAYS, - new LinkedBlockingQueue(MAX_PENDING_FOREGROUND_LOADS), + new LinkedBlockingQueue(MAX_PENDING_DISK_LOADS), + new ThreadPoolExecutor.DiscardOldestPolicy()); + + private final ThreadPoolExecutor networkExecutor = new ThreadPoolExecutor( + MAX_CONCURRENT_NETWORK_LOADS, MAX_CONCURRENT_NETWORK_LOADS, + Long.MAX_VALUE, TimeUnit.DAYS, + new LinkedBlockingQueue(MAX_PENDING_NETWORK_LOADS), new ThreadPoolExecutor.DiscardOldestPolicy()); private final ComputerDetails computer; @@ -63,9 +70,14 @@ public class CachedAppAssetLoader { public void cancelForegroundLoads() { Runnable r; + while ((r = foregroundExecutor.getQueue().poll()) != null) { foregroundExecutor.remove(r); } + + while ((r = networkExecutor.getQueue().poll()) != null) { + networkExecutor.remove(r); + } } public void freeCacheMemory() { @@ -73,8 +85,6 @@ public class CachedAppAssetLoader { } private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) { - Bitmap bmp; - // Try 3 times for (int i = 0; i < 3; i++) { // Check again whether we've been cancelled or the image view is gone @@ -87,10 +97,13 @@ public class CachedAppAssetLoader { // Write the stream straight to disk diskLoader.populateCacheWithStream(tuple, in); - // Read it back scaled - bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider); - if (bmp != null) { - return bmp; + // If there's a task associated with this load, we should return the bitmap + if (task != null) { + return diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider); + } + else { + // Otherwise it's a background load and we return nothing + return null; } } @@ -107,11 +120,13 @@ public class CachedAppAssetLoader { private class LoaderTask extends AsyncTask { private final WeakReference imageViewRef; - private LoaderTuple tuple; - private boolean loadFinished; + private final boolean diskOnly; - public LoaderTask(ImageView imageView) { - imageViewRef = new WeakReference(imageView); + private LoaderTuple tuple; + + public LoaderTask(ImageView imageView, boolean diskOnly) { + this.imageViewRef = new WeakReference(imageView); + this.diskOnly = diskOnly; } @Override @@ -120,22 +135,23 @@ public class CachedAppAssetLoader { // Check whether it has been cancelled or the image view is gone if (isCancelled() || imageViewRef.get() == null) { - System.out.println("Cancelled or no image view in doInBackground"); return null; } Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider); if (bmp == null) { - // Report progress to display the placeholder - publishProgress(); - - // Try to load the asset from the network - bmp = doNetworkAssetLoad(tuple, this); + if (!diskOnly) { + // Try to load the asset from the network + bmp = doNetworkAssetLoad(tuple, this); + } else { + // Report progress to display the placeholder and spin + // off the network-capable task + publishProgress(); + } } // Cache the bitmap if (bmp != null) { - loadFinished = true; memoryLoader.populateCache(tuple, bmp); } @@ -144,25 +160,20 @@ public class CachedAppAssetLoader { @Override protected void onProgressUpdate(Void... nothing) { - // Do nothing if the load has already completed - if (loadFinished) { - return; - } - // Do nothing if cancelled if (isCancelled()) { return; } + // If the current loader task for this view isn't us, do nothing final ImageView imageView = imageViewRef.get(); - if (imageView != null) { - // If the current loader task for this view isn't us, do nothing - if (getLoaderTask(imageView) != this) { - return; - } - - // Show the placeholder by setting alpha to 1.0 + if (getLoaderTask(imageView) == this) { + // Set off another loader task on the network executor + LoaderTask task = new LoaderTask(imageView, false); + AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task); imageView.setAlpha(1.0f); + imageView.setImageDrawable(asyncDrawable); + task.executeOnExecutor(networkExecutor, tuple); } } @@ -174,12 +185,7 @@ public class CachedAppAssetLoader { } final ImageView imageView = imageViewRef.get(); - if (imageView != null) { - // If the current loader task for this view isn't us, do nothing - if (getLoaderTask(imageView) != this) { - return; - } - + if (getLoaderTask(imageView) == this) { // Set the bitmap if (bitmap != null) { imageView.setImageBitmap(bitmap); @@ -206,6 +212,10 @@ public class CachedAppAssetLoader { } private static LoaderTask getLoaderTask(ImageView imageView) { + if (imageView == null) { + return null; + } + final Drawable drawable = imageView.getDrawable(); // If our drawable is in play, get the loader task @@ -249,20 +259,13 @@ public class CachedAppAssetLoader { cacheExecutor.execute(new Runnable() { @Override public void run() { - Bitmap bmp; - // Check if the image is cached on disk - bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider); - if (bmp == null) { - // Try to load the asset from the network and cache on disk - bmp = doNetworkAssetLoad(tuple, null); + if (diskLoader.checkCacheExists(tuple)) { + return; } - // If the bitmap was loaded, recycle it immediately. We can do this - // because it's not loaded into any image views or cached in memory - if (bmp != null) { - bmp.recycle(); - } + // Try to load the asset from the network and cache result on disk + doNetworkAssetLoad(tuple, null); } }); } @@ -270,14 +273,6 @@ public class CachedAppAssetLoader { public void populateImageView(NvApp app, ImageView view) { LoaderTuple tuple = new LoaderTuple(computer, app); - // First, try the memory cache in the current context - Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple); - if (bmp != null) { - // Show the bitmap immediately - view.setImageBitmap(bmp); - return; - } - // If there's already a task in progress for this view, // cancel it. If the task is already loading the same image, // we return and let that load finish. @@ -285,9 +280,18 @@ public class CachedAppAssetLoader { return; } + // First, try the memory cache in the current context + Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple); + if (bmp != null) { + // Show the bitmap immediately + view.setAlpha(1.0f); + view.setImageBitmap(bmp); + return; + } + // If it's not in memory, create an async task to load it. This task will be attached // via AsyncDrawable to this view. - final LoaderTask task = new LoaderTask(view); + final LoaderTask task = new LoaderTask(view, true); final AsyncDrawable asyncDrawable = new AsyncDrawable(view.getResources(), placeholderBitmap, task); view.setAlpha(0.0f); view.setImageDrawable(asyncDrawable); diff --git a/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java index e9c0709f..353c08b0 100644 --- a/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java @@ -19,6 +19,10 @@ public class DiskAssetLoader { this.cacheDir = cacheDir; } + public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) { + return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png"); + } + public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) { InputStream in = null; Bitmap bmp = null; diff --git a/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java index 47831a75..29063fa8 100644 --- a/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java @@ -7,7 +7,7 @@ import com.limelight.LimeLog; public class MemoryAssetLoader { private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); - private static final LruCache memoryCache = new LruCache(maxMemory / 12) { + private static final LruCache memoryCache = new LruCache(maxMemory / 16) { @Override protected int sizeOf(String key, Bitmap bitmap) { // Sizeof returns kilobytes diff --git a/app/src/main/java/com/limelight/utils/CacheHelper.java b/app/src/main/java/com/limelight/utils/CacheHelper.java index 581c26fa..8a008a77 100644 --- a/app/src/main/java/com/limelight/utils/CacheHelper.java +++ b/app/src/main/java/com/limelight/utils/CacheHelper.java @@ -30,6 +30,10 @@ public class CacheHelper { return f; } + public static boolean cacheFileExists(File root, String... path) { + return openPath(false, root, path).exists(); + } + public static InputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException { return new BufferedInputStream(new FileInputStream(openPath(false, root, path))); } From 2ab67380d68097960fb061ffdb3e0f57a6ffc888 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 9 Mar 2015 01:49:52 -0500 Subject: [PATCH 059/202] Use direct submit decoding for MediaCodec. Based on my profiling of a few devices, dequeueInputBuffer and queueInputBuffer don't take much time anyway. It allows us to stop our semi-busy looping which saves power. The depacketizer can avoid expensive synchronization and additional context switching which costs time and CPU cycles. --- app/libs/limelight-common.jar | Bin 953516 -> 956588 bytes .../video/ConfigurableDecoderRenderer.java | 6 + .../video/EnhancedDecoderRenderer.java | 2 +- .../video/MediaCodecDecoderRenderer.java | 191 +++++------------- 4 files changed, 56 insertions(+), 143 deletions(-) diff --git a/app/libs/limelight-common.jar b/app/libs/limelight-common.jar index 5ff38374009fdec43afafab5a299efa1a03d3318..530e78dbc71e458b945c9a69cce646c44eb4852a 100644 GIT binary patch delta 22993 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