Compare commits

..
68 Commits
Author SHA1 Message Date
Cameron Gutman 3de9765eaa Version 5.10 2018-10-27 23:45:01 -07:00
Cameron Gutman d4072eb295 Avoid nulling activeAddress during polling 2018-10-27 23:38:11 -07:00
Cameron Gutman cac2bdbb81 Disable back mouse button on Xiaomi devices to workaround issue 2018-10-27 13:50:37 -07:00
Cameron Gutman 66f0aee3f8 Use STUN to discover WAN address when PC is found using mDNS 2018-10-27 10:46:28 -07:00
Cameron Gutman b690dc5474 Rewrite reachability code and computer DB to bring it inline with other modern Moonlight clients 2018-10-27 02:18:33 -07:00
Cameron Gutman c2fbe6ad91 Version 5.9.4 2018-10-24 19:41:39 -07:00
Cameron Gutman cf07c02398 Update AGP to 3.2.1 2018-10-24 19:40:26 -07:00
Cameron Gutman 42dc928ad5 Fixes to make the translation build without warnings 2018-10-24 19:37:41 -07:00
bubuleur 11597f0aa7 Update of the French version (#636)
* Update of the French version

Hello,
Update your application in French
thank you

* Update strings.xml

* Update strings.xml
2018-10-24 19:36:50 -07:00
Cameron Gutman cdcd4d48f2 Always handle KEYCODE_BACK to prevent synthetic right-clicks on back. Possibly fixes #634 2018-10-24 19:25:47 -07:00
Cameron Gutman a9af4e54a9 Add confirmation dialog for PC deletion 2018-10-24 18:47:52 -07:00
Cameron Gutman 7eac609219 Fix root mouse capture with su binaries that don't like additional parameters after -c 2018-10-10 21:23:41 -07:00
Cameron Gutman fa761debc4 Fix root build 2018-10-05 01:44:03 -07:00
Cameron Gutman 62e175f069 Avoid crashing when opening an app context menu in list mode 2018-10-05 01:42:19 -07:00
Cameron Gutman d7d8c40565 Version 5.9.3 2018-10-05 01:34:59 -07:00
Cameron Gutman 64de13ab50 Try to disambiguate right clicks from back presses 2018-10-05 01:29:18 -07:00
Cameron Gutman 2f02939638 Always process key events before the IME 2018-10-05 01:10:27 -07:00
Cameron Gutman 1d7c8697e9 Add support for X1 and X2 mouse buttons 2018-10-05 00:56:30 -07:00
Cameron Gutman 7dea322bbd Update build tools to 28.0.3 2018-09-29 15:37:31 -07:00
Cameron Gutman 349ecb16ab Increment version code 2018-09-29 15:36:21 -07:00
Cameron Gutman a3867735c1 Update to AGP 3.2 2018-09-29 15:31:13 -07:00
Cameron Gutman 5b087e9f70 Update common-c with split encode change 2018-09-22 20:22:59 -07:00
Cameron Gutman eed18223eb Version 5.9.2 2018-09-18 20:36:53 -07:00
Cameron Gutman 30d4d2a918 Update moonlight-common 2018-09-18 20:27:08 -07:00
Cameron Gutman 30f666c70e Update AGP to 3.2 rc3 2018-09-18 20:23:25 -07:00
Cameron Gutman 209fead0e8 Only add the create shortcut option if the box art is present to avoid crashing 2018-09-18 20:22:16 -07:00
Cameron Gutman 5c6889bf6d Version 5.9.1 2018-08-12 00:45:19 -07:00
Cameron Gutman 7d24900756 Update build tools to 28.0.2 2018-08-12 00:44:09 -07:00
Cameron Gutman 79a75b9d19 Update common with audio fix and game launch fix 2018-08-12 00:40:56 -07:00
Cameron Gutman 29b64992bd Enable stale and no-response bots to reduce inactive issues 2018-08-12 00:20:03 -07:00
Cameron Gutman c9b14540f2 Remove reference to old Moonlight Java project 2018-08-10 22:29:59 -07:00
Cameron Gutman 546843a26c Fix crash on quit confirmation prompt 2018-07-28 00:13:20 -07:00
Cameron Gutman d03d260535 Add status bar and navigation bar color on L+ 2018-07-27 23:48:20 -07:00
Cameron Gutman 6946e3c7a2 Just use the PC name as the app list title 2018-07-27 23:42:57 -07:00
Cameron Gutman b79d328961 Version 5.9 2018-07-16 18:46:01 -07:00
Cameron Gutman c313797d93 Make OSC reconfigure button non-focusable so it doesn't eat hardware enter/space presses. Fixes #611 2018-07-16 18:40:19 -07:00
Cameron Gutman c8cb8e1346 Update build tools to 28.0.1 2018-07-16 18:25:33 -07:00
Cameron Gutman 6a9f8da14e Update common to reduce syscall overhead 2018-07-16 18:21:48 -07:00
Cameron Gutman ff9260a0fd Update AGP to 3.2-beta4 2018-07-16 18:16:35 -07:00
zacklmoore 62bedb1609 Pinned Game Shortcuts (Android Oreo) (#603)
* Initial changes to add game shortcuts.

* Initial working cut.

* Cleanup and converting strings to resource strings.

* Additional cleanup.

* Removed a blank line

* Changes based on review feedback.

* Forgot to save some changes before commiting...

* Standardized dialogs and tried to fix the dialogs auto-closing when the PCView is already opened.
2018-07-06 21:53:19 -07:00
Cameron Gutman a519723d44 Monkey-proof Moonlight 2018-06-20 01:26:59 -07:00
Cameron Gutman 36191781ed Version 5.8.2 2018-06-16 17:05:44 -07:00
Cameron Gutman 61b6a49669 Correct MT8176 errata 2018-06-16 16:37:50 -07:00
Cameron Gutman e97845e46e Add comments and documentation on MT8176 testing 2018-06-16 16:31:06 -07:00
Cameron Gutman 6bba68207d Ignore spurious ACTION_HOVER_ENTER with wrong coordinates and KEYCODE_BACK repeats. Fixes #554 2018-06-16 15:57:44 -07:00
Cameron Gutman 0e17cccc06 Process historical values for relative mouse events 2018-06-16 15:22:01 -07:00
Cameron Gutman 918e922e40 Avoid processing mouse move history 2018-06-16 15:14:16 -07:00
Cameron Gutman a08854ddfd Properly handle SOURCE_MOUSE_RELATIVE in the mouse back button hack. Fixes #424 2018-06-16 15:01:11 -07:00
Cameron Gutman eb6f15c2b7 Add dynamic method for allowing back buttons for navigation 2018-06-16 14:32:07 -07:00
Cameron Gutman 2cd9e31684 Update gitignore to handle app bundles and other new files dropped by AS 2018-06-16 14:27:54 -07:00
Cameron Gutman 791d6624e2 Update to AGP 3.2 alpha 18 2018-06-16 14:21:14 -07:00
Cameron Gutman af41021271 Use HEVC by default on MediaTek SoCs with PowerVR graphics 2018-06-14 22:55:10 -07:00
Cameron Gutman d726d939f4 Version 5.8.1-r3 2018-06-09 21:49:41 -07:00
Cameron Gutman 748085e7bb Update common to fix reconnection issue 2018-06-09 21:39:26 -07:00
Cameron Gutman d57d19174b Version 5.8.1 r2 2018-06-09 18:16:41 -07:00
Cameron Gutman efebe1828a Update common with Lint fixes 2018-06-09 18:14:39 -07:00
Cameron Gutman 06007e0597 Add a button for adding a PC manually 2018-06-09 18:14:09 -07:00
Cameron Gutman 3a868045d7 Allow the display to go off if the stream disconnects 2018-06-09 17:48:07 -07:00
Cameron Gutman e0a7ff1880 Remove in-progress toast for WOL 2018-06-09 17:23:59 -07:00
Cameron Gutman 88d43bbd40 Disable density splits until I can figure out why we're crashing 2018-06-08 01:13:00 -07:00
Cameron Gutman 30ff319b13 Version 5.8.1 2018-06-08 01:08:09 -07:00
Cameron Gutman 9a0f48b799 Add support for display cutouts on P 2018-06-08 01:05:32 -07:00
Cameron Gutman b52c8a1a8f Use ImageDecoder API on P and higher quality decodes on non-low ram devices 2018-06-08 00:46:40 -07:00
Cameron Gutman 3fde115670 Update to AGP 3.2 alpha 17 2018-06-08 00:24:39 -07:00
Cameron Gutman b6f4d8ff1e Target API 28 2018-06-08 00:23:41 -07:00
Cameron Gutman a7d85a7dd5 Update to AGP 3.2-alpha16 2018-05-29 18:40:26 -07:00
Cameron Gutman 9b238ab6c3 Version 5.8 (take 2) 2018-05-27 20:05:51 -07:00
Cameron Gutman f82ee97c05 Update common to fix channel mapping error in 5.1 high quality mode 2018-05-27 20:04:28 -07:00
43 changed files with 1090 additions and 577 deletions

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
# ProBot No Response (https://probot.github.io/apps/no-response/)
daysUntilClose: 7
responseRequiredLabel: 'need more info'
closeComment: >
This issue has been automatically closed because there was no response to a
request for more information from the issue opener. Please leave a comment or
open a new issue if you have additional information related to this issue.
+14
View File
@@ -0,0 +1,14 @@
# ProBot Stale (https://probot.github.io/apps/stale/)
daysUntilStale: 90
daysUntilClose: 7
exemptLabels:
- accepted
- bug
- enhancement
- meta
staleLabel: stale
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs.
closeComment: false
+4 -1
View File
@@ -1,6 +1,9 @@
#built application files # built application files
*.apk *.apk
*.ap_ *.ap_
*.aab
output.json
out/
# files for the dex VM # files for the dex VM
*.dex *.dex
+1 -3
View File
@@ -1,4 +1,4 @@
# Moonlight # Moonlight Android
[Moonlight](http://moonlight-stream.com) 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. We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
@@ -6,8 +6,6 @@ We reverse engineered the Shield streaming software and created a version that c
Moonlight 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. whether in your own home or over the internet.
[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/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide. Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
## Features ## Features
+11 -4
View File
@@ -1,14 +1,15 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
compileSdkVersion 27 buildToolsVersion '28.0.3'
compileSdkVersion 28
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 27 targetSdkVersion 28
versionName "5.8" versionName "5.10"
versionCode = 161 versionCode = 175
} }
flavorDimensions "root" flavorDimensions "root"
@@ -51,6 +52,12 @@ android {
// to manually switch language in settings. // to manually switch language in settings.
enableSplit = false enableSplit = false
} }
density {
// FIXME: This should not be neccessary but we get
// weird crashes due to missing drawable resources
// when this split is enabled.
enableSplit = false
}
} }
buildTypes { buildTypes {
+2 -1
View File
@@ -67,8 +67,9 @@
</activity> </activity>
<!-- Small hack to support launcher shortcuts without relaunching over and over again when the back button is pressed --> <!-- Small hack to support launcher shortcuts without relaunching over and over again when the back button is pressed -->
<activity <activity
android:name=".AppViewShortcutTrampoline" android:name=".ShortcutTrampoline"
android:noHistory="true" android:noHistory="true"
android:exported="true"
android:resizeableActivity="true" android:resizeableActivity="true"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"> android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
<meta-data <meta-data
+40 -6
View File
@@ -26,6 +26,10 @@ import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.view.ContextMenu; import android.view.ContextMenu;
@@ -36,6 +40,7 @@ import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AbsListView; import android.widget.AbsListView;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.AdapterContextMenuInfo;
@@ -56,7 +61,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
private final static int START_OR_RESUME_ID = 1; private final static int START_OR_RESUME_ID = 1;
private final static int QUIT_ID = 2; private final static int QUIT_ID = 2;
private final static int CANCEL_ID = 3; private final static int CANCEL_ID = 3;
private final static int START_WTIH_QUIT = 4; private final static int START_WITH_QUIT = 4;
private final static int VIEW_DETAILS_ID = 5;
private final static int CREATE_SHORTCUT_ID = 6;
public final static String NAME_EXTRA = "Name"; public final static String NAME_EXTRA = "Name";
public final static String UUID_EXTRA = "UUID"; public final static String UUID_EXTRA = "UUID";
@@ -251,10 +258,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
String computerName = getIntent().getStringExtra(NAME_EXTRA); String computerName = getIntent().getStringExtra(NAME_EXTRA);
String labelText = getResources().getString(R.string.title_applist)+" "+computerName;
TextView label = findViewById(R.id.appListText); TextView label = findViewById(R.id.appListText);
setTitle(labelText); setTitle(computerName);
label.setText(labelText); label.setText(computerName);
// Add a launcher shortcut for this PC (forced, since this is user interaction) // Add a launcher shortcut for this PC (forced, since this is user interaction)
shortcutHelper.createAppViewShortcut(uuidString, computerName, uuidString, true); shortcutHelper.createAppViewShortcut(uuidString, computerName, uuidString, true);
@@ -331,10 +337,25 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit)); menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
} }
else { else {
menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start)); menu.add(Menu.NONE, START_WITH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel)); menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
} }
} }
menu.add(Menu.NONE, VIEW_DETAILS_ID, 3, getResources().getString(R.string.applist_menu_details));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Only add an option to create shortcut if box art is loaded
// and when we're in grid-mode (not list-mode).
ImageView appImageView = info.targetView.findViewById(R.id.grid_image);
if (appImageView != null) {
// We have a grid ImageView, so we must be in grid-mode
BitmapDrawable drawable = (BitmapDrawable)appImageView.getDrawable();
if (drawable != null && drawable.getBitmap() != null) {
// We have a bitmap loaded too
menu.add(Menu.NONE, CREATE_SHORTCUT_ID, 4, getResources().getString(R.string.applist_menu_scut));
}
}
}
} }
@Override @Override
@@ -346,7 +367,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
final AppObject app = (AppObject) appGridAdapter.getItem(info.position); final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
switch (item.getItemId()) { switch (item.getItemId()) {
case START_WTIH_QUIT: case START_WITH_QUIT:
// Display a confirmation dialog first // Display a confirmation dialog first
UiHelper.displayQuitConfirmationDialog(this, new Runnable() { UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override @Override
@@ -386,6 +407,19 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
case CANCEL_ID: case CANCEL_ID:
return true; return true;
case VIEW_DETAILS_ID:
Dialog.displayDialog(AppView.this, getResources().getString(R.string.title_details),
getResources().getString(R.string.applist_details_id) + " " + app.app.getAppId(), false);
return true;
case CREATE_SHORTCUT_ID:
ImageView appImageView = info.targetView.findViewById(R.id.grid_image);
Bitmap appBits = ((BitmapDrawable)appImageView.getDrawable()).getBitmap();
if (!shortcutHelper.createPinnedGameShortcut(uuidString + Integer.valueOf(app.app.getAppId()).toString(), appBits, computer, app.app)) {
Toast.makeText(AppView.this, getResources().getString(R.string.unable_to_pin_shortcut), Toast.LENGTH_LONG).show();
}
return true;
default: default:
return super.onContextItemSelected(item); return super.onContextItemSelected(item);
} }
@@ -1,171 +0,0 @@
package com.limelight;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import java.util.ArrayList;
import java.util.UUID;
public class AppViewShortcutTrampoline extends Activity {
private String uuidString;
private ComputerDetails computer;
private SpinnerDialog blockingLoadSpinner;
public final static String UUID_EXTRA = "UUID";
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder);
// Wait in a separate thread to avoid stalling the UI
new Thread() {
@Override
public void run() {
// Wait for the binder to be ready
localBinder.waitForReady();
// Now make the binder visible
managerBinder = localBinder;
// Get the computer object
computer = managerBinder.getComputer(UUID.fromString(uuidString));
// Force CMS to repoll this machine
managerBinder.invalidateStateForComputer(computer.uuid);
// Start polling
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(final ComputerDetails details) {
// Don't care about other computers
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
return;
}
if (details.state != ComputerDetails.State.UNKNOWN) {
runOnUiThread(new Runnable() {
@Override
public void run() {
// Stop showing the spinner
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
// If the managerBinder was destroyed before this callback,
// just finish the activity.
if (managerBinder == null) {
finish();
return;
}
if (details.state == ComputerDetails.State.ONLINE) {
// Close this activity
finish();
// Create a new activity stack for this launch
ArrayList<Intent> intentStack = new ArrayList<>();
Intent i;
// Add the PC view at the back (and clear the task)
i = new Intent(AppViewShortcutTrampoline.this, PcView.class);
i.setAction(Intent.ACTION_MAIN);
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
intentStack.add(i);
// Take this intent's data and create an intent to start the app view
i = new Intent(getIntent());
i.setClass(AppViewShortcutTrampoline.this, AppView.class);
intentStack.add(i);
// If a game is running, we'll make the stream the top level activity
if (details.runningGameId != 0) {
intentStack.add(ServerHelper.createStartIntent(AppViewShortcutTrampoline.this,
new NvApp("app", details.runningGameId, false), details, managerBinder));
}
// Now start the activities
startActivities(intentStack.toArray(new Intent[]{}));
}
else if (details.state == ComputerDetails.State.OFFLINE) {
// Computer offline - display an error dialog
Dialog.displayDialog(AppViewShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.error_pc_offline),
true);
}
// We don't want any more callbacks from now on, so go ahead
// and unbind from the service
if (managerBinder != null) {
managerBinder.stopPolling();
unbindService(serviceConnection);
managerBinder = null;
}
}
});
}
}
});
}
}.start();
}
public void onServiceDisconnected(ComponentName className) {
managerBinder = null;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
UiHelper.notifyNewRootView(this);
uuidString = getIntent().getStringExtra(UUID_EXTRA);
// Bind to the computer manager service
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title),
getResources().getString(R.string.applist_connect_msg), true);
}
@Override
protected void onPause() {
super.onPause();
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
Dialog.closeDialogs();
if (managerBinder != null) {
managerBinder.stopPolling();
unbindService(serviceConnection);
managerBinder = null;
}
finish();
}
}
+139 -60
View File
@@ -105,6 +105,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private boolean grabbedInput = true; private boolean grabbedInput = true;
private boolean grabComboDown = false; private boolean grabComboDown = false;
private StreamView streamView; private StreamView streamView;
private boolean gotBackPointerEvent = false;
private boolean syntheticBackDown = false;
private ShortcutHelper shortcutHelper; private ShortcutHelper shortcutHelper;
@@ -146,10 +148,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// We don't want a title bar // We don't want a title bar
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
// Full-screen and don't let the display go off // Full-screen
getWindow().addFlags( getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
WindowManager.LayoutParams.FLAG_FULLSCREEN |
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
// If we're going to use immersive mode, we want to have // If we're going to use immersive mode, we want to have
// the entire screen // the entire screen
@@ -185,6 +185,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
prefConfig = PreferenceConfiguration.readPreferences(this); prefConfig = PreferenceConfiguration.readPreferences(this);
tombstonePrefs = Game.this.getSharedPreferences("DecoderTombstone", 0); tombstonePrefs = Game.this.getSharedPreferences("DecoderTombstone", 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && prefConfig.stretchVideo) {
// Allow the activity to layout under notches if the fill-screen option
// was turned on by the user
getWindow().getAttributes().layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
}
// Listen for events on the game surface // Listen for events on the game surface
streamView = findViewById(R.id.surfaceView); streamView = findViewById(R.id.surfaceView);
@@ -830,9 +836,18 @@ public class Game extends Activity implements SurfaceHolder.Callback,
} }
// Handle a synthetic back button event that some Android OS versions // Handle a synthetic back button event that some Android OS versions
// create as a result of a right-click. // create as a result of a right-click. This event WILL repeat if
if (event.getSource() == InputDevice.SOURCE_MOUSE && event.getKeyCode() == KeyEvent.KEYCODE_BACK) { // the right mouse button is held down, so we ignore those.
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT); if ((event.getSource() == InputDevice.SOURCE_MOUSE ||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
// It appears this may get turned into a right-click pointer event
// if we don't return true to indicate that we handled it on
// some devices. https://github.com/moonlight-stream/moonlight-android/issues/634
if (event.getRepeatCount() == 0 && !gotBackPointerEvent) {
syntheticBackDown = true;
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
}
return true; return true;
} }
@@ -881,8 +896,19 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Handle a synthetic back button event that some Android OS versions // Handle a synthetic back button event that some Android OS versions
// create as a result of a right-click. // create as a result of a right-click.
if (event.getSource() == InputDevice.SOURCE_MOUSE && event.getKeyCode() == KeyEvent.KEYCODE_BACK) { if ((event.getSource() == InputDevice.SOURCE_MOUSE ||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT); event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
// It appears this may get turned into a right-click pointer event
// if we don't return true to indicate that we handled it on
// some devices. https://github.com/moonlight-stream/moonlight-android/issues/634
if (!gotBackPointerEvent || syntheticBackDown) {
// We need to raise the button if gotBackPointerEvent is true
// in the case where it transitioned to true after we already
// sent the right click down event.
syntheticBackDown = false;
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
return true; return true;
} }
@@ -949,6 +975,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
{ {
// This case is for mice // This case is for mice
if (event.getSource() == InputDevice.SOURCE_MOUSE || if (event.getSource() == InputDevice.SOURCE_MOUSE ||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE ||
(event.getPointerCount() >= 1 && (event.getPointerCount() >= 1 &&
event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE)) event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE))
{ {
@@ -964,6 +991,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL); byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
conn.sendMouseScroll(vScrollClicks); conn.sendMouseScroll(vScrollClicks);
} }
else if (event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER ||
event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) {
// On some devices (Galaxy S8 without Oreo pointer capture), we can
// get spurious ACTION_HOVER_ENTER events when right clicking with
// incorrect X and Y coordinates. Just eat this event without processing it.
return true;
}
if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) { if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) { if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {
@@ -981,6 +1015,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
else { else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT); conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
} }
// Don't use the KEYCODE_BACK hack (which interferes with mice
// with actual back buttons) since we're getting right clicks
// using this callback.
gotBackPointerEvent = true;
} }
if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) { if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) {
@@ -992,6 +1031,32 @@ public class Game extends Activity implements SurfaceHolder.Callback,
} }
} }
// HACK: Disable mouse back button press on Xiaomi due to reported
// issues with right clicks triggering it.
if (!("Xiaomi".equalsIgnoreCase(Build.MANUFACTURER))) {
if ((changedButtons & MotionEvent.BUTTON_BACK) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_BACK) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_X1);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_X1);
}
// Don't use the KEYCODE_BACK hack. That will cause this
// button press to trigger a right-click.
gotBackPointerEvent = true;
}
}
if ((changedButtons & MotionEvent.BUTTON_FORWARD) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_FORWARD) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_X2);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_X2);
}
}
// Get relative axis values if we can // Get relative axis values if we can
if (inputCaptureProvider.eventHasRelativeMouseAxes(event)) { if (inputCaptureProvider.eventHasRelativeMouseAxes(event)) {
// Send the deltas straight from the motion event // Send the deltas straight from the motion event
@@ -1010,12 +1075,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
lastMouseY = (int)event.getY(); lastMouseY = (int)event.getY();
} }
else { else {
// First process the history // Don't process the history. We just want the current position now.
for (int i = 0; i < event.getHistorySize(); i++) {
updateMousePosition((int)event.getHistoricalX(i), (int)event.getHistoricalY(i));
}
// Now process the current values
updateMousePosition((int)event.getX(), (int)event.getY()); updateMousePosition((int)event.getX(), (int)event.getY());
} }
@@ -1163,10 +1223,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
} }
@Override @Override
public void stageStarting(String stage) { public void stageStarting(final String stage) {
if (spinner != null) { runOnUiThread(new Runnable() {
spinner.setMessage(getResources().getString(R.string.conn_starting)+" "+stage); @Override
} public void run() {
if (spinner != null) {
spinner.setMessage(getResources().getString(R.string.conn_starting) + " " + stage);
}
}
});
} }
@Override @Override
@@ -1191,70 +1256,78 @@ public class Game extends Activity implements SurfaceHolder.Callback,
} }
@Override @Override
public void stageFailed(String stage, long errorCode) { public void stageFailed(final String stage, final long errorCode) {
if (spinner != null) { runOnUiThread(new Runnable() {
spinner.dismiss(); @Override
spinner = null; public void run() {
} if (spinner != null) {
spinner.dismiss();
spinner = null;
}
// Enable cursor visibility again if (!displayedFailureDialog) {
inputCaptureProvider.disableCapture(); displayedFailureDialog = true;
LimeLog.severe(stage + " failed: " + errorCode);
if (!displayedFailureDialog) { // If video initialization failed and the surface is still valid, display extra information for the user
displayedFailureDialog = true; if (stage.contains("video") && streamView.getHolder().getSurface().isValid()) {
LimeLog.severe(stage+" failed: "+errorCode);
// If video initialization failed and the surface is still valid, display extra information for the user
if (stage.contains("video") && streamView.getHolder().getSurface().isValid()) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(Game.this, "Video decoder failed to initialize. Your device may not support the selected resolution.", Toast.LENGTH_LONG).show(); Toast.makeText(Game.this, "Video decoder failed to initialize. Your device may not support the selected resolution.", Toast.LENGTH_LONG).show();
} }
});
}
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.conn_error_msg)+" "+stage, true); getResources().getString(R.string.conn_error_msg) + " " + stage, true);
} }
}
});
} }
@Override @Override
public void connectionTerminated(long errorCode) { public void connectionTerminated(final long errorCode) {
// Enable cursor visibility again runOnUiThread(new Runnable() {
inputCaptureProvider.disableCapture(); @Override
public void run() {
// Let the display go to sleep now
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
if (!displayedFailureDialog) { // Enable cursor visibility again
displayedFailureDialog = true; inputCaptureProvider.disableCapture();
LimeLog.severe("Connection terminated: "+errorCode);
stopConnection();
Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title), if (!displayedFailureDialog) {
getResources().getString(R.string.conn_terminated_msg), true); displayedFailureDialog = true;
} LimeLog.severe("Connection terminated: " + errorCode);
stopConnection();
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_terminated_title),
getResources().getString(R.string.conn_terminated_msg), true);
}
}
});
} }
@Override @Override
public void connectionStarted() { public void connectionStarted() {
if (spinner != null) {
spinner.dismiss();
spinner = null;
}
connected = true;
connecting = false;
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
if (spinner != null) {
spinner.dismiss();
spinner = null;
}
connected = true;
connecting = false;
// Hide the mouse cursor now. Doing it before // Hide the mouse cursor now. Doing it before
// dismissing the spinner seems to be undone // dismissing the spinner seems to be undone
// when the spinner gets displayed. // when the spinner gets displayed.
inputCaptureProvider.enableCapture(); inputCaptureProvider.enableCapture();
// Keep the display on
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
hideSystemUi(1000);
} }
}); });
hideSystemUi(1000);
} }
@Override @Override
@@ -1334,6 +1407,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
case EvdevListener.BUTTON_RIGHT: case EvdevListener.BUTTON_RIGHT:
buttonIndex = MouseButtonPacket.BUTTON_RIGHT; buttonIndex = MouseButtonPacket.BUTTON_RIGHT;
break; break;
case EvdevListener.BUTTON_X1:
buttonIndex = MouseButtonPacket.BUTTON_X1;
break;
case EvdevListener.BUTTON_X2:
buttonIndex = MouseButtonPacket.BUTTON_X2;
break;
default: default:
LimeLog.warning("Unhandled button: "+buttonId); LimeLog.warning("Unhandled button: "+buttonId);
return; return;
+27 -12
View File
@@ -28,6 +28,7 @@ import com.limelight.utils.ShortcutHelper;
import com.limelight.utils.UiHelper; import com.limelight.utils.UiHelper;
import android.app.Activity; import android.app.Activity;
import android.app.ActivityManager;
import android.app.Service; import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Intent; import android.content.Intent;
@@ -112,6 +113,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
private final static int DELETE_ID = 5; private final static int DELETE_ID = 5;
private final static int RESUME_ID = 6; private final static int RESUME_ID = 6;
private final static int QUIT_ID = 7; private final static int QUIT_ID = 7;
private final static int VIEW_DETAILS_ID = 8;
private void initializeViews() { private void initializeViews() {
setContentView(R.layout.activity_pc_view); setContentView(R.layout.activity_pc_view);
@@ -311,8 +313,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position); ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
// Inflate the context menu // Inflate the context menu
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE || if (computer.details.state == ComputerDetails.State.OFFLINE ||
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) { computer.details.state == ComputerDetails.State.UNKNOWN) {
menu.add(Menu.NONE, WOL_ID, 1, getResources().getString(R.string.pcview_menu_send_wol)); menu.add(Menu.NONE, WOL_ID, 1, getResources().getString(R.string.pcview_menu_send_wol));
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc)); menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
} }
@@ -332,6 +334,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
// it with delete which actually work // it with delete which actually work
menu.add(Menu.NONE, DELETE_ID, 4, getResources().getString(R.string.pcview_menu_delete_pc)); menu.add(Menu.NONE, DELETE_ID, 4, getResources().getString(R.string.pcview_menu_delete_pc));
} }
menu.add(Menu.NONE, VIEW_DETAILS_ID, 5, getResources().getString(R.string.pcview_menu_details));
} }
@Override @Override
@@ -343,7 +346,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
} }
private void doPair(final ComputerDetails computer) { private void doPair(final ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) { if (computer.state == ComputerDetails.State.OFFLINE) {
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
return; return;
} }
@@ -452,7 +455,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
return; return;
} }
Toast.makeText(PcView.this, getResources().getString(R.string.wol_waking_pc), Toast.LENGTH_SHORT).show();
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -476,7 +478,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
} }
private void doUnpair(final ComputerDetails computer) { private void doUnpair(final ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) { if (computer.state == ComputerDetails.State.OFFLINE) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
return; return;
} }
@@ -528,7 +530,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
} }
private void doAppList(ComputerDetails computer) { private void doAppList(ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) { if (computer.state == ComputerDetails.State.OFFLINE) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
return; return;
} }
@@ -561,12 +563,21 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
return true; return true;
case DELETE_ID: case DELETE_ID:
if (managerBinder == null) { if (ActivityManager.isUserAMonkey()) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); LimeLog.info("Ignoring delete PC request from monkey");
return true; return true;
} }
managerBinder.removeComputer(computer.details.name); UiHelper.displayDeletePcConfirmationDialog(this, computer.details, new Runnable() {
removeComputer(computer.details); @Override
public void run() {
if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return;
}
managerBinder.removeComputer(computer.details.name);
removeComputer(computer.details);
}
}, null);
return true; return true;
case APP_LIST_ID: case APP_LIST_ID:
@@ -599,6 +610,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
}, null); }, null);
return true; return true;
case VIEW_DETAILS_ID:
Dialog.displayDialog(PcView.this, getResources().getString(R.string.title_details), computer.details.toString(), false);
return true;
default: default:
return super.onContextItemSelected(item); return super.onContextItemSelected(item);
} }
@@ -675,8 +690,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
public void onItemClick(AdapterView<?> arg0, View arg1, int pos, public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) { long id) {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos); ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos);
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN || if (computer.details.state == ComputerDetails.State.UNKNOWN ||
computer.details.reachability == ComputerDetails.Reachability.OFFLINE) { computer.details.state == ComputerDetails.State.OFFLINE) {
// Open the context menu if a PC is offline or refreshing // Open the context menu if a PC is offline or refreshing
openContextMenu(arg1); openContextMenu(arg1);
} else if (computer.details.pairState != PairState.PAIRED) { } else if (computer.details.pairState != PairState.PAIRED) {
@@ -0,0 +1,267 @@
package com.limelight;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import java.util.ArrayList;
import java.util.UUID;
public class ShortcutTrampoline extends Activity {
private String uuidString;
private String appIdString;
private ArrayList<Intent> intentStack = new ArrayList<>();
private ComputerDetails computer;
private SpinnerDialog blockingLoadSpinner;
public final static String APP_ID_EXTRA = "AppId";
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder);
// Wait in a separate thread to avoid stalling the UI
new Thread() {
@Override
public void run() {
// Wait for the binder to be ready
localBinder.waitForReady();
// Now make the binder visible
managerBinder = localBinder;
// Get the computer object
computer = managerBinder.getComputer(UUID.fromString(uuidString));
if (computer == null) {
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.scut_pc_not_found),
true);
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
if (managerBinder != null) {
unbindService(serviceConnection);
managerBinder = null;
}
return;
}
// Force CMS to repoll this machine
managerBinder.invalidateStateForComputer(computer.uuid);
// Start polling
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(final ComputerDetails details) {
// Don't care about other computers
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
return;
}
if (details.state != ComputerDetails.State.UNKNOWN) {
runOnUiThread(new Runnable() {
@Override
public void run() {
// Stop showing the spinner
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
// If the managerBinder was destroyed before this callback,
// just finish the activity.
if (managerBinder == null) {
finish();
return;
}
if (details.state == ComputerDetails.State.ONLINE && details.pairState == PairingManager.PairState.PAIRED) {
// Launch game if provided app ID, otherwise launch app view
if (appIdString != null && appIdString.length() > 0) {
if (details.runningGameId == 0 || details.runningGameId == Integer.parseInt(appIdString)) {
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this,
new NvApp("app", Integer.parseInt(appIdString), false), details, managerBinder));
// Close this activity
finish();
// Now start the activities
startActivities(intentStack.toArray(new Intent[]{}));
} else {
// Create the start intent immediately, so we can safely unbind the managerBinder
// below before we return.
final Intent startIntent = ServerHelper.createStartIntent(ShortcutTrampoline.this,
new NvApp("app", Integer.parseInt(appIdString), false), details, managerBinder);
UiHelper.displayQuitConfirmationDialog(ShortcutTrampoline.this, new Runnable() {
@Override
public void run() {
intentStack.add(startIntent);
// Close this activity
finish();
// Now start the activities
startActivities(intentStack.toArray(new Intent[]{}));
}
}, new Runnable() {
@Override
public void run() {
// Close this activity
finish();
}
});
}
} else {
// Close this activity
finish();
// Add the PC view at the back (and clear the task)
Intent i;
i = new Intent(ShortcutTrampoline.this, PcView.class);
i.setAction(Intent.ACTION_MAIN);
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
intentStack.add(i);
// Take this intent's data and create an intent to start the app view
i = new Intent(getIntent());
i.setClass(ShortcutTrampoline.this, AppView.class);
intentStack.add(i);
// If a game is running, we'll make the stream the top level activity
if (details.runningGameId != 0) {
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this,
new NvApp("app", details.runningGameId, false), details, managerBinder));
}
// Now start the activities
startActivities(intentStack.toArray(new Intent[]{}));
}
}
else if (details.state == ComputerDetails.State.OFFLINE) {
// Computer offline - display an error dialog
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.error_pc_offline),
true);
} else if (details.pairState != PairingManager.PairState.PAIRED) {
// Computer not paired - display an error dialog
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.scut_not_paired),
true);
}
// We don't want any more callbacks from now on, so go ahead
// and unbind from the service
if (managerBinder != null) {
managerBinder.stopPolling();
unbindService(serviceConnection);
managerBinder = null;
}
}
});
}
}
});
}
}.start();
}
public void onServiceDisconnected(ComponentName className) {
managerBinder = null;
}
};
protected boolean validateInput() {
// Validate UUID
try {
UUID.fromString(uuidString);
} catch (IllegalArgumentException ex) {
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.scut_invalid_uuid),
true);
return false;
}
// Validate App ID (if provided)
if (appIdString != null && !appIdString.isEmpty()) {
try {
Integer.parseInt(appIdString);
} catch (NumberFormatException ex) {
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.scut_invalid_app_id),
true);
return false;
}
}
return true;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
UiHelper.notifyNewRootView(this);
uuidString = getIntent().getStringExtra(AppView.UUID_EXTRA);
appIdString = getIntent().getStringExtra(APP_ID_EXTRA);
if (validateInput()) {
// Bind to the computer manager service
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title),
getResources().getString(R.string.applist_connect_msg), true);
}
}
@Override
protected void onStop() {
super.onStop();
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
Dialog.closeDialogs();
if (managerBinder != null) {
managerBinder.stopPolling();
unbindService(serviceConnection);
managerBinder = null;
}
finish();
}
}
@@ -4,6 +4,7 @@ import android.content.Context;
import android.hardware.input.InputManager; import android.hardware.input.InputManager;
import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager; import android.hardware.usb.UsbManager;
import android.os.Build;
import android.os.SystemClock; import android.os.SystemClock;
import android.util.SparseArray; import android.util.SparseArray;
import android.view.InputDevice; import android.view.InputDevice;
@@ -22,6 +23,7 @@ import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.ui.GameGestures; import com.limelight.ui.GameGestures;
import com.limelight.utils.Vector2d; import com.limelight.utils.Vector2d;
import java.lang.reflect.InvocationTargetException;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
@@ -308,6 +310,79 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
return context; return context;
} }
private static boolean isExternal(InputDevice dev) {
try {
// Landroid/view/InputDevice;->isExternal()Z is on the light graylist in Android P
return (Boolean)dev.getClass().getMethod("isExternal").invoke(dev);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (ClassCastException e) {
e.printStackTrace();
}
// Answer true if we don't know
return true;
}
private boolean shouldIgnoreBack(InputDevice dev) {
String devName = dev.getName();
// The Serval has a Select button but the framework doesn't
// know about that because it uses a non-standard scancode.
if (devName.contains("Razer Serval")) {
return true;
}
// Classify this device as a remote by name if it has no joystick axes
if (getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_X) == null &&
getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Y) == null &&
devName.toLowerCase().contains("remote")) {
return true;
}
// Otherwise, dynamically try to determine whether we should allow this
// back button to function for navigation.
//
// First, check if this is an internal device we're being called on.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !isExternal(dev)) {
InputManager im = (InputManager) activityContext.getSystemService(Context.INPUT_SERVICE);
boolean foundInternalGamepad = false;
boolean foundInternalSelect = false;
for (int id : im.getInputDeviceIds()) {
InputDevice currentDev = im.getInputDevice(id);
// Ignore external devices
if (currentDev == null || isExternal(currentDev)) {
continue;
}
// Note that we are explicitly NOT excluding the current device we're examining here,
// since the other gamepad buttons may be on our current device and that's fine.
boolean[] keys = currentDev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BUTTON_A);
if (keys[0]) {
foundInternalSelect = true;
}
if (keys[1]) {
foundInternalGamepad = true;
}
}
// Allow the back button to function for navigation if we either:
// a) have no internal gamepad (most phones)
// b) have an internal gamepad but also have an internal select button (GPD XD)
// but not:
// c) have an internal gamepad but no internal select button (NVIDIA SHIELD Portable)
return !foundInternalGamepad || foundInternalSelect;
}
return false;
}
private InputDeviceContext createInputDeviceContextForDevice(InputDevice dev) { private InputDeviceContext createInputDeviceContextForDevice(InputDevice dev) {
InputDeviceContext context = new InputDeviceContext(); InputDeviceContext context = new InputDeviceContext();
String devName = dev.getName(); String devName = dev.getName();
@@ -440,6 +515,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
} }
} }
context.ignoreBack = shouldIgnoreBack(dev);
if (devName != null) { if (devName != null) {
// For the Nexus Player (and probably other ATV devices), we should // 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 // use the back button as start since it doesn't have a start/menu button
@@ -459,30 +536,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
// so we increase the deadzone on them to minimize this // so we increase the deadzone on them to minimize this
context.triggerDeadzone = 0.30f; context.triggerDeadzone = 0.30f;
} }
// Classify this device as a remote by name
else if (devName.toLowerCase().contains("remote")) {
// It's only a remote if it doesn't any sticks
if (!context.hasJoystickAxes) {
context.ignoreBack = true;
}
}
// SHIELD controllers will use small stick deadzones // SHIELD controllers will use small stick deadzones
else if (devName.contains("SHIELD")) { else if (devName.contains("SHIELD")) {
context.leftStickDeadzoneRadius = 0.07f; context.leftStickDeadzoneRadius = 0.07f;
context.rightStickDeadzoneRadius = 0.07f; context.rightStickDeadzoneRadius = 0.07f;
} }
// Samsung's face buttons appear as a non-virtual button so we'll explicitly ignore
// back presses on this device. The Goodix buttons on the Nokia 6 also appear
// non-virtual so we'll ignore those too.
else if (devName.equals("sec_touchscreen") || devName.equals("sec_touchkey") ||
devName.equals("goodix_fp")) {
context.ignoreBack = true;
}
// The Serval has a couple of unknown buttons that are start and select. It also has // 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. // a back button which we want to ignore since there's already a select button.
else if (devName.contains("Razer Serval")) { else if (devName.contains("Razer Serval")) {
context.isServal = true; context.isServal = true;
context.ignoreBack = true;
} }
// The Xbox One S Bluetooth controller has some mappings that need fixing up. // The Xbox One S Bluetooth controller has some mappings that need fixing up.
// However, Microsoft released a firmware update with no change to VID/PID // However, Microsoft released a firmware update with no change to VID/PID
@@ -43,11 +43,19 @@ public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
@Override @Override
public float getRelativeAxisX(MotionEvent event) { public float getRelativeAxisX(MotionEvent event) {
return event.getX(); float x = event.getX();
for (int i = 0; i < event.getHistorySize(); i++) {
x += event.getHistoricalX(i);
}
return x;
} }
@Override @Override
public float getRelativeAxisY(MotionEvent event) { public float getRelativeAxisY(MotionEvent event) {
return event.getY(); float y = event.getY();
for (int i = 0; i < event.getHistorySize(); i++) {
y += event.getHistoricalY(i);
}
return y;
} }
} }
@@ -4,6 +4,8 @@ public interface EvdevListener {
int BUTTON_LEFT = 1; int BUTTON_LEFT = 1;
int BUTTON_MIDDLE = 2; int BUTTON_MIDDLE = 2;
int BUTTON_RIGHT = 3; int BUTTON_RIGHT = 3;
int BUTTON_X1 = 4;
int BUTTON_X2 = 5;
void mouseMove(int deltaX, int deltaY); void mouseMove(int deltaX, int deltaY);
void mouseButtonEvent(int buttonId, boolean down); void mouseButtonEvent(int buttonId, boolean down);
@@ -60,6 +60,7 @@ public class VirtualController {
buttonConfigure = new Button(context); buttonConfigure = new Button(context);
buttonConfigure.setAlpha(0.25f); buttonConfigure.setAlpha(0.25f);
buttonConfigure.setFocusable(false);
buttonConfigure.setBackgroundResource(R.drawable.ic_settings); buttonConfigure.setBackgroundResource(R.drawable.ic_settings);
buttonConfigure.setOnClickListener(new View.OnClickListener() { buttonConfigure.setOnClickListener(new View.OnClickListener() {
@Override @Override
@@ -131,8 +131,8 @@ public class MediaCodecHelper {
} }
// Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame). // Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame).
// I know the Fire TV 2 works, so I'll just whitelist Amazon devices which seem // I know the Fire TV 2 and 3 works, so I'll just whitelist Amazon devices which seem
// to actually be tested. Ugh... // to actually be tested.
if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) { if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) {
whitelistedHevcDecoders.add("omx.mtk"); whitelistedHevcDecoders.add("omx.mtk");
whitelistedHevcDecoders.add("omx.amlogic"); whitelistedHevcDecoders.add("omx.amlogic");
@@ -166,6 +166,10 @@ public class MediaCodecHelper {
} }
} }
private static boolean isPowerVR(String glRenderer) {
return glRenderer.toLowerCase().contains("powervr");
}
private static String getAdrenoVersionString(String glRenderer) { private static String getAdrenoVersionString(String glRenderer) {
glRenderer = glRenderer.toLowerCase().trim(); glRenderer = glRenderer.toLowerCase().trim();
@@ -259,6 +263,18 @@ public class MediaCodecHelper {
else { else {
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevc"); blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevc");
} }
// Older MediaTek SoCs have issues with HEVC rendering but the newer chips with
// PowerVR GPUs have good HEVC support.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isPowerVR(glRenderer)) {
LimeLog.info("Added omx.mtk to HEVC decoders based on PowerVR GPU");
whitelistedHevcDecoders.add("omx.mtk");
// This SoC (MT8176 in GPD XD+) supports AVC RFI too, but the maxNumReferenceFrames setting
// required to make it work adds a huge amount of latency.
LimeLog.info("Added omx.mtk to RFI list for HEVC");
refFrameInvalidationHevcPrefixes.add("omx.mtk");
}
} }
initialized = true; initialized = true;
@@ -1,7 +1,5 @@
package com.limelight.computers; package com.limelight.computers;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@@ -17,15 +15,14 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteException;
public class ComputerDatabaseManager { public class ComputerDatabaseManager {
private static final String COMPUTER_DB_NAME = "computers.db"; private static final String COMPUTER_DB_NAME = "computers2.db";
private static final String COMPUTER_TABLE_NAME = "Computers"; 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 COMPUTER_UUID_COLUMN_NAME = "UUID";
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp"; private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp"; private static final String LOCAL_ADDRESS_COLUMN_NAME = "LocalAddress";
private static final String MAC_COLUMN_NAME = "Mac"; private static final String REMOTE_ADDRESS_COLUMN_NAME = "RemoteAddress";
private static final String MANUAL_ADDRESS_COLUMN_NAME = "ManualAddress";
private static final String ADDRESS_PREFIX = "ADDRESS_PREFIX__"; private static final String MAC_ADDRESS_COLUMN_NAME = "MacAddress";
private SQLiteDatabase computerDb; private SQLiteDatabase computerDb;
@@ -38,20 +35,27 @@ public class ComputerDatabaseManager {
c.deleteDatabase(COMPUTER_DB_NAME); c.deleteDatabase(COMPUTER_DB_NAME);
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null); computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
} }
initializeDb(); initializeDb(c);
} }
public void close() { public void close() {
computerDb.close(); computerDb.close();
} }
private void initializeDb() { private void initializeDb(Context c) {
// Create tables if they aren't already there // Create tables if they aren't already there
computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," + computerDb.execSQL(String.format((Locale)null,
" %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)", "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY, %s TEXT NOT NULL, %s TEXT, %s TEXT, %s TEXT, %s TEXT)",
COMPUTER_TABLE_NAME, COMPUTER_TABLE_NAME,
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME,
REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME)); LOCAL_ADDRESS_COLUMN_NAME, REMOTE_ADDRESS_COLUMN_NAME, MANUAL_ADDRESS_COLUMN_NAME,
MAC_ADDRESS_COLUMN_NAME));
// Move all computers from the old DB (if any) to the new one
List<ComputerDetails> oldComputers = LegacyDatabaseReader.migrateAllComputers(c);
for (ComputerDetails computer : oldComputers) {
updateComputer(computer);
}
} }
public void deleteComputer(String name) { public void deleteComputer(String name) {
@@ -60,20 +64,19 @@ public class ComputerDatabaseManager {
public boolean updateComputer(ComputerDetails details) { public boolean updateComputer(ComputerDetails details) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString()); values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
values.put(LOCAL_IP_COLUMN_NAME, ADDRESS_PREFIX+details.localAddress); values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
values.put(REMOTE_IP_COLUMN_NAME, ADDRESS_PREFIX+details.remoteAddress); values.put(LOCAL_ADDRESS_COLUMN_NAME, details.localAddress);
values.put(MAC_COLUMN_NAME, details.macAddress); values.put(REMOTE_ADDRESS_COLUMN_NAME, details.remoteAddress);
values.put(MANUAL_ADDRESS_COLUMN_NAME, details.manualAddress);
values.put(MAC_ADDRESS_COLUMN_NAME, details.macAddress);
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE); return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
} }
private ComputerDetails getComputerFromCursor(Cursor c) { private ComputerDetails getComputerFromCursor(Cursor c) {
ComputerDetails details = new ComputerDetails(); ComputerDetails details = new ComputerDetails();
details.name = c.getString(0); String uuidStr = c.getString(0);
String uuidStr = c.getString(1);
try { try {
details.uuid = UUID.fromString(uuidStr); details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
@@ -81,43 +84,14 @@ public class ComputerDatabaseManager {
LimeLog.severe("DB: Corrupted UUID for "+details.name); LimeLog.severe("DB: Corrupted UUID for "+details.name);
} }
// An earlier schema defined addresses as byte blobs. We'll details.name = c.getString(1);
// gracefully migrate those to strings so we can store DNS names details.localAddress = c.getString(2);
// too. To disambiguate, we'll need to prefix them with a string details.remoteAddress = c.getString(3);
// greater than the allowable IP address length. details.manualAddress = c.getString(4);
try { details.macAddress = c.getString(5);
details.localAddress = InetAddress.getByAddress(c.getBlob(2)).getHostAddress();
LimeLog.warning("DB: Legacy local address for "+details.name);
} catch (UnknownHostException e) {
// This is probably a hostname/address with the prefix string
String stringData = c.getString(2);
if (stringData.startsWith(ADDRESS_PREFIX)) {
details.localAddress = c.getString(2).substring(ADDRESS_PREFIX.length());
}
else {
LimeLog.severe("DB: Corrupted local address for "+details.name);
}
}
try {
details.remoteAddress = InetAddress.getByAddress(c.getBlob(3)).getHostAddress();
LimeLog.warning("DB: Legacy remote address for "+details.name);
} catch (UnknownHostException e) {
// This is probably a hostname/address with the prefix string
String stringData = c.getString(3);
if (stringData.startsWith(ADDRESS_PREFIX)) {
details.remoteAddress = c.getString(3).substring(ADDRESS_PREFIX.length());
}
else {
LimeLog.severe("DB: Corrupted local address for "+details.name);
}
}
details.macAddress = c.getString(4);
// This signifies we don't have dynamic state (like pair state) // This signifies we don't have dynamic state (like pair state)
details.state = ComputerDetails.State.UNKNOWN; details.state = ComputerDetails.State.UNKNOWN;
details.reachability = ComputerDetails.Reachability.UNKNOWN;
return details; return details;
} }
@@ -128,13 +102,11 @@ public class ComputerDatabaseManager {
while (c.moveToNext()) { while (c.moveToNext()) {
ComputerDetails details = getComputerFromCursor(c); ComputerDetails details = getComputerFromCursor(c);
// If a field is corrupt or missing, skip the database entry // If a critical field is corrupt or missing, skip the database entry
if (details.uuid == null || details.localAddress == null || details.remoteAddress == null || if (details.uuid == null) {
details.macAddress == null) {
continue; continue;
} }
computerList.add(details); computerList.add(details);
} }
@@ -143,8 +115,8 @@ public class ComputerDatabaseManager {
return computerList; return computerList;
} }
public ComputerDetails getComputerByName(String name) { public ComputerDetails getComputerByUUID(UUID uuid) {
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name}, null, null, null); Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{ uuid.toString() }, null, null, null);
if (!c.moveToFirst()) { if (!c.moveToFirst()) {
// No matching computer // No matching computer
c.close(); c.close();
@@ -154,9 +126,8 @@ public class ComputerDatabaseManager {
ComputerDetails details = getComputerFromCursor(c); ComputerDetails details = getComputerFromCursor(c);
c.close(); c.close();
// If a field is corrupt or missing, delete the database entry // If a critical field is corrupt or missing, delete the database entry
if (details.uuid == null || details.localAddress == null || details.remoteAddress == null || if (details.uuid == null) {
details.macAddress == null) {
deleteComputer(details.name); deleteComputer(details.name);
return null; return null;
} }
@@ -3,10 +3,9 @@ package com.limelight.computers;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.StringReader; import java.io.StringReader;
import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.Socket; import java.net.Socket;
import java.net.UnknownHostException; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@@ -15,6 +14,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import com.limelight.LimeLog; import com.limelight.LimeLog;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.discovery.DiscoveryService; import com.limelight.discovery.DiscoveryService;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp; import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.NvHTTP;
@@ -95,7 +95,6 @@ public class ComputerManagerService extends Service {
} }
details.state = ComputerDetails.State.OFFLINE; details.state = ComputerDetails.State.OFFLINE;
details.reachability = ComputerDetails.Reachability.OFFLINE;
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
releaseLocalDatabaseReference(); releaseLocalDatabaseReference();
@@ -109,7 +108,7 @@ public class ComputerManagerService extends Service {
if (!newPc) { if (!newPc) {
// Check if it's in the database because it could have been // Check if it's in the database because it could have been
// removed after this was issued // removed after this was issued
if (dbManager.getComputerByName(details.name) == null) { if (dbManager.getComputerByUUID(details.uuid) == null) {
// It's gone // It's gone
releaseLocalDatabaseReference(); releaseLocalDatabaseReference();
return false; return false;
@@ -156,7 +155,7 @@ public class ComputerManagerService extends Service {
} }
} }
}; };
t.setName("Polling thread for " + tuple.computer.localAddress); t.setName("Polling thread for " + tuple.computer.name);
return t; return t;
} }
@@ -177,7 +176,6 @@ public class ComputerManagerService extends Service {
if (System.currentTimeMillis() - tuple.lastSuccessfulPollMs > POLL_DATA_TTL_MS) { if (System.currentTimeMillis() - tuple.lastSuccessfulPollMs > POLL_DATA_TTL_MS) {
LimeLog.info("Timing out polled state for "+tuple.computer.name); LimeLog.info("Timing out polled state for "+tuple.computer.name);
tuple.computer.state = ComputerDetails.State.UNKNOWN; tuple.computer.state = ComputerDetails.State.UNKNOWN;
tuple.computer.reachability = ComputerDetails.Reachability.UNKNOWN;
} }
// Report this computer initially // Report this computer initially
@@ -253,7 +251,6 @@ public class ComputerManagerService extends Service {
// from wiping this change out // from wiping this change out
synchronized (tuple.networkLock) { synchronized (tuple.networkLock) {
tuple.computer.state = ComputerDetails.State.UNKNOWN; tuple.computer.state = ComputerDetails.State.UNKNOWN;
tuple.computer.reachability = ComputerDetails.Reachability.UNKNOWN;
} }
} }
} }
@@ -312,17 +309,8 @@ public class ComputerManagerService extends Service {
for (PollingTuple tuple : pollingTuples) { for (PollingTuple tuple : pollingTuples) {
// Check if this is the same computer // Check if this is the same computer
if (tuple.computer.uuid.equals(details.uuid)) { if (tuple.computer.uuid.equals(details.uuid)) {
if (manuallyAdded) { // Update the saved computer with potentially new details
// Update details anyway in case this machine has been re-added by IP tuple.computer.update(details);
// after not being reachable by our existing information
tuple.computer.localAddress = details.localAddress;
tuple.computer.remoteAddress = details.remoteAddress;
}
else {
// This indicates that mDNS discovered this address, so we
// should only apply the local address.
tuple.computer.localAddress = details.localAddress;
}
// Start a polling thread if polling is active // Start a polling thread if polling is active
if (pollingActive && tuple.thread == null) { if (pollingActive && tuple.thread == null) {
@@ -350,8 +338,20 @@ public class ComputerManagerService extends Service {
public boolean addComputerBlocking(String addr, boolean manuallyAdded) { public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
// Setup a placeholder // Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails(); ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localAddress = addr;
fakeDetails.remoteAddress = addr; if (manuallyAdded) {
// Add PC UI
fakeDetails.manualAddress = addr;
}
else {
// mDNS
fakeDetails.localAddress = addr;
// Since we're on the same network, we can use STUN to find
// our WAN address, which is also very likely the WAN address
// of the PC. We can use this later to connect remotely.
fakeDetails.remoteAddress = NvConnection.findExternalAddressForMdns();
}
// Block while we try to fill the details // Block while we try to fill the details
try { try {
@@ -438,6 +438,9 @@ public class ComputerManagerService extends Service {
return null; return null;
} }
// Set the new active address
newDetails.activeAddress = address;
return newDetails; return newDetails;
} catch (Exception e) { } catch (Exception e) {
return null; return null;
@@ -447,6 +450,11 @@ public class ComputerManagerService extends Service {
// Just try to establish a TCP connection to speculatively detect a running // Just try to establish a TCP connection to speculatively detect a running
// GFE server // GFE server
private boolean fastPollIp(String address) { private boolean fastPollIp(String address) {
if (address == null) {
// Don't bother if our address is null
return false;
}
Socket s = new Socket(); Socket s = new Socket();
try { try {
s.connect(new InetSocketAddress(address, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT); s.connect(new InetSocketAddress(address, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
@@ -475,12 +483,14 @@ public class ComputerManagerService extends Service {
t.start(); t.start();
} }
private ComputerDetails.Reachability fastPollPc(final String localAddress, final String remoteAddress) throws InterruptedException { private String fastPollPc(final String localAddress, final String remoteAddress, final String manualAddress) throws InterruptedException {
final boolean[] remoteInfo = new boolean[2]; final boolean[] remoteInfo = new boolean[2];
final boolean[] localInfo = new boolean[2]; final boolean[] localInfo = new boolean[2];
final boolean[] manualInfo = new boolean[2];
startFastPollThread(localAddress, localInfo); startFastPollThread(localAddress, localInfo);
startFastPollThread(remoteAddress, remoteInfo); startFastPollThread(remoteAddress, remoteInfo);
startFastPollThread(manualAddress, manualInfo);
// Check local first // Check local first
synchronized (localInfo) { synchronized (localInfo) {
@@ -489,174 +499,78 @@ public class ComputerManagerService extends Service {
} }
if (localInfo[1]) { if (localInfo[1]) {
return ComputerDetails.Reachability.LOCAL; return localAddress;
} }
} }
// Now remote // Now manual
synchronized (manualInfo) {
while (!manualInfo[0]) {
manualInfo.wait(500);
}
if (manualInfo[1]) {
return manualAddress;
}
}
// And finally, remote
synchronized (remoteInfo) { synchronized (remoteInfo) {
while (!remoteInfo[0]) { while (!remoteInfo[0]) {
remoteInfo.wait(500); remoteInfo.wait(500);
} }
if (remoteInfo[1]) { if (remoteInfo[1]) {
return ComputerDetails.Reachability.REMOTE; return remoteAddress;
} }
} }
return ComputerDetails.Reachability.OFFLINE; return null;
}
private static boolean isAddressLikelyLocal(String str) {
try {
// This will tend to be wrong for IPv6 but falling back to
// remote will be fine in that case. For IPv4, it should be
// pretty accurate due to NAT prevalence.
InetAddress addr = InetAddress.getByName(str);
return addr.isSiteLocalAddress() || addr.isLinkLocalAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
return false;
}
}
private ReachabilityTuple pollForReachability(ComputerDetails details) throws InterruptedException {
ComputerDetails polledDetails;
ComputerDetails.Reachability reachability;
if (details.localAddress.equals(details.remoteAddress)) {
reachability = isAddressLikelyLocal(details.localAddress) ?
ComputerDetails.Reachability.LOCAL : 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.localAddress +", "+details.remoteAddress +")");
reachability = fastPollPc(details.localAddress, details.remoteAddress);
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 null;
}
}
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
if (localFirst) {
polledDetails = tryPollIp(details, details.localAddress);
}
else {
polledDetails = tryPollIp(details, details.remoteAddress);
}
String reachableAddr = null;
if (polledDetails == null && !details.localAddress.equals(details.remoteAddress)) {
// Failed, so let's try the fallback
if (!localFirst) {
polledDetails = tryPollIp(details, details.localAddress);
}
else {
polledDetails = tryPollIp(details, details.remoteAddress);
}
if (polledDetails != null) {
// The fallback poll worked
reachableAddr = !localFirst ? details.localAddress : details.remoteAddress;
}
}
else if (polledDetails != null) {
reachableAddr = localFirst ? details.localAddress : details.remoteAddress;
}
if (reachableAddr == null) {
return null;
}
// If both addresses are the same, guess whether we're local based on
// IP address heuristics.
if (reachableAddr.equals(polledDetails.localAddress) &&
reachableAddr.equals(polledDetails.remoteAddress)) {
polledDetails.reachability = isAddressLikelyLocal(reachableAddr) ?
ComputerDetails.Reachability.LOCAL : ComputerDetails.Reachability.REMOTE;
}
else if (polledDetails.remoteAddress.equals(reachableAddr)) {
polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
}
else if (polledDetails.localAddress.equals(reachableAddr)) {
polledDetails.reachability = ComputerDetails.Reachability.LOCAL;
}
else {
polledDetails.reachability = ComputerDetails.Reachability.UNKNOWN;
}
return new ReachabilityTuple(polledDetails, reachableAddr);
} }
private boolean pollComputer(ComputerDetails details) throws InterruptedException { private boolean pollComputer(ComputerDetails details) throws InterruptedException {
ReachabilityTuple initialReachTuple = pollForReachability(details); ComputerDetails polledDetails;
if (initialReachTuple == null) {
// Do a TCP-level connection to the HTTP server to see if it's listening.
// Do not write this address to details.activeAddress because:
// a) it's only a candidate and may be wrong (multiple PCs behind a single router)
// b) if it's null, it will be unexpectedly nulling the activeAddress of a possibly online PC
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +", "+details.manualAddress +")");
String candidateAddress = fastPollPc(details.localAddress, details.remoteAddress, details.manualAddress);
LimeLog.info("Fast poll for "+details.name+" returned active address: "+details.activeAddress);
// If no connection could be established to either IP address, there's nothing we can do
if (candidateAddress == null) {
return false; return false;
} }
if (initialReachTuple.computer.reachability == ComputerDetails.Reachability.UNKNOWN) { // Try using the active address from fast-poll
// Neither IP address reported in the serverinfo response was the one we used. polledDetails = tryPollIp(details, candidateAddress);
// Poll again to see if we can contact this machine on either of its reported addresses. if (polledDetails == null) {
ReachabilityTuple confirmationReachTuple = pollForReachability(initialReachTuple.computer); // If that failed, try all unique addresses except what we've
if (confirmationReachTuple == null) { // already tried
// Neither of those seem to work, so we'll hold onto the address that did work HashSet<String> uniqueAddresses = new HashSet<>();
initialReachTuple.computer.localAddress = initialReachTuple.reachableAddress; uniqueAddresses.add(details.localAddress);
initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL; uniqueAddresses.add(details.remoteAddress);
} uniqueAddresses.add(details.manualAddress);
else { for (String addr : uniqueAddresses) {
// We got it on one of the returned addresses; replace the original reach tuple if (addr == null || addr.equals(candidateAddress)) {
// with the new one continue;
initialReachTuple = confirmationReachTuple; }
polledDetails = tryPollIp(details, addr);
if (polledDetails != null) {
break;
}
} }
} }
// Save some details about the old state of the PC that we may wish if (polledDetails != null) {
// to restore later. details.update(polledDetails);
String savedMacAddress = details.macAddress; return true;
String savedLocalAddress = details.localAddress;
String savedRemoteAddress = details.remoteAddress;
// If we got here, it's reachable
details.update(initialReachTuple.computer);
// If the new MAC address is empty, restore the old one (workaround for GFE bug)
if (details.macAddress.equals("00:00:00:00:00:00") && savedMacAddress != null) {
LimeLog.info("MAC address was empty; using existing value: "+savedMacAddress);
details.macAddress = savedMacAddress;
} }
else {
// We never want to lose IP addresses by polling server info. If we get a poll back return false;
// where localAddress == remoteAddress but savedLocalAddress != savedRemoteAddress,
// then we've lost an address in the polling and we should restore the one that's missing.
if (details.localAddress.equals(details.remoteAddress) &&
!savedLocalAddress.equals(savedRemoteAddress)) {
if (details.localAddress.equals(savedLocalAddress)) {
// Local addresses are identical, so put the old remote address back
details.remoteAddress = savedRemoteAddress;
}
else if (details.remoteAddress.equals(savedRemoteAddress)) {
// Remote addresses are identical, so put the old local address back
details.localAddress = savedLocalAddress;
}
else {
// Neither IP address match. Let's restore the remote address to be safe.
details.remoteAddress = savedRemoteAddress;
}
// Now update the reachability so the correct address is used
if (details.localAddress.equals(initialReachTuple.reachableAddress)) {
details.reachability = ComputerDetails.Reachability.LOCAL;
}
else {
details.reachability = ComputerDetails.Reachability.REMOTE;
}
} }
return true;
} }
@Override @Override
@@ -841,7 +755,7 @@ public class ComputerManagerService extends Service {
} while (waitPollingDelay()); } while (waitPollingDelay());
} }
}; };
thread.setName("App list polling thread for " + computer.localAddress); thread.setName("App list polling thread for " + computer.name);
thread.start(); thread.start();
} }
@@ -0,0 +1,113 @@
package com.limelight.computers;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import com.limelight.LimeLog;
import com.limelight.nvstream.http.ComputerDetails;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
public class LegacyDatabaseReader {
private static final String COMPUTER_DB_NAME = "computers.db";
private static final String COMPUTER_TABLE_NAME = "Computers";
private static final String ADDRESS_PREFIX = "ADDRESS_PREFIX__";
private static ComputerDetails getComputerFromCursor(Cursor c) {
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);
}
// An earlier schema defined addresses as byte blobs. We'll
// gracefully migrate those to strings so we can store DNS names
// too. To disambiguate, we'll need to prefix them with a string
// greater than the allowable IP address length.
try {
details.localAddress = InetAddress.getByAddress(c.getBlob(2)).getHostAddress();
LimeLog.warning("DB: Legacy local address for " + details.name);
} catch (UnknownHostException e) {
// This is probably a hostname/address with the prefix string
String stringData = c.getString(2);
if (stringData.startsWith(ADDRESS_PREFIX)) {
details.localAddress = c.getString(2).substring(ADDRESS_PREFIX.length());
} else {
LimeLog.severe("DB: Corrupted local address for " + details.name);
}
}
try {
details.remoteAddress = InetAddress.getByAddress(c.getBlob(3)).getHostAddress();
LimeLog.warning("DB: Legacy remote address for " + details.name);
} catch (UnknownHostException e) {
// This is probably a hostname/address with the prefix string
String stringData = c.getString(3);
if (stringData.startsWith(ADDRESS_PREFIX)) {
details.remoteAddress = c.getString(3).substring(ADDRESS_PREFIX.length());
} else {
LimeLog.severe("DB: Corrupted remote address for " + details.name);
}
}
// On older versions of Moonlight, this is typically where manual addresses got stored,
// so let's initialize it just to be safe.
details.manualAddress = details.remoteAddress;
details.macAddress = c.getString(4);
// This signifies we don't have dynamic state (like pair state)
details.state = ComputerDetails.State.UNKNOWN;
return details;
}
private static List<ComputerDetails> getAllComputers(SQLiteDatabase db) {
Cursor c = db.rawQuery("SELECT * FROM " + COMPUTER_TABLE_NAME, null);
LinkedList<ComputerDetails> computerList = new LinkedList<>();
while (c.moveToNext()) {
ComputerDetails details = getComputerFromCursor(c);
// If a critical field is corrupt or missing, skip the database entry
if (details.uuid == null) {
continue;
}
computerList.add(details);
}
c.close();
return computerList;
}
public static List<ComputerDetails> migrateAllComputers(Context c) {
SQLiteDatabase computerDb = null;
try {
// Open the existing database
computerDb = SQLiteDatabase.openDatabase(c.getDatabasePath(COMPUTER_DB_NAME).getPath(), null, SQLiteDatabase.OPEN_READONLY);
return getAllComputers(computerDb);
} catch (SQLiteException e) {
return new LinkedList<ComputerDetails>();
} finally {
// Close and delete the old DB
if (computerDb != null) {
computerDb.close();
}
c.deleteDatabase(COMPUTER_DB_NAME);
}
}
}
@@ -1,7 +1,6 @@
package com.limelight.grid; package com.limelight.grid;
import android.app.Activity; import android.app.Activity;
import android.graphics.BitmapFactory;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
@@ -46,13 +45,10 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
} }
LimeLog.info("Art scaling divisor: " + scalingDivisor); LimeLog.info("Art scaling divisor: " + scalingDivisor);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = (int) scalingDivisor;
this.loader = new CachedAppAssetLoader(computer, scalingDivisor, this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
new NetworkAssetLoader(context, uniqueId), new NetworkAssetLoader(context, uniqueId),
new MemoryAssetLoader(), new MemoryAssetLoader(),
new DiskAssetLoader(context.getCacheDir())); new DiskAssetLoader(context));
} }
public void cancelQueuedOperations() { public void cancelQueuedOperations() {
@@ -46,7 +46,7 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
imgView.setAlpha(0.4f); imgView.setAlpha(0.4f);
} }
if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) { if (obj.details.state == ComputerDetails.State.UNKNOWN) {
prgView.setVisibility(View.VISIBLE); prgView.setVisibility(View.VISIBLE);
} }
else { else {
@@ -1,7 +1,11 @@
package com.limelight.grid.assets; package com.limelight.grid.assets;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.ImageDecoder;
import android.os.Build;
import com.limelight.LimeLog; import com.limelight.LimeLog;
import com.limelight.utils.CacheHelper; import com.limelight.utils.CacheHelper;
@@ -19,10 +23,19 @@ public class DiskAssetLoader {
private static final int STANDARD_ASSET_WIDTH = 300; private static final int STANDARD_ASSET_WIDTH = 300;
private static final int STANDARD_ASSET_HEIGHT = 400; private static final int STANDARD_ASSET_HEIGHT = 400;
private final boolean isLowRamDevice;
private final File cacheDir; private final File cacheDir;
public DiskAssetLoader(File cacheDir) { public DiskAssetLoader(Context context) {
this.cacheDir = cacheDir; this.cacheDir = context.getCacheDir();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
this.isLowRamDevice =
((ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)).isLowRamDevice();
}
else {
// Use conservative low RAM behavior on very old devices
this.isLowRamDevice = true;
}
} }
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) { public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
@@ -66,28 +79,55 @@ public class DiskAssetLoader {
return null; return null;
} }
// Lookup bounds of the downloaded image Bitmap bmp;
BitmapFactory.Options decodeOnlyOptions = new BitmapFactory.Options();
decodeOnlyOptions.inJustDecodeBounds = true; // For OSes prior to P, we have to use the ugly BitmapFactory API
BitmapFactory.decodeFile(file.getAbsolutePath(), decodeOnlyOptions); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
if (decodeOnlyOptions.outWidth <= 0 || decodeOnlyOptions.outHeight <= 0) { // Lookup bounds of the downloaded image
// Dimensions set to -1 on error. Return value always null. BitmapFactory.Options decodeOnlyOptions = new BitmapFactory.Options();
return null; decodeOnlyOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), decodeOnlyOptions);
if (decodeOnlyOptions.outWidth <= 0 || decodeOnlyOptions.outHeight <= 0) {
// Dimensions set to -1 on error. Return value always null.
return null;
}
LimeLog.info("Tuple "+tuple+" has cached art of size: "+decodeOnlyOptions.outWidth+"x"+decodeOnlyOptions.outHeight);
// Load the image scaled to the appropriate size
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calculateInSampleSize(decodeOnlyOptions,
STANDARD_ASSET_WIDTH / sampleSize,
STANDARD_ASSET_HEIGHT / sampleSize);
if (isLowRamDevice) {
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inDither = true;
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
options.inPreferredConfig = Bitmap.Config.HARDWARE;
}
bmp = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
if (bmp != null) {
LimeLog.info("Tuple "+tuple+" decoded from disk cache with sample size: "+options.inSampleSize);
}
} }
else {
LimeLog.info("Tuple "+tuple+" has cached art of size: "+decodeOnlyOptions.outWidth+"x"+decodeOnlyOptions.outHeight); // On P, we can get a bitmap back in one step with ImageDecoder
try {
// Load the image scaled to the appropriate size bmp = ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), new ImageDecoder.OnHeaderDecodedListener() {
BitmapFactory.Options options = new BitmapFactory.Options(); @Override
options.inSampleSize = calculateInSampleSize(decodeOnlyOptions, public void onHeaderDecoded(ImageDecoder imageDecoder, ImageDecoder.ImageInfo imageInfo, ImageDecoder.Source source) {
STANDARD_ASSET_WIDTH / sampleSize, imageDecoder.setTargetSize(STANDARD_ASSET_WIDTH, STANDARD_ASSET_HEIGHT);
STANDARD_ASSET_HEIGHT / sampleSize); if (isLowRamDevice) {
options.inPreferredConfig = Bitmap.Config.RGB_565; imageDecoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
options.inDither = true; }
Bitmap bmp = BitmapFactory.decodeFile(file.getAbsolutePath(), options); }
});
if (bmp != null) { } catch (IOException e) {
LimeLog.info("Tuple "+tuple+" decoded from disk cache with sample size: "+options.inSampleSize); e.printStackTrace();
return null;
}
} }
return bmp; return bmp;
@@ -24,6 +24,7 @@ import android.content.ServiceConnection;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import android.widget.TextView; import android.widget.TextView;
@@ -196,12 +197,7 @@ public class AddComputerManually extends Activity {
(keyEvent != null && (keyEvent != null &&
keyEvent.getAction() == KeyEvent.ACTION_DOWN && keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) { keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
if (hostText.getText().length() == 0) { return handleDoneEvent();
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
return true;
}
computersToAdd.add(hostText.getText().toString().trim());
} }
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) { else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
// This is how the Fire TV dismisses the keyboard // This is how the Fire TV dismisses the keyboard
@@ -214,8 +210,28 @@ public class AddComputerManually extends Activity {
} }
}); });
findViewById(R.id.addPcButton).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
handleDoneEvent();
}
});
// Bind to the ComputerManager service // Bind to the ComputerManager service
bindService(new Intent(AddComputerManually.this, bindService(new Intent(AddComputerManually.this,
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE); ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
} }
// Returns true if the event should be eaten
private boolean handleDoneEvent() {
String hostAddress = hostText.getText().toString().trim();
if (hostAddress.length() == 0) {
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
return true;
}
computersToAdd.add(hostAddress);
return false;
}
} }
@@ -62,9 +62,8 @@ public class StreamView extends SurfaceView {
@Override @Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) { public boolean onKeyPreIme(int keyCode, KeyEvent event) {
// This callbacks allows us to override dumb IME behavior like when // This callbacks allows us to override dumb IME behavior like when
// Samsung's default keyboard consumes Shift+Space. We'll process // Samsung's default keyboard consumes Shift+Space.
// the input event directly if any modifier keys are down. if (inputCallbacks != null) {
if (inputCallbacks != null && event.getModifiers() != 0) {
if (event.getAction() == KeyEvent.ACTION_DOWN) { if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (inputCallbacks.handleKeyDown(event)) { if (inputCallbacks.handleKeyDown(event)) {
return true; return true;
@@ -18,8 +18,7 @@ import java.net.UnknownHostException;
public class ServerHelper { public class ServerHelper {
public static String getCurrentAddressFromComputer(ComputerDetails computer) { public static String getCurrentAddressFromComputer(ComputerDetails computer) {
return computer.reachability == ComputerDetails.Reachability.LOCAL ? return computer.activeAddress;
computer.localAddress : computer.remoteAddress;
} }
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer, public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
@@ -30,8 +29,7 @@ public class ServerHelper {
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId()); intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
intent.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported()); intent.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported());
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId()); intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
intent.putExtra(Game.EXTRA_STREAMING_REMOTE, intent.putExtra(Game.EXTRA_STREAMING_REMOTE, getCurrentAddressFromComputer(computer).equals(computer.remoteAddress));
computer.reachability != ComputerDetails.Reachability.LOCAL);
intent.putExtra(Game.EXTRA_PC_UUID, computer.uuid.toString()); intent.putExtra(Game.EXTRA_PC_UUID, computer.uuid.toString());
intent.putExtra(Game.EXTRA_PC_NAME, computer.name); intent.putExtra(Game.EXTRA_PC_NAME, computer.name);
return intent; return intent;
@@ -5,13 +5,15 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager; import android.content.pm.ShortcutManager;
import android.graphics.Bitmap;
import android.graphics.drawable.Icon; import android.graphics.drawable.Icon;
import android.os.Build; import android.os.Build;
import com.limelight.AppView; import com.limelight.AppView;
import com.limelight.AppViewShortcutTrampoline; import com.limelight.ShortcutTrampoline;
import com.limelight.R; import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
@@ -80,8 +82,7 @@ public class ShortcutHelper {
public void reportShortcutUsed(String id) { public void reportShortcutUsed(String id) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
ShortcutInfo sinfo = getInfoForId(id); if (getInfoForId(id) != null) {
if (sinfo != null) {
sm.reportShortcutUsed(id); sm.reportShortcutUsed(id);
} }
} }
@@ -89,7 +90,7 @@ public class ShortcutHelper {
public void createAppViewShortcut(String id, String computerName, String computerUuid, boolean forceAdd) { public void createAppViewShortcut(String id, String computerName, String computerUuid, boolean forceAdd) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
Intent i = new Intent(context, AppViewShortcutTrampoline.class); Intent i = new Intent(context, ShortcutTrampoline.class);
i.putExtra(AppView.NAME_EXTRA, computerName); i.putExtra(AppView.NAME_EXTRA, computerName);
i.putExtra(AppView.UUID_EXTRA, computerUuid); i.putExtra(AppView.UUID_EXTRA, computerUuid);
i.setAction(Intent.ACTION_DEFAULT); i.setAction(Intent.ACTION_DEFAULT);
@@ -127,10 +128,42 @@ public class ShortcutHelper {
createAppViewShortcut(id, details.name, details.uuid.toString(), forceAdd); createAppViewShortcut(id, details.name, details.uuid.toString(), forceAdd);
} }
@TargetApi(Build.VERSION_CODES.O)
public boolean createPinnedGameShortcut(String id, Bitmap iconBits, String computerName, String computerUuid, String appName, String appId) {
if (sm.isRequestPinShortcutSupported()) {
Icon appIcon;
Intent i = new Intent(context, ShortcutTrampoline.class);
i.putExtra(AppView.NAME_EXTRA, computerName);
i.putExtra(AppView.UUID_EXTRA, computerUuid);
i.putExtra(ShortcutTrampoline.APP_ID_EXTRA, appId);
i.setAction(Intent.ACTION_DEFAULT);
if (iconBits != null) {
appIcon = Icon.createWithAdaptiveBitmap(iconBits);
} else {
appIcon = Icon.createWithResource(context, R.mipmap.ic_pc_scut);
}
ShortcutInfo sInfo = new ShortcutInfo.Builder(context, id)
.setIntent(i)
.setShortLabel(appName + " (" + computerName + ")")
.setIcon(appIcon)
.build();
return sm.requestPinShortcut(sInfo, null);
} else {
return false;
}
}
public boolean createPinnedGameShortcut(String id, Bitmap iconBits, ComputerDetails cDetails, NvApp app) {
return createPinnedGameShortcut(id, iconBits, cDetails.name, cDetails.uuid.toString(), app.getAppName(), Integer.valueOf(app.getAppId()).toString());
}
public void disableShortcut(String id, CharSequence reason) { public void disableShortcut(String id, CharSequence reason) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
ShortcutInfo sinfo = getInfoForId(id); if (getInfoForId(id) != null) {
if (sinfo != null) {
sm.disableShortcuts(Collections.singletonList(id), reason); sm.disableShortcuts(Collections.singletonList(id), reason);
} }
} }
@@ -7,9 +7,12 @@ import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Build;
import android.view.View; import android.view.View;
import android.view.WindowManager;
import com.limelight.R; import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.preferences.PreferenceConfiguration; import com.limelight.preferences.PreferenceConfiguration;
import java.util.Locale; import java.util.Locale;
@@ -57,6 +60,16 @@ public class UiHelper {
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels, rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
horizontalPaddingPixels, verticalPaddingPixels); horizontalPaddingPixels, verticalPaddingPixels);
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// Allow this non-streaming activity to layout under notches.
//
// We should NOT do this for the Game activity unless
// the user specifically opts in, because it can obscure
// parts of the streaming surface.
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
}
} }
public static void showDecoderCrashDialog(Activity activity) { public static void showDecoderCrashDialog(Activity activity) {
@@ -123,4 +136,32 @@ public class UiHelper {
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener) .setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
.show(); .show();
} }
public static void displayDeletePcConfirmationDialog(Activity parent, ComputerDetails computer, 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.delete_pc_msg))
.setTitle(computer.name)
.setPositiveButton(parent.getResources().getString(R.string.yes), dialogClickListener)
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
.show();
}
} }
@@ -22,11 +22,11 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/manuallyAddPcText" android:layout_below="@+id/manuallyAddPcText"
android:layout_toLeftOf="@+id/addPcButton"
android:layout_toStartOf="@+id/addPcButton"
android:layout_marginTop="25dp" android:layout_marginTop="25dp"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:ems="10" android:ems="10"
android:singleLine="true" android:singleLine="true"
android:inputType="textNoSuggestions" android:inputType="textNoSuggestions"
@@ -34,5 +34,15 @@
<requestFocus /> <requestFocus />
</EditText> </EditText>
<Button
android:id="@+id/addPcButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="25dp"
android:layout_below="@+id/manuallyAddPcText"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:text="@android:string/ok"/>
</RelativeLayout> </RelativeLayout>
-1
View File
@@ -61,7 +61,6 @@
<string name="lost_connection">Conexión perdida</string> <string name="lost_connection">Conexión perdida</string>
<!-- AppList activity --> <!-- AppList activity -->
<string name="title_applist">Aplicaciones en</string>
<string name="applist_menu_resume">Reanudar sesión</string> <string name="applist_menu_resume">Reanudar sesión</string>
<string name="applist_menu_quit">Cerrar sesión</string> <string name="applist_menu_quit">Cerrar sesión</string>
<string name="applist_menu_quit_and_start">Cerrar juego actual e iniciar</string> <string name="applist_menu_quit_and_start">Cerrar juego actual e iniciar</string>
+31 -3
View File
@@ -3,6 +3,9 @@
<!-- Shortcut strings --> <!-- Shortcut strings -->
<string name="scut_deleted_pc">PC supprimé</string> <string name="scut_deleted_pc">PC supprimé</string>
<string name="scut_not_paired">PC non appairé</string> <string name="scut_not_paired">PC non appairé</string>
<string name="scut_pc_not_found">PC non trouvé</string>
<string name="scut_invalid_uuid">Le PC fourni n\'est pas valide</string>
<string name="scut_invalid_app_id">L\'application fournie n\'est pas valide</string>
<!-- Help strings --> <!-- Help strings -->
<string name="help_loading_title">Visionneuse d\'aide</string> <string name="help_loading_title">Visionneuse d\'aide</string>
@@ -14,6 +17,7 @@
<string name="pcview_menu_unpair_pc">Désappairer</string> <string name="pcview_menu_unpair_pc">Désappairer</string>
<string name="pcview_menu_send_wol">Envoyer la requête Wake-On-LAN</string> <string name="pcview_menu_send_wol">Envoyer la requête Wake-On-LAN</string>
<string name="pcview_menu_delete_pc">Supprimer PC</string> <string name="pcview_menu_delete_pc">Supprimer PC</string>
<string name="pcview_menu_details">Voir les détails</string>
<!-- Pair messages --> <!-- Pair messages -->
<string name="pairing">Appariement…</string> <string name="pairing">Appariement…</string>
@@ -47,6 +51,12 @@
<string name="error_404">GFE renvoi une erreur HTTP 404. Assurez-vous que votre PC exécute un GPU pris en charge. <string name="error_404">GFE renvoi une erreur HTTP 404. Assurez-vous que votre PC exécute un GPU pris en charge.
L\'utilisation d\'un logiciel de bureau à distance peut également provoquer cette erreur. Essayez de redémarrer votre machine ou de réinstaller GFE. L\'utilisation d\'un logiciel de bureau à distance peut également provoquer cette erreur. Essayez de redémarrer votre machine ou de réinstaller GFE.
</string> </string>
<string name="title_decoding_error">Le décodeur vidéo s\'est écrasé</string>
<string name="message_decoding_error">Moonlight s\'est écrasé en raison d\'une incompatibilité avec le décodeur vidéo de cet appareil. Assurez-vous que GeForce Experience soit mis à jour vers la dernière version sur votre PC. Essayez de régler les paramètres de diffusion si les plantages continuent.</string>
<string name="title_decoding_reset">Paramètres vidéo réinitialiser</string>
<string name="message_decoding_reset">Le décodeur vidéo de votre appareil continue de planter avec les paramètres de diffusion sélectionnés. Vos paramètres de diffusion ont été réinitialisés par défaut.</string>
<string name="error_usb_prohibited">L\'accès USB est interdit par votre appareil. Vérifiez vos paramètres Knox ou MDM.</string>
<string name="unable_to_pin_shortcut">Votre lanceur actuel ne permet pas de créer des raccourcis épinglés.</string>
<!-- Start application messages --> <!-- Start application messages -->
<string name="conn_establishing_title">Établissement de la connexion</string> <string name="conn_establishing_title">Établissement de la connexion</string>
@@ -63,20 +73,22 @@
<!-- General strings --> <!-- General strings -->
<string name="ip_hint">Adresse IP de GeForce PC</string> <string name="ip_hint">Adresse IP de GeForce PC</string>
<string name="searching_pc">Recherche de PC avec GameStream en cours...\n\n <string name="searching_pc">Recherche de PC avec GameStream en cours…\n\n
Assurez-vous que GameStream est activé dans les paramètres GeForce Experience SHIELD.</string> Assurez-vous que GameStream est activé dans les paramètres GeForce Experience SHIELD.</string>
<string name="yes">Oui</string> <string name="yes">Oui</string>
<string name="no">Non</string> <string name="no">Non</string>
<string name="lost_connection">Perte de connexion avec le PC</string> <string name="lost_connection">Perte de connexion avec le PC</string>
<string name="title_details">Détails</string>
<string name="help">Aide</string> <string name="help">Aide</string>
<!-- AppList activity --> <!-- AppList activity -->
<string name="title_applist">Applications sur</string>
<string name="applist_connect_msg">Connexion au PC…</string> <string name="applist_connect_msg">Connexion au PC…</string>
<string name="applist_menu_resume">Reprise de la session</string> <string name="applist_menu_resume">Reprise de la session</string>
<string name="applist_menu_quit">Quitter la session</string> <string name="applist_menu_quit">Quitter la session</string>
<string name="applist_menu_quit_and_start">Quitter le jeu actuel et démarrer</string> <string name="applist_menu_quit_and_start">Quitter le jeu actuel et démarrer</string>
<string name="applist_menu_cancel">Annuler</string> <string name="applist_menu_cancel">Annuler</string>
<string name="applist_menu_details">Voir les détails</string>
<string name="applist_menu_scut">Créer un raccourci</string>
<string name="applist_refresh_title">Liste des applications</string> <string name="applist_refresh_title">Liste des applications</string>
<string name="applist_refresh_msg">Actualisation des applications…</string> <string name="applist_refresh_msg">Actualisation des applications…</string>
<string name="applist_refresh_error_title">Erreur</string> <string name="applist_refresh_error_title">Erreur</string>
@@ -85,6 +97,7 @@
<string name="applist_quit_success">Fermeture avec succès</string> <string name="applist_quit_success">Fermeture avec succès</string>
<string name="applist_quit_fail">Échec de la fermeture</string> <string name="applist_quit_fail">Échec de la fermeture</string>
<string name="applist_quit_confirmation">Voulez-vous vraiment quitter l\'application en cours d\'exécution? Toutes les données non enregistrées seront perdues.</string> <string name="applist_quit_confirmation">Voulez-vous vraiment quitter l\'application en cours d\'exécution? Toutes les données non enregistrées seront perdues.</string>
<string name="applist_details_id">ID app:</string>
<!-- Add computer manually activity --> <!-- Add computer manually activity -->
<string name="title_add_pc">Ajouter un PC manuellement</string> <string name="title_add_pc">Ajouter un PC manuellement</string>
@@ -93,6 +106,7 @@
<string name="addpc_success">Ajouté avec succès de l\'ordinateur</string> <string name="addpc_success">Ajouté avec succès de l\'ordinateur</string>
<string name="addpc_unknown_host">Impossible de résoudre l\'adresse du PC. Assurez-vous que vous n\'avez pas fait une faute de frappe dans l\'adresse.</string> <string name="addpc_unknown_host">Impossible de résoudre l\'adresse du PC. Assurez-vous que vous n\'avez pas fait une faute de frappe dans l\'adresse.</string>
<string name="addpc_enter_ip">Vous devez entrer une adresse IP</string> <string name="addpc_enter_ip">Vous devez entrer une adresse IP</string>
<string name="addpc_wrong_sitelocal">Cette adresse ne semble pas correcte. Vous devez utiliser l\'adresse IP publique de votre routeur pour la diffusion en continu sur Internet..</string>
<!-- Preferences --> <!-- Preferences -->
<string name="category_basic_settings">Paramètres de base</string> <string name="category_basic_settings">Paramètres de base</string>
@@ -104,6 +118,8 @@
<string name="title_checkbox_stretch_video">Étirez la vidéo en plein écran</string> <string name="title_checkbox_stretch_video">Étirez la vidéo en plein écran</string>
<string name="title_checkbox_disable_warnings">Désactiver les messages d\'avertissement</string> <string name="title_checkbox_disable_warnings">Désactiver les messages d\'avertissement</string>
<string name="summary_checkbox_disable_warnings">Désactiver les messages d\'avertissement de connexion à l\'écran pendant le streaming</string> <string name="summary_checkbox_disable_warnings">Désactiver les messages d\'avertissement de connexion à l\'écran pendant le streaming</string>
<string name="title_checkbox_enable_pip">Activer le mode observateur dans l\'image</string>
<string name="summary_checkbox_enable_pip">Permet de visualiser le flux (sans le contrôleur) tout en multitâche</string>
<string name="category_audio_settings">Paramètres audio</string> <string name="category_audio_settings">Paramètres audio</string>
<string name="title_checkbox_51_surround">Activer son surround 5.1</string> <string name="title_checkbox_51_surround">Activer son surround 5.1</string>
@@ -116,12 +132,21 @@
<string name="suffix_seekbar_deadzone">%</string> <string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Pilote de contrôleur Xbox 360/One</string> <string name="title_checkbox_xb1_driver">Pilote de contrôleur Xbox 360/One</string>
<string name="summary_checkbox_xb1_driver">Active un pilote USB intégré pour les périphériques sans prise en charge du contrôleur Xbox natif.</string> <string name="summary_checkbox_xb1_driver">Active un pilote USB intégré pour les périphériques sans prise en charge du contrôleur Xbox natif.</string>
<string name="title_checkbox_usb_bind_all">Ignorer le support du contrôleur Android</string>
<string name="summary_checkbox_usb_bind_all">Force le pilote USB de Moonlight à prendre en charge tous les gamepads Xbox pris en charge</string>
<string name="title_checkbox_mouse_emulation">Emulation de la souris via le gamepad</string>
<string name="summary_checkbox_mouse_emulation">Appuyez longuement sur le bouton Start pour faire basculer la manette de jeu en mode souris.</string>
<string name="category_on_screen_controls_settings">Paramètres des contrôles à l\'écran</string> <string name="category_on_screen_controls_settings">Paramètres des contrôles à l\'écran</string>
<string name="title_checkbox_show_onscreen_controls">Afficher les commandes à l\'écran</string> <string name="title_checkbox_show_onscreen_controls">Afficher les commandes à l\'écran</string>
<string name="summary_checkbox_show_onscreen_controls">Afficher la superposition du contrôleur virtuel sur l\'écran tactile</string> <string name="summary_checkbox_show_onscreen_controls">Afficher la superposition du contrôleur virtuel sur l\'écran tactile</string>
<string name="title_only_l3r3">Montre seulement L3 et R3</string> <string name="title_only_l3r3">Montre seulement L3 et R3</string>
<string name="summary_only_l3r3">Cacher tout sauf L3 et R3</string> <string name="summary_only_l3r3">Cacher tout sauf L3 et R3</string>
<string name="title_reset_osc">Effacer la disposition des commandes à l\'écran sauvegardée</string>
<string name="summary_reset_osc">Rétablit la taille et la position par défaut de tous les contrôles à l\'écran</string>
<string name="dialog_title_reset_osc">Réinitialiser la mise en page</string>
<string name="dialog_text_reset_osc">Êtes-vous sûr de vouloir supprimer la disposition des commandes à l\'écran que vous avez sauvegardée?</string>
<string name="toast_reset_osc_success">Les contrôles à l\'écran sont réinitialisés</string>
<string name="category_ui_settings">Paramètres de l\'interface utilisateur</string> <string name="category_ui_settings">Paramètres de l\'interface utilisateur</string>
<string name="title_language_list">Langue</string> <string name="title_language_list">Langue</string>
@@ -139,5 +164,8 @@
<string name="category_advanced_settings">Réglages avancés</string> <string name="category_advanced_settings">Réglages avancés</string>
<string name="title_video_format">Modifier les paramètres H.265</string> <string name="title_video_format">Modifier les paramètres H.265</string>
<string name="summary_video_format">H.265 réduit les exigences de bande passante vidéo, mais requiert un périphérique très récent.</string> <string name="summary_video_format">H.265 réduit les besoins en bande passante vidéo mais nécessite un périphérique très récent</string>
<string name="title_enable_hdr">Activer le HDR (expérimental)</string>
<string name="summary_enable_hdr">Diffuser du HDR lorsque le jeu et le processeur graphique du PC le prennent en charge. HDR nécessite un GPU série GTX 1000 ou une version ultérieure.</string>
</resources> </resources>
-1
View File
@@ -76,7 +76,6 @@
<string name="help">Assistenza</string> <string name="help">Assistenza</string>
<!-- AppList activity --> <!-- AppList activity -->
<string name="title_applist">Applicazioni su</string>
<string name="applist_connect_msg">Connessione al PC in corso…</string> <string name="applist_connect_msg">Connessione al PC in corso…</string>
<string name="applist_menu_resume">Riprendi sessione</string> <string name="applist_menu_resume">Riprendi sessione</string>
<string name="applist_menu_quit">Chiudi sessione</string> <string name="applist_menu_quit">Chiudi sessione</string>
-1
View File
@@ -57,7 +57,6 @@
<string name="lost_connection">コンピュータとの接続が失われました</string> <string name="lost_connection">コンピュータとの接続が失われました</string>
<!-- AppList activity --> <!-- AppList activity -->
<string name="title_applist">ゲーム</string>
<string name="applist_menu_resume">セッションを続ける</string> <string name="applist_menu_resume">セッションを続ける</string>
<string name="applist_menu_quit">セッションを終了する</string> <string name="applist_menu_quit">セッションを終了する</string>
<string name="applist_menu_quit_and_start">現在のゲームを終了して新しいゲームを始める</string> <string name="applist_menu_quit_and_start">現在のゲームを終了して新しいゲームを始める</string>
-1
View File
@@ -71,7 +71,6 @@
<string name="help">도움말</string> <string name="help">도움말</string>
<!-- AppList activity --> <!-- AppList activity -->
<string name="title_applist">앱 사용 가능</string>
<string name="applist_connect_msg">PC에 연결중…</string> <string name="applist_connect_msg">PC에 연결중…</string>
<string name="applist_menu_resume">세션 계속</string> <string name="applist_menu_resume">세션 계속</string>
<string name="applist_menu_quit">세션 종료</string> <string name="applist_menu_quit">세션 종료</string>
-1
View File
@@ -61,7 +61,6 @@
<string name="lost_connection">Verbinding met PC verloren</string> <string name="lost_connection">Verbinding met PC verloren</string>
<!-- AppList activity --> <!-- AppList activity -->
<string name="title_applist">Apps op</string>
<string name="applist_menu_resume">Hervat Sessie</string> <string name="applist_menu_resume">Hervat Sessie</string>
<string name="applist_menu_quit">Stop Sessie</string> <string name="applist_menu_quit">Stop Sessie</string>
<string name="applist_menu_quit_and_start">Stop Huidige Spel en Start</string> <string name="applist_menu_quit_and_start">Stop Huidige Spel en Start</string>
-1
View File
@@ -62,7 +62,6 @@
<string name="lost_connection">Потеряно соединение с PC</string> <string name="lost_connection">Потеряно соединение с PC</string>
<!-- AppList activity --> <!-- AppList activity -->
<string name="title_applist">Приложения на</string>
<string name="applist_menu_resume">Возобновить сессию</string> <string name="applist_menu_resume">Возобновить сессию</string>
<string name="applist_menu_quit">Выйти из сессии</string> <string name="applist_menu_quit">Выйти из сессии</string>
<string name="applist_menu_quit_and_start">Выйти из текущей игры и запустить</string> <string name="applist_menu_quit_and_start">Выйти из текущей игры и запустить</string>
+2
View File
@@ -7,6 +7,8 @@
--> -->
<style name="AppBaseTheme" parent="android:Theme.Material"> <style name="AppBaseTheme" parent="android:Theme.Material">
<!-- API 21 theme customizations can go here. --> <!-- API 21 theme customizations can go here. -->
<item name="android:statusBarColor">#212121</item>
<item name="android:navigationBarColor">#212121</item>
</style> </style>
</resources> </resources>
@@ -71,7 +71,6 @@
<string name="help">帮助</string> <string name="help">帮助</string>
<!-- AppList activity --> <!-- AppList activity -->
<string name="title_applist">Apps on</string>
<string name="applist_connect_msg"> 连接到电脑中…… </string> <string name="applist_connect_msg"> 连接到电脑中…… </string>
<string name="applist_menu_resume"> 恢复串流 </string> <string name="applist_menu_resume"> 恢复串流 </string>
<string name="applist_menu_quit"> 退出串流 </string> <string name="applist_menu_quit"> 退出串流 </string>
@@ -71,7 +71,6 @@
<string name="help">幫助</string> <string name="help">幫助</string>
<!-- AppList activity --> <!-- AppList activity -->
<string name="title_applist">Apps on</string>
<string name="applist_connect_msg"> 連接到電腦中…… </string> <string name="applist_connect_msg"> 連接到電腦中…… </string>
<string name="applist_menu_resume"> 恢復串流 </string> <string name="applist_menu_resume"> 恢復串流 </string>
<string name="applist_menu_quit"> 退出串流 </string> <string name="applist_menu_quit"> 退出串流 </string>
+10 -1
View File
@@ -6,6 +6,9 @@
<!-- Shortcut strings --> <!-- Shortcut strings -->
<string name="scut_deleted_pc">PC deleted</string> <string name="scut_deleted_pc">PC deleted</string>
<string name="scut_not_paired">PC not paired</string> <string name="scut_not_paired">PC not paired</string>
<string name="scut_pc_not_found">PC not found</string>
<string name="scut_invalid_uuid">Provided PC is not valid</string>
<string name="scut_invalid_app_id">Provided App is not valid</string>
<!-- Help strings --> <!-- Help strings -->
<string name="help_loading_title">Help Viewer</string> <string name="help_loading_title">Help Viewer</string>
@@ -17,6 +20,7 @@
<string name="pcview_menu_unpair_pc">Unpair</string> <string name="pcview_menu_unpair_pc">Unpair</string>
<string name="pcview_menu_send_wol">Send Wake-On-LAN request</string> <string name="pcview_menu_send_wol">Send Wake-On-LAN request</string>
<string name="pcview_menu_delete_pc">Delete PC</string> <string name="pcview_menu_delete_pc">Delete PC</string>
<string name="pcview_menu_details">View Details</string>
<!-- Pair messages --> <!-- Pair messages -->
<string name="pairing">Pairing…</string> <string name="pairing">Pairing…</string>
@@ -55,6 +59,7 @@
<string name="title_decoding_reset">Video Settings Reset</string> <string name="title_decoding_reset">Video Settings Reset</string>
<string name="message_decoding_reset">Your device\'s video decoder continues to crash at your selected streaming settings. Your streaming settings have been reset to default.</string> <string name="message_decoding_reset">Your device\'s video decoder continues to crash at your selected streaming settings. Your streaming settings have been reset to default.</string>
<string name="error_usb_prohibited">USB access is prohibited by your device administrator. Check your Knox or MDM settings.</string> <string name="error_usb_prohibited">USB access is prohibited by your device administrator. Check your Knox or MDM settings.</string>
<string name="unable_to_pin_shortcut">Your current launcher does not allow for creating pinned shortcuts.</string>
<!-- Start application messages --> <!-- Start application messages -->
<string name="conn_establishing_title">Establishing Connection</string> <string name="conn_establishing_title">Establishing Connection</string>
@@ -76,15 +81,18 @@
<string name="yes">Yes</string> <string name="yes">Yes</string>
<string name="no">No</string> <string name="no">No</string>
<string name="lost_connection">Lost connection to PC</string> <string name="lost_connection">Lost connection to PC</string>
<string name="title_details">Details</string>
<string name="help">Help</string> <string name="help">Help</string>
<string name="delete_pc_msg">Are you sure you want to delete this PC?</string>
<!-- AppList activity --> <!-- AppList activity -->
<string name="title_applist">Apps on</string>
<string name="applist_connect_msg">Connecting to PC…</string> <string name="applist_connect_msg">Connecting to PC…</string>
<string name="applist_menu_resume">Resume Session</string> <string name="applist_menu_resume">Resume Session</string>
<string name="applist_menu_quit">Quit Session</string> <string name="applist_menu_quit">Quit Session</string>
<string name="applist_menu_quit_and_start">Quit Current Game and Start</string> <string name="applist_menu_quit_and_start">Quit Current Game and Start</string>
<string name="applist_menu_cancel">Cancel</string> <string name="applist_menu_cancel">Cancel</string>
<string name="applist_menu_details">View Details</string>
<string name="applist_menu_scut">Create Shortcut</string>
<string name="applist_refresh_title">App List</string> <string name="applist_refresh_title">App List</string>
<string name="applist_refresh_msg">Refreshing apps…</string> <string name="applist_refresh_msg">Refreshing apps…</string>
<string name="applist_refresh_error_title">Error</string> <string name="applist_refresh_error_title">Error</string>
@@ -93,6 +101,7 @@
<string name="applist_quit_success">Successfully quit</string> <string name="applist_quit_success">Successfully quit</string>
<string name="applist_quit_fail">Failed to quit</string> <string name="applist_quit_fail">Failed to quit</string>
<string name="applist_quit_confirmation">Are you sure you want to quit the running app? All unsaved data will be lost.</string> <string name="applist_quit_confirmation">Are you sure you want to quit the running app? All unsaved data will be lost.</string>
<string name="applist_details_id">App ID:</string>
<!-- Add computer manually activity --> <!-- Add computer manually activity -->
<string name="title_add_pc">Add PC Manually</string> <string name="title_add_pc">Add PC Manually</string>
@@ -55,7 +55,7 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// Launch evdev_reader directly via SU // Launch evdev_reader directly via SU
try { try {
su = Runtime.getRuntime().exec("su -c "+evdevReaderCmd); su = new ProcessBuilder("su", "-c", evdevReaderCmd).start();
} catch (IOException e) { } catch (IOException e) {
reportDeviceNotRooted(); reportDeviceNotRooted();
e.printStackTrace(); e.printStackTrace();
@@ -151,7 +151,15 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
break; break;
case EvdevEvent.BTN_SIDE: case EvdevEvent.BTN_SIDE:
listener.mouseButtonEvent(EvdevListener.BUTTON_X1,
event.value != 0);
break;
case EvdevEvent.BTN_EXTRA: case EvdevEvent.BTN_EXTRA:
listener.mouseButtonEvent(EvdevListener.BUTTON_X2,
event.value != 0);
break;
case EvdevEvent.BTN_FORWARD: case EvdevEvent.BTN_FORWARD:
case EvdevEvent.BTN_BACK: case EvdevEvent.BTN_BACK:
case EvdevEvent.BTN_TASK: case EvdevEvent.BTN_TASK:
+1 -2
View File
@@ -5,8 +5,7 @@ buildscript {
google() google()
} }
dependencies { dependencies {
classpath "com.android.tools:r8:1.0.25" classpath 'com.android.tools.build:gradle:3.2.1'
classpath 'com.android.tools.build:gradle:3.2.0-alpha15'
} }
} }
+2 -2
View File
@@ -1,9 +1,9 @@
This file serves to document some of the decoder errata when using MediaCodec hardware decoders on certain devices. This file serves to document some of the decoder errata when using MediaCodec hardware decoders on certain devices.
1. num_ref_frames is set to 16 by NVENC which causes decoders to allocate 16+ buffers. This can cause an error or lag on some devices. 1. num_ref_frames is set to 16 by NVENC which causes decoders to allocate 16+ buffers. This can cause an error or lag on some devices.
- Affected decoders: TI OMAP4, Allwinner A20 - Affected decoders: TI OMAP4 crashes, Allwinner A20, MT8176 lags (HEVC not affected)
2. Some decoders have a huge per-frame latency with the unmodified SPS sent from NVENC. Setting max_dec_frame_buffering fixes this latency issue. 2. Some H.264 decoders have a huge per-frame latency with the unmodified SPS sent from NVENC. Setting max_dec_frame_buffering=1 fixes this latency issue.
- Affected decoders: NVIDIA Tegra 3 and 4, Broadcom VideoCore IV - Affected decoders: NVIDIA Tegra 3 and 4, Broadcom VideoCore IV
3. Some decoders strictly require that you pass BUFFER_FLAG_CODEC_CONFIG and crash upon the IDR frame if you don't 3. Some decoders strictly require that you pass BUFFER_FLAG_CODEC_CONFIG and crash upon the IDR frame if you don't