mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-10-20 07:24:42 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3de9765eaa | ||
|
|
d4072eb295 | ||
|
|
cac2bdbb81 | ||
|
|
66f0aee3f8 | ||
|
|
b690dc5474 | ||
|
|
c2fbe6ad91 | ||
|
|
cf07c02398 | ||
|
|
42dc928ad5 | ||
|
|
11597f0aa7 | ||
|
|
cdcd4d48f2 | ||
|
|
a9af4e54a9 | ||
|
|
7eac609219 | ||
|
|
fa761debc4 | ||
|
|
62e175f069 | ||
|
|
d7d8c40565 | ||
|
|
64de13ab50 | ||
|
|
2f02939638 | ||
|
|
1d7c8697e9 | ||
|
|
7dea322bbd | ||
|
|
349ecb16ab | ||
|
|
a3867735c1 | ||
|
|
5b087e9f70 | ||
|
|
eed18223eb | ||
|
|
30d4d2a918 | ||
|
|
30f666c70e | ||
|
|
209fead0e8 | ||
|
|
5c6889bf6d | ||
|
|
7d24900756 | ||
|
|
79a75b9d19 | ||
|
|
29b64992bd | ||
|
|
c9b14540f2 | ||
|
|
546843a26c | ||
|
|
d03d260535 | ||
|
|
6946e3c7a2 | ||
|
|
b79d328961 | ||
|
|
c313797d93 | ||
|
|
c8cb8e1346 | ||
|
|
6a9f8da14e | ||
|
|
ff9260a0fd | ||
|
|
62bedb1609 | ||
|
|
a519723d44 | ||
|
|
36191781ed | ||
|
|
61b6a49669 | ||
|
|
e97845e46e | ||
|
|
6bba68207d | ||
|
|
0e17cccc06 | ||
|
|
918e922e40 | ||
|
|
a08854ddfd | ||
|
|
eb6f15c2b7 | ||
|
|
2cd9e31684 | ||
|
|
791d6624e2 | ||
|
|
af41021271 | ||
|
|
d726d939f4 | ||
|
|
748085e7bb | ||
|
|
d57d19174b | ||
|
|
efebe1828a | ||
|
|
06007e0597 | ||
|
|
3a868045d7 | ||
|
|
e0a7ff1880 | ||
|
|
88d43bbd40 | ||
|
|
30ff319b13 | ||
|
|
9a0f48b799 | ||
|
|
b52c8a1a8f | ||
|
|
3fde115670 | ||
|
|
b6f4d8ff1e | ||
|
|
a7d85a7dd5 | ||
|
|
9b238ab6c3 | ||
|
|
f82ee97c05 |
No files matched your search
@@ -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.
|
||||||
@@ -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
@@ -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,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
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+10
-2
@@ -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);
|
||||||
|
|||||||
+1
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
+1
-1
Submodule moonlight-common updated: d87f24c617...dd6e76c0f2.
Reference in new issue
Block a user