Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad3614c58e | |||
| 9401ecc9fb | |||
| 1711e5e1a4 | |||
| 8eb4014f01 | |||
| df0d7952db | |||
| 77d1770063 | |||
| f433bfdc02 | |||
| f75b6f9b80 | |||
| 621df9996d | |||
| 6c29503db9 | |||
| 304a02e2ec | |||
| 7aea7ed8c6 | |||
| e5ab3baa7b | |||
| 41b73f7cd9 | |||
| 38da42caf3 | |||
| 424d71fa13 | |||
| dbc9d78002 | |||
| b7ef8f54b7 | |||
| bea7cab0c3 | |||
| 352b6f7dd9 | |||
| 8665fe364f | |||
| 7d023c8865 | |||
| 503d4b970c | |||
| 6b07072a08 | |||
| c873bae3e4 | |||
| 7397a97a9e | |||
| b567db9ab7 | |||
| 3440f54598 | |||
| d533b25b29 | |||
| 72290bd725 | |||
| 0ac83e1cf7 | |||
| e27129fc48 | |||
| d54fdc9f5f | |||
| dc984e8679 | |||
| ee46906376 | |||
| 1d76536e31 | |||
| dc97adc7a1 | |||
| a1c659b7b8 | |||
| 27f0fd63b3 | |||
| 83b66b19de | |||
| ba0171221c | |||
| 6fa1c35521 | |||
| 7a3fbd8dae | |||
| 329ee1a0bc | |||
| 11908e07bf | |||
| fd53122cb3 | |||
| d9c0830198 | |||
| d0aafb3814 | |||
| 40a3cc2ecb | |||
| 4469013bb5 | |||
| 78393932d0 | |||
| dbc2491151 | |||
| 01eb7a2b64 | |||
| 252285e4f7 | |||
| df7333b8d0 | |||
| cf98ec2c41 | |||
| 754773420f | |||
| 6574a0aab2 | |||
| 5d4988969e | |||
| 5121eb1852 | |||
| 004aeef2a7 | |||
| aa65a0312a | |||
| 1308a4ed80 |
@@ -38,3 +38,6 @@ build/
|
||||
# Compiled JNI libraries folder
|
||||
**/jniLibs
|
||||
app/.externalNativeBuild/
|
||||
|
||||
# NDK stuff
|
||||
.cxx/
|
||||
@@ -0,0 +1,15 @@
|
||||
language: android
|
||||
dist: trusty
|
||||
|
||||
git:
|
||||
depth: 1
|
||||
|
||||
android:
|
||||
components:
|
||||
- tools
|
||||
- platform-tools
|
||||
- build-tools-29.0.1
|
||||
- android-29
|
||||
|
||||
install:
|
||||
- yes | sdkmanager "ndk-bundle"
|
||||
@@ -1,5 +1,7 @@
|
||||
# Moonlight Android
|
||||
|
||||
[](https://travis-ci.org/moonlight-stream/moonlight-android)
|
||||
|
||||
[Moonlight](https://moonlight-stream.org) 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.
|
||||
|
||||
@@ -17,7 +19,7 @@ Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for mo
|
||||
## Installation
|
||||
|
||||
* Download and install Moonlight for Android from
|
||||
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
|
||||
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [F-Droid](https://f-droid.org/packages/com.limelight/), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
|
||||
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
buildToolsVersion '29.0.0'
|
||||
compileSdkVersion 29
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 29
|
||||
|
||||
versionName "7.4"
|
||||
versionCode = 195
|
||||
versionName "8.1"
|
||||
versionCode = 198
|
||||
}
|
||||
|
||||
flavorDimensions "root"
|
||||
@@ -44,6 +43,7 @@ android {
|
||||
|
||||
lintOptions {
|
||||
disable 'MissingTranslation'
|
||||
lintConfig file("lint.xml")
|
||||
}
|
||||
|
||||
bundle {
|
||||
@@ -114,8 +114,8 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.59'
|
||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.59'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.62'
|
||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.62'
|
||||
implementation 'org.jcodec:jcodec:0.2.3'
|
||||
|
||||
implementation project(':moonlight-common')
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<issue id="InvalidPackage">
|
||||
<ignore path="**/bcpkix-jdk15on-*.jar"/>
|
||||
</issue>
|
||||
</lint>
|
||||
@@ -8,6 +8,8 @@
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA"/>
|
||||
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA"/>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
@@ -39,8 +41,14 @@
|
||||
android:appCategory="game"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:installLocation="auto"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<provider
|
||||
android:name=".PosterContentProvider"
|
||||
android:authorities="poster.${applicationId}"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
</provider>
|
||||
<!-- Samsung multi-window support -->
|
||||
<uses-library
|
||||
android:name="com.sec.android.app.multiwindow"
|
||||
|
||||
@@ -68,6 +68,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
public final static String NAME_EXTRA = "Name";
|
||||
public final static String UUID_EXTRA = "UUID";
|
||||
public final static String NEW_PAIR_EXTRA = "NewPair";
|
||||
|
||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
@@ -89,6 +90,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
||||
shortcutHelper.createAppViewShortcut(computer, true, getIntent().getBooleanExtra(NEW_PAIR_EXTRA, false));
|
||||
shortcutHelper.reportComputerShortcutUsed(computer);
|
||||
|
||||
try {
|
||||
appGridAdapter = new AppGridAdapter(AppView.this,
|
||||
PreferenceConfiguration.readPreferences(AppView.this).listMode,
|
||||
@@ -181,7 +186,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
@Override
|
||||
public void run() {
|
||||
// Disable shortcuts referencing this PC for now
|
||||
shortcutHelper.disableShortcut(details.uuid,
|
||||
shortcutHelper.disableComputerShortcut(details,
|
||||
getResources().getString(R.string.scut_not_paired));
|
||||
|
||||
// Display a toast to the user and quit the activity
|
||||
@@ -267,10 +272,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
setTitle(computerName);
|
||||
label.setText(computerName);
|
||||
|
||||
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
||||
shortcutHelper.createAppViewShortcut(uuidString, computerName, uuidString, true);
|
||||
shortcutHelper.reportShortcutUsed(uuidString);
|
||||
|
||||
// Bind to the computer manager service
|
||||
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
||||
Service.BIND_AUTO_CREATE);
|
||||
@@ -419,7 +420,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
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)) {
|
||||
if (!shortcutHelper.createPinnedGameShortcut(computer, app.app, appBits)) {
|
||||
Toast.makeText(AppView.this, getResources().getString(R.string.unable_to_pin_shortcut), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
return true;
|
||||
@@ -515,6 +516,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
// This app was removed in the latest app list
|
||||
if (!foundExistingApp) {
|
||||
shortcutHelper.disableAppShortcut(computer, existingApp.app, "App removed from PC");
|
||||
appGridAdapter.removeApp(existingApp);
|
||||
updated = true;
|
||||
|
||||
|
||||
@@ -13,9 +13,11 @@ import com.limelight.binding.input.virtual_controller.VirtualController;
|
||||
import com.limelight.binding.video.CrashListener;
|
||||
import com.limelight.binding.video.MediaCodecDecoderRenderer;
|
||||
import com.limelight.binding.video.MediaCodecHelper;
|
||||
import com.limelight.binding.video.PerfOverlayListener;
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
import com.limelight.nvstream.NvConnectionListener;
|
||||
import com.limelight.nvstream.StreamConfiguration;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.input.KeyboardPacket;
|
||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||
@@ -78,7 +80,8 @@ import java.util.Locale;
|
||||
|
||||
public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
|
||||
OnSystemUiVisibilityChangeListener, GameGestures, StreamView.InputCallbacks
|
||||
OnSystemUiVisibilityChangeListener, GameGestures, StreamView.InputCallbacks,
|
||||
PerfOverlayListener
|
||||
{
|
||||
private int lastMouseX = Integer.MIN_VALUE;
|
||||
private int lastMouseY = Integer.MIN_VALUE;
|
||||
@@ -112,7 +115,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
private boolean grabbedInput = true;
|
||||
private boolean grabComboDown = false;
|
||||
private StreamView streamView;
|
||||
|
||||
private boolean isHidingOverlays;
|
||||
private TextView notificationOverlayView;
|
||||
private int requestedNotificationOverlayVisibility = View.GONE;
|
||||
private TextView performanceOverlayView;
|
||||
|
||||
private ShortcutHelper shortcutHelper;
|
||||
|
||||
@@ -207,6 +214,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
|
||||
notificationOverlayView = findViewById(R.id.notificationOverlay);
|
||||
|
||||
performanceOverlayView = findViewById(R.id.performanceOverlay);
|
||||
|
||||
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@@ -263,10 +272,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
||||
// Report this shortcut being used
|
||||
ComputerDetails computer = new ComputerDetails();
|
||||
computer.name = pcName;
|
||||
computer.uuid = uuid;
|
||||
shortcutHelper = new ShortcutHelper(this);
|
||||
shortcutHelper.createAppViewShortcut(uuid, pcName, uuid, true);
|
||||
shortcutHelper.reportShortcutUsed(uuid);
|
||||
shortcutHelper.reportComputerShortcutUsed(computer);
|
||||
if (appName != null) {
|
||||
// This may be null if launched from the "Resume Session" PC context menu item
|
||||
shortcutHelper.reportGameLaunched(computer, new NvApp(appName, appId, willStreamHdr));
|
||||
}
|
||||
|
||||
// Initialize the MediaCodec helper before creating the decoder
|
||||
GlPreferences glPrefs = GlPreferences.readPreferences(this);
|
||||
@@ -286,10 +301,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
|
||||
// We must now ensure our display is compatible with HDR10
|
||||
boolean foundHdr10 = false;
|
||||
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
|
||||
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
|
||||
LimeLog.info("Display supports HDR10");
|
||||
foundHdr10 = true;
|
||||
if (hdrCaps != null) {
|
||||
// getHdrCapabilities() returns null on Lenovo Lenovo Mirage Solo (vega), Android 8.0
|
||||
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
|
||||
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
|
||||
foundHdr10 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +325,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
willStreamHdr = false;
|
||||
}
|
||||
|
||||
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig,
|
||||
// Check if the user has enabled performance stats overlay
|
||||
if (prefConfig.enablePerfOverlay) {
|
||||
performanceOverlayView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
decoderRenderer = new MediaCodecDecoderRenderer(
|
||||
this,
|
||||
prefConfig,
|
||||
new CrashListener() {
|
||||
@Override
|
||||
public void notifyCrash(Exception e) {
|
||||
@@ -323,8 +347,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
tombstonePrefs.getInt("CrashCount", 0),
|
||||
connMgr.isActiveNetworkMetered(),
|
||||
willStreamHdr,
|
||||
glPrefs.glRenderer
|
||||
);
|
||||
glPrefs.glRenderer,
|
||||
this);
|
||||
|
||||
// Don't stream HDR if the decoder can't support it
|
||||
if (willStreamHdr && !decoderRenderer.isHevcMain10Hdr10Supported()) {
|
||||
@@ -398,7 +422,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
StreamConfiguration config = new StreamConfiguration.Builder()
|
||||
.setResolution(prefConfig.width, prefConfig.height)
|
||||
.setRefreshRate(prefConfig.fps)
|
||||
.setApp(new NvApp(appName, appId, willStreamHdr))
|
||||
.setApp(new NvApp(appName != null ? appName : "app", appId, willStreamHdr))
|
||||
.setBitrate(prefConfig.bitrate)
|
||||
.setEnableSops(prefConfig.enableSops)
|
||||
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
||||
@@ -472,15 +496,34 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
if (virtualController != null) {
|
||||
// Refresh layout of OSC for possible new screen size
|
||||
virtualController.refreshLayout();
|
||||
}
|
||||
|
||||
// Hide OSC in PiP
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (isInPictureInPictureMode()) {
|
||||
// Hide on-screen overlays in PiP mode
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (isInPictureInPictureMode()) {
|
||||
isHidingOverlays = true;
|
||||
|
||||
if (virtualController != null) {
|
||||
virtualController.hide();
|
||||
}
|
||||
else {
|
||||
|
||||
performanceOverlayView.setVisibility(View.GONE);
|
||||
notificationOverlayView.setVisibility(View.GONE);
|
||||
}
|
||||
else {
|
||||
isHidingOverlays = false;
|
||||
|
||||
// Restore overlays to previous state when leaving PiP
|
||||
|
||||
if (virtualController != null) {
|
||||
virtualController.show();
|
||||
}
|
||||
|
||||
if (prefConfig.enablePerfOverlay) {
|
||||
performanceOverlayView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
notificationOverlayView.setVisibility(requestedNotificationOverlayVisibility);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1381,10 +1424,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
notificationOverlayView.setText(getResources().getString(R.string.poor_connection_msg));
|
||||
}
|
||||
|
||||
notificationOverlayView.setVisibility(View.VISIBLE);
|
||||
requestedNotificationOverlayVisibility = View.VISIBLE;
|
||||
}
|
||||
else if (connectionStatus == MoonBridge.CONN_STATUS_OKAY) {
|
||||
notificationOverlayView.setVisibility(View.GONE);
|
||||
requestedNotificationOverlayVisibility = View.GONE;
|
||||
}
|
||||
|
||||
if (!isHidingOverlays) {
|
||||
notificationOverlayView.setVisibility(requestedNotificationOverlayVisibility);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1572,4 +1619,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
hideSystemUi(2000);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPerfUpdate(final String text) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
performanceOverlayView.setText(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.limelight.binding.crypto.AndroidCryptoProvider;
|
||||
import com.limelight.computers.ComputerManagerListener;
|
||||
import com.limelight.computers.ComputerManagerService;
|
||||
import com.limelight.grid.PcGridAdapter;
|
||||
import com.limelight.grid.assets.DiskAssetLoader;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
@@ -440,7 +441,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
if (toastSuccess) {
|
||||
// Open the app list after a successful pairing attempt
|
||||
doAppList(computer);
|
||||
doAppList(computer, true);
|
||||
}
|
||||
else {
|
||||
// Start polling again if we're still in the foreground
|
||||
@@ -539,7 +540,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void doAppList(ComputerDetails computer) {
|
||||
private void doAppList(ComputerDetails computer, boolean newlyPaired) {
|
||||
if (computer.state == ComputerDetails.State.OFFLINE) {
|
||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
@@ -552,6 +553,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
Intent i = new Intent(this, AppView.class);
|
||||
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
||||
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
|
||||
i.putExtra(AppView.NEW_PAIR_EXTRA, newlyPaired);
|
||||
startActivity(i);
|
||||
}
|
||||
|
||||
@@ -584,14 +586,13 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
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;
|
||||
|
||||
case APP_LIST_ID:
|
||||
doAppList(computer.details);
|
||||
doAppList(computer.details, false);
|
||||
return true;
|
||||
|
||||
case RESUME_ID:
|
||||
@@ -629,12 +630,16 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
|
||||
private void removeComputer(ComputerDetails details) {
|
||||
managerBinder.removeComputer(details);
|
||||
|
||||
new DiskAssetLoader(this).deleteAssetsForComputer(details.uuid);
|
||||
|
||||
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
|
||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
||||
|
||||
if (details.equals(computer.details)) {
|
||||
// Disable or delete shortcuts referencing this PC
|
||||
shortcutHelper.disableShortcut(details.uuid,
|
||||
shortcutHelper.disableComputerShortcut(details,
|
||||
getResources().getString(R.string.scut_deleted_pc));
|
||||
|
||||
pcGridAdapter.removeComputer(computer);
|
||||
@@ -665,7 +670,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
// Add a launcher shortcut for this PC
|
||||
if (details.pairState == PairState.PAIRED) {
|
||||
shortcutHelper.createAppViewShortcut(details.uuid, details, false);
|
||||
shortcutHelper.createAppViewShortcutForOnlineHost(details);
|
||||
}
|
||||
|
||||
if (existingEntry != null) {
|
||||
@@ -707,7 +712,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
// Pair an unpaired machine by default
|
||||
doPair(computer.details);
|
||||
} else {
|
||||
doAppList(computer.details);
|
||||
doAppList(computer.details, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.limelight;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import com.limelight.grid.assets.DiskAssetLoader;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.List;
|
||||
|
||||
public class PosterContentProvider extends ContentProvider {
|
||||
|
||||
|
||||
public static final String AUTHORITY = "poster." + BuildConfig.APPLICATION_ID;
|
||||
public static final String PNG_MIME_TYPE = "image/png";
|
||||
public static final int APP_ID_PATH_INDEX = 2;
|
||||
public static final int COMPUTER_UUID_PATH_INDEX = 1;
|
||||
private DiskAssetLoader mDiskAssetLoader;
|
||||
|
||||
private static final UriMatcher sUriMatcher;
|
||||
private static final String BOXART_PATH = "boxart";
|
||||
private static final int BOXART_URI_ID = 1;
|
||||
|
||||
static {
|
||||
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
sUriMatcher.addURI(AUTHORITY, BOXART_PATH, BOXART_URI_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
|
||||
int match = sUriMatcher.match(uri);
|
||||
if (match == BOXART_URI_ID) {
|
||||
return openBoxArtFile(uri, mode);
|
||||
}
|
||||
return openBoxArtFile(uri, mode);
|
||||
|
||||
}
|
||||
|
||||
public ParcelFileDescriptor openBoxArtFile(Uri uri, String mode) throws FileNotFoundException {
|
||||
if (!"r".equals(mode)) {
|
||||
throw new UnsupportedOperationException("This provider is only for read mode");
|
||||
}
|
||||
|
||||
List<String> segments = uri.getPathSegments();
|
||||
if (segments.size() != 3) {
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
String appId = segments.get(APP_ID_PATH_INDEX);
|
||||
String uuid = segments.get(COMPUTER_UUID_PATH_INDEX);
|
||||
File file = mDiskAssetLoader.getFile(uuid, Integer.parseInt(appId));
|
||||
if (file.exists()) {
|
||||
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
}
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
||||
throw new UnsupportedOperationException("This provider is only for read mode");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
return PNG_MIME_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
throw new UnsupportedOperationException("This provider is only for read mode");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
mDiskAssetLoader = new DiskAssetLoader(getContext());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection,
|
||||
String[] selectionArgs, String sortOrder) {
|
||||
throw new UnsupportedOperationException("This provider doesn't support query");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String selection,
|
||||
String[] selectionArgs) {
|
||||
throw new UnsupportedOperationException("This provider is support read only");
|
||||
}
|
||||
|
||||
|
||||
public static Uri createBoxArtUri(String uuid, String appId) {
|
||||
return new Uri.Builder()
|
||||
.scheme(ContentResolver.SCHEME_CONTENT)
|
||||
.authority(AUTHORITY)
|
||||
.appendPath(BOXART_PATH)
|
||||
.appendPath(uuid)
|
||||
.appendPath(appId)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,14 +23,12 @@ import java.util.UUID;
|
||||
|
||||
public class ShortcutTrampoline extends Activity {
|
||||
private String uuidString;
|
||||
private String appIdString;
|
||||
private NvApp app;
|
||||
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) {
|
||||
@@ -101,10 +99,9 @@ public class ShortcutTrampoline extends Activity {
|
||||
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));
|
||||
if (app != null) {
|
||||
if (details.runningGameId == 0 || details.runningGameId == app.getAppId()) {
|
||||
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this, app, details, managerBinder));
|
||||
|
||||
// Close this activity
|
||||
finish();
|
||||
@@ -114,8 +111,7 @@ public class ShortcutTrampoline extends Activity {
|
||||
} 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);
|
||||
final Intent startIntent = ServerHelper.createStartIntent(ShortcutTrampoline.this, app, details, managerBinder);
|
||||
|
||||
UiHelper.displayQuitConfirmationDialog(ShortcutTrampoline.this, new Runnable() {
|
||||
@Override
|
||||
@@ -155,7 +151,7 @@ public class ShortcutTrampoline extends Activity {
|
||||
// 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));
|
||||
new NvApp(null, details.runningGameId, false), details, managerBinder));
|
||||
}
|
||||
|
||||
// Now start the activities
|
||||
@@ -198,7 +194,7 @@ public class ShortcutTrampoline extends Activity {
|
||||
}
|
||||
};
|
||||
|
||||
protected boolean validateInput() {
|
||||
protected boolean validateInput(String uuidString, String appIdString) {
|
||||
// Validate UUID
|
||||
if (uuidString == null) {
|
||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
||||
@@ -240,10 +236,16 @@ public class ShortcutTrampoline extends Activity {
|
||||
|
||||
UiHelper.notifyNewRootView(this);
|
||||
|
||||
String appIdString = getIntent().getStringExtra(Game.EXTRA_APP_ID);
|
||||
uuidString = getIntent().getStringExtra(AppView.UUID_EXTRA);
|
||||
appIdString = getIntent().getStringExtra(APP_ID_EXTRA);
|
||||
|
||||
if (validateInput()) {
|
||||
if (validateInput(uuidString, appIdString)) {
|
||||
if (appIdString != null && !appIdString.isEmpty()) {
|
||||
app = new NvApp(getIntent().getStringExtra(Game.EXTRA_APP_NAME),
|
||||
Integer.parseInt(appIdString),
|
||||
getIntent().getBooleanExtra(Game.EXTRA_APP_HDR, false));
|
||||
}
|
||||
|
||||
// Bind to the computer manager service
|
||||
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
||||
Service.BIND_AUTO_CREATE);
|
||||
|
||||
@@ -148,7 +148,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
||||
|
||||
try {
|
||||
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(bcProvider).build(keyPair.getPrivate());
|
||||
ContentSigner sigGen = new JcaContentSignerBuilder("SHA256withRSA").setProvider(bcProvider).build(keyPair.getPrivate());
|
||||
cert = new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(sigGen));
|
||||
key = (RSAPrivateKey) keyPair.getPrivate();
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -8,10 +8,12 @@ import org.jcodec.codecs.h264.io.model.SeqParameterSet;
|
||||
import org.jcodec.codecs.h264.io.model.VUIParameters;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.R;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
import com.limelight.nvstream.jni.MoonBridge;
|
||||
import com.limelight.preferences.PreferenceConfiguration;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaFormat;
|
||||
@@ -38,6 +40,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
private boolean submittedCsd;
|
||||
private boolean submitCsdNextCall;
|
||||
|
||||
private Context context;
|
||||
private MediaCodec videoDecoder;
|
||||
private Thread rendererThread;
|
||||
private boolean needsSpsBitstreamFixup, isExynos4;
|
||||
@@ -55,6 +58,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
private String glRenderer;
|
||||
private boolean foreground = true;
|
||||
private boolean legacyFrameDropRendering = false;
|
||||
private PerfOverlayListener perfListener;
|
||||
|
||||
private boolean needsBaselineSpsHack;
|
||||
private SeqParameterSet savedSps;
|
||||
@@ -63,13 +67,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
private long initialExceptionTimestamp;
|
||||
private static final int EXCEPTION_REPORT_DELAY_MS = 3000;
|
||||
|
||||
private VideoStats activeWindowVideoStats;
|
||||
private VideoStats lastWindowVideoStats;
|
||||
private VideoStats globalVideoStats;
|
||||
|
||||
private long lastTimestampUs;
|
||||
private long decoderTimeMs;
|
||||
private long totalTimeMs;
|
||||
private int totalFramesReceived;
|
||||
private int totalFramesRendered;
|
||||
private int frameLossEvents;
|
||||
private int framesLost;
|
||||
private int lastFrameNumber;
|
||||
private int refreshRate;
|
||||
private PreferenceConfiguration prefs;
|
||||
@@ -119,16 +121,22 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
this.renderTarget = renderTarget;
|
||||
}
|
||||
|
||||
public MediaCodecDecoderRenderer(PreferenceConfiguration prefs,
|
||||
public MediaCodecDecoderRenderer(Context context, PreferenceConfiguration prefs,
|
||||
CrashListener crashListener, int consecutiveCrashCount,
|
||||
boolean meteredData, boolean requestedHdr,
|
||||
String glRenderer) {
|
||||
String glRenderer, PerfOverlayListener perfListener) {
|
||||
//dumpDecoders();
|
||||
|
||||
this.context = context;
|
||||
this.prefs = prefs;
|
||||
this.crashListener = crashListener;
|
||||
this.consecutiveCrashCount = consecutiveCrashCount;
|
||||
this.glRenderer = glRenderer;
|
||||
this.perfListener = perfListener;
|
||||
|
||||
this.activeWindowVideoStats = new VideoStats();
|
||||
this.lastWindowVideoStats = new VideoStats();
|
||||
this.globalVideoStats = new VideoStats();
|
||||
|
||||
avcDecoder = findAvcDecoder();
|
||||
if (avcDecoder != null) {
|
||||
@@ -311,7 +319,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
long delta = (renderTimeNanos / 1000000L) - (presentationTimeUs / 1000);
|
||||
if (delta >= 0 && delta < 1000) {
|
||||
if (USE_FRAME_RENDER_TIME) {
|
||||
totalTimeMs += delta;
|
||||
activeWindowVideoStats.totalTimeMs += delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,14 +429,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||
}
|
||||
|
||||
totalFramesRendered++;
|
||||
activeWindowVideoStats.totalFramesRendered++;
|
||||
|
||||
// Add delta time to the totals (excluding probable outliers)
|
||||
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
|
||||
if (delta >= 0 && delta < 1000) {
|
||||
decoderTimeMs += delta;
|
||||
activeWindowVideoStats.decoderTimeMs += delta;
|
||||
if (!USE_FRAME_RENDER_TIME) {
|
||||
totalTimeMs += delta;
|
||||
activeWindowVideoStats.totalTimeMs += delta;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -585,17 +593,58 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
return MoonBridge.DR_OK;
|
||||
}
|
||||
|
||||
totalFramesReceived++;
|
||||
|
||||
// We can receive the same "frame" multiple times if it's an IDR frame.
|
||||
// In that case, each frame start NALU is submitted independently.
|
||||
if (frameNumber != lastFrameNumber && frameNumber != lastFrameNumber + 1) {
|
||||
framesLost += frameNumber - lastFrameNumber - 1;
|
||||
frameLossEvents++;
|
||||
if (lastFrameNumber == 0) {
|
||||
activeWindowVideoStats.measurementStartTimestamp = System.currentTimeMillis();
|
||||
} else if (frameNumber != lastFrameNumber && frameNumber != lastFrameNumber + 1) {
|
||||
// We can receive the same "frame" multiple times if it's an IDR frame.
|
||||
// In that case, each frame start NALU is submitted independently.
|
||||
activeWindowVideoStats.framesLost += frameNumber - lastFrameNumber - 1;
|
||||
activeWindowVideoStats.totalFrames += frameNumber - lastFrameNumber - 1;
|
||||
activeWindowVideoStats.frameLossEvents++;
|
||||
}
|
||||
|
||||
lastFrameNumber = frameNumber;
|
||||
|
||||
// Flip stats windows roughly every second
|
||||
if (System.currentTimeMillis() >= activeWindowVideoStats.measurementStartTimestamp + 1000) {
|
||||
if (prefs.enablePerfOverlay) {
|
||||
VideoStats lastTwo = new VideoStats();
|
||||
lastTwo.add(lastWindowVideoStats);
|
||||
lastTwo.add(activeWindowVideoStats);
|
||||
VideoStatsFps fps = lastTwo.getFps();
|
||||
String decoder;
|
||||
|
||||
if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H264) != 0) {
|
||||
decoder = avcDecoder.getName();
|
||||
} else if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H265) != 0) {
|
||||
decoder = hevcDecoder.getName();
|
||||
} else {
|
||||
decoder = "(unknown)";
|
||||
}
|
||||
|
||||
float decodeTimeMs = (float)lastTwo.decoderTimeMs / lastTwo.totalFramesReceived;
|
||||
String perfText = context.getString(
|
||||
R.string.perf_overlay_text,
|
||||
initialWidth + "x" + initialHeight,
|
||||
decoder,
|
||||
fps.totalFps,
|
||||
fps.receivedFps,
|
||||
fps.renderedFps,
|
||||
(float)lastTwo.framesLost / lastTwo.totalFrames * 100,
|
||||
((float)lastTwo.totalTimeMs / lastTwo.totalFramesReceived) - decodeTimeMs,
|
||||
decodeTimeMs);
|
||||
perfListener.onPerfUpdate(perfText);
|
||||
}
|
||||
|
||||
globalVideoStats.add(activeWindowVideoStats);
|
||||
lastWindowVideoStats.copy(activeWindowVideoStats);
|
||||
activeWindowVideoStats.clear();
|
||||
activeWindowVideoStats.measurementStartTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
activeWindowVideoStats.totalFramesReceived++;
|
||||
activeWindowVideoStats.totalFrames++;
|
||||
|
||||
int inputBufferIndex;
|
||||
ByteBuffer buf;
|
||||
|
||||
@@ -603,7 +652,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
|
||||
if (!FRAME_RENDER_TIME_ONLY) {
|
||||
// Count time from first packet received to decode start
|
||||
totalTimeMs += (timestampUs / 1000) - receiveTimeMs;
|
||||
activeWindowVideoStats.totalTimeMs += (timestampUs / 1000) - receiveTimeMs;
|
||||
}
|
||||
|
||||
if (timestampUs <= lastTimestampUs) {
|
||||
@@ -910,17 +959,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
}
|
||||
|
||||
public int getAverageEndToEndLatency() {
|
||||
if (totalFramesReceived == 0) {
|
||||
if (globalVideoStats.totalFramesReceived == 0) {
|
||||
return 0;
|
||||
}
|
||||
return (int)(totalTimeMs / totalFramesReceived);
|
||||
return (int)(globalVideoStats.totalTimeMs / globalVideoStats.totalFramesReceived);
|
||||
}
|
||||
|
||||
public int getAverageDecoderLatency() {
|
||||
if (totalFramesReceived == 0) {
|
||||
if (globalVideoStats.totalFramesReceived == 0) {
|
||||
return 0;
|
||||
}
|
||||
return (int)(decoderTimeMs / totalFramesReceived);
|
||||
return (int)(globalVideoStats.decoderTimeMs / globalVideoStats.totalFramesReceived);
|
||||
}
|
||||
|
||||
static class DecoderHungException extends RuntimeException {
|
||||
@@ -981,9 +1030,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
str += "FPS target: "+renderer.refreshRate+"\n";
|
||||
str += "Bitrate: "+renderer.prefs.bitrate+" Kbps \n";
|
||||
str += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n";
|
||||
str += "Total frames received: "+renderer.totalFramesReceived+"\n";
|
||||
str += "Total frames rendered: "+renderer.totalFramesRendered+"\n";
|
||||
str += "Frame losses: "+renderer.framesLost+" in "+renderer.frameLossEvents+" loss events\n";
|
||||
str += "Total frames received: "+renderer.globalVideoStats.totalFramesReceived+"\n";
|
||||
str += "Total frames rendered: "+renderer.globalVideoStats.totalFramesRendered+"\n";
|
||||
str += "Frame losses: "+renderer.globalVideoStats.framesLost+" in "+renderer.globalVideoStats.frameLossEvents+" loss events\n";
|
||||
str += "Average end-to-end client latency: "+renderer.getAverageEndToEndLatency()+"ms\n";
|
||||
str += "Average hardware decoder latency: "+renderer.getAverageDecoderLatency()+"ms\n";
|
||||
|
||||
|
||||
@@ -554,7 +554,7 @@ public class MediaCodecHelper {
|
||||
|
||||
// Skip blacklisted codecs
|
||||
if (isCodecBlacklisted(codecInfo)) {
|
||||
//continue;
|
||||
continue;
|
||||
}
|
||||
|
||||
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.limelight.binding.video;
|
||||
|
||||
public interface PerfOverlayListener {
|
||||
void onPerfUpdate(final String text);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.limelight.binding.video;
|
||||
|
||||
class VideoStats {
|
||||
|
||||
long decoderTimeMs;
|
||||
long totalTimeMs;
|
||||
int totalFrames;
|
||||
int totalFramesReceived;
|
||||
int totalFramesRendered;
|
||||
int frameLossEvents;
|
||||
int framesLost;
|
||||
long measurementStartTimestamp;
|
||||
|
||||
void add(VideoStats other) {
|
||||
this.decoderTimeMs += other.decoderTimeMs;
|
||||
this.totalTimeMs += other.totalTimeMs;
|
||||
this.totalFrames += other.totalFrames;
|
||||
this.totalFramesReceived += other.totalFramesReceived;
|
||||
this.totalFramesRendered += other.totalFramesRendered;
|
||||
this.frameLossEvents += other.frameLossEvents;
|
||||
this.framesLost += other.framesLost;
|
||||
|
||||
if (this.measurementStartTimestamp == 0) {
|
||||
this.measurementStartTimestamp = other.measurementStartTimestamp;
|
||||
}
|
||||
|
||||
assert other.measurementStartTimestamp <= this.measurementStartTimestamp;
|
||||
}
|
||||
|
||||
void copy(VideoStats other) {
|
||||
this.decoderTimeMs = other.decoderTimeMs;
|
||||
this.totalTimeMs = other.totalTimeMs;
|
||||
this.totalFrames = other.totalFrames;
|
||||
this.totalFramesReceived = other.totalFramesReceived;
|
||||
this.totalFramesRendered = other.totalFramesRendered;
|
||||
this.frameLossEvents = other.frameLossEvents;
|
||||
this.framesLost = other.framesLost;
|
||||
this.measurementStartTimestamp = other.measurementStartTimestamp;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
this.decoderTimeMs = 0;
|
||||
this.totalTimeMs = 0;
|
||||
this.totalFrames = 0;
|
||||
this.totalFramesReceived = 0;
|
||||
this.totalFramesRendered = 0;
|
||||
this.frameLossEvents = 0;
|
||||
this.framesLost = 0;
|
||||
this.measurementStartTimestamp = 0;
|
||||
}
|
||||
|
||||
VideoStatsFps getFps() {
|
||||
float elapsed = (System.currentTimeMillis() - this.measurementStartTimestamp) / (float) 1000;
|
||||
|
||||
VideoStatsFps fps = new VideoStatsFps();
|
||||
if (elapsed > 0) {
|
||||
fps.totalFps = this.totalFrames / elapsed;
|
||||
fps.receivedFps = this.totalFramesReceived / elapsed;
|
||||
fps.renderedFps = this.totalFramesRendered / elapsed;
|
||||
}
|
||||
return fps;
|
||||
}
|
||||
}
|
||||
|
||||
class VideoStatsFps {
|
||||
|
||||
float totalFps;
|
||||
float receivedFps;
|
||||
float renderedFps;
|
||||
}
|
||||
@@ -18,16 +18,16 @@ import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
|
||||
public class ComputerDatabaseManager {
|
||||
private static final String COMPUTER_DB_NAME = "computers2.db";
|
||||
private static final String COMPUTER_DB_NAME = "computers3.db";
|
||||
private static final String COMPUTER_TABLE_NAME = "Computers";
|
||||
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
|
||||
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
|
||||
private static final String LOCAL_ADDRESS_COLUMN_NAME = "LocalAddress";
|
||||
private static final String REMOTE_ADDRESS_COLUMN_NAME = "RemoteAddress";
|
||||
private static final String MANUAL_ADDRESS_COLUMN_NAME = "ManualAddress";
|
||||
private static final String ADDRESSES_COLUMN_NAME = "Addresses";
|
||||
private static final String MAC_ADDRESS_COLUMN_NAME = "MacAddress";
|
||||
private static final String SERVER_CERT_COLUMN_NAME = "ServerCert";
|
||||
|
||||
private static final char ADDRESS_DELIMITER = ';';
|
||||
|
||||
private SQLiteDatabase computerDb;
|
||||
|
||||
public ComputerDatabaseManager(Context c) {
|
||||
@@ -47,40 +47,39 @@ public class ComputerDatabaseManager {
|
||||
}
|
||||
|
||||
private void initializeDb(Context c) {
|
||||
// Add cert column to the table if not present
|
||||
try {
|
||||
computerDb.execSQL(String.format((Locale)null,
|
||||
"ALTER TABLE %s ADD COLUMN %s TEXT",
|
||||
COMPUTER_TABLE_NAME, SERVER_CERT_COLUMN_NAME));
|
||||
} catch (SQLiteException e) {}
|
||||
|
||||
|
||||
// Create tables if they aren't already there
|
||||
computerDb.execSQL(String.format((Locale)null,
|
||||
"CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY, %s TEXT NOT NULL, %s TEXT, %s TEXT, %s TEXT, %s TEXT, %s TEXT)",
|
||||
COMPUTER_TABLE_NAME,
|
||||
COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME,
|
||||
LOCAL_ADDRESS_COLUMN_NAME, REMOTE_ADDRESS_COLUMN_NAME, MANUAL_ADDRESS_COLUMN_NAME,
|
||||
MAC_ADDRESS_COLUMN_NAME, SERVER_CERT_COLUMN_NAME));
|
||||
"CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT, %s TEXT)",
|
||||
COMPUTER_TABLE_NAME, COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME,
|
||||
ADDRESSES_COLUMN_NAME, MAC_ADDRESS_COLUMN_NAME, SERVER_CERT_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);
|
||||
}
|
||||
oldComputers = LegacyDatabaseReader2.migrateAllComputers(c);
|
||||
for (ComputerDetails computer : oldComputers) {
|
||||
updateComputer(computer);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteComputer(String name) {
|
||||
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name});
|
||||
public void deleteComputer(ComputerDetails details) {
|
||||
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{details.uuid});
|
||||
}
|
||||
|
||||
public boolean updateComputer(ComputerDetails details) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid);
|
||||
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
||||
values.put(LOCAL_ADDRESS_COLUMN_NAME, details.localAddress);
|
||||
values.put(REMOTE_ADDRESS_COLUMN_NAME, details.remoteAddress);
|
||||
values.put(MANUAL_ADDRESS_COLUMN_NAME, details.manualAddress);
|
||||
|
||||
StringBuilder addresses = new StringBuilder();
|
||||
addresses.append(details.localAddress != null ? details.localAddress : "");
|
||||
addresses.append(ADDRESS_DELIMITER).append(details.remoteAddress != null ? details.remoteAddress : "");
|
||||
addresses.append(ADDRESS_DELIMITER).append(details.manualAddress != null ? details.manualAddress : "");
|
||||
addresses.append(ADDRESS_DELIMITER).append(details.ipv6Address != null ? details.ipv6Address : "");
|
||||
|
||||
values.put(ADDRESSES_COLUMN_NAME, addresses.toString());
|
||||
values.put(MAC_ADDRESS_COLUMN_NAME, details.macAddress);
|
||||
try {
|
||||
if (details.serverCert != null) {
|
||||
@@ -96,18 +95,31 @@ public class ComputerDatabaseManager {
|
||||
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
|
||||
private static String readNonEmptyString(String input) {
|
||||
if (input.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
private ComputerDetails getComputerFromCursor(Cursor c) {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
|
||||
details.uuid = c.getString(0);
|
||||
details.name = c.getString(1);
|
||||
details.localAddress = c.getString(2);
|
||||
details.remoteAddress = c.getString(3);
|
||||
details.manualAddress = c.getString(4);
|
||||
details.macAddress = c.getString(5);
|
||||
|
||||
String[] addresses = c.getString(2).split(""+ADDRESS_DELIMITER, -1);
|
||||
|
||||
details.localAddress = readNonEmptyString(addresses[0]);
|
||||
details.remoteAddress = readNonEmptyString(addresses[1]);
|
||||
details.manualAddress = readNonEmptyString(addresses[2]);
|
||||
details.ipv6Address = readNonEmptyString(addresses[3]);
|
||||
|
||||
details.macAddress = c.getString(3);
|
||||
|
||||
try {
|
||||
byte[] derCertData = c.getBlob(6);
|
||||
byte[] derCertData = c.getBlob(4);
|
||||
|
||||
if (derCertData != null) {
|
||||
details.serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")
|
||||
@@ -127,14 +139,7 @@ public class ComputerDatabaseManager {
|
||||
Cursor c = computerDb.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);
|
||||
computerList.add(getComputerFromCursor(c));
|
||||
}
|
||||
|
||||
c.close();
|
||||
@@ -153,12 +158,6 @@ public class ComputerDatabaseManager {
|
||||
ComputerDetails details = getComputerFromCursor(c);
|
||||
c.close();
|
||||
|
||||
// If a critical field is corrupt or missing, delete the database entry
|
||||
if (details.uuid == null) {
|
||||
deleteComputer(details.name);
|
||||
return null;
|
||||
}
|
||||
|
||||
return details;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.limelight.computers;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.StringReader;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.HashSet;
|
||||
@@ -220,12 +221,12 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
|
||||
return ComputerManagerService.this.addComputerBlocking(addr, manuallyAdded);
|
||||
public boolean addComputerBlocking(ComputerDetails fakeDetails) {
|
||||
return ComputerManagerService.this.addComputerBlocking(fakeDetails);
|
||||
}
|
||||
|
||||
public void removeComputer(String name) {
|
||||
ComputerManagerService.this.removeComputer(name);
|
||||
public void removeComputer(ComputerDetails computer) {
|
||||
ComputerManagerService.this.removeComputer(computer);
|
||||
}
|
||||
|
||||
public void stopPolling() {
|
||||
@@ -297,8 +298,27 @@ public class ComputerManagerService extends Service {
|
||||
return new MdnsDiscoveryListener() {
|
||||
@Override
|
||||
public void notifyComputerAdded(MdnsComputer computer) {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
|
||||
// Populate the computer template with mDNS info
|
||||
if (computer.getLocalAddress() != null) {
|
||||
details.localAddress = computer.getLocalAddress().getHostAddress();
|
||||
|
||||
// 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.
|
||||
if (computer.getLocalAddress() instanceof Inet4Address) {
|
||||
details.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
|
||||
}
|
||||
}
|
||||
if (computer.getIpv6Address() != null) {
|
||||
details.ipv6Address = computer.getIpv6Address().getHostAddress();
|
||||
}
|
||||
|
||||
// Kick off a serverinfo poll on this machine
|
||||
addComputerBlocking(computer.getAddress().getHostAddress(), false);
|
||||
if (!addComputerBlocking(details)) {
|
||||
LimeLog.warning("Auto-discovered PC failed to respond: "+details);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -345,24 +365,7 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
|
||||
// Setup a placeholder
|
||||
ComputerDetails fakeDetails = new ComputerDetails();
|
||||
|
||||
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("stun.moonlight-stream.org", 3478);
|
||||
}
|
||||
|
||||
public boolean addComputerBlocking(ComputerDetails fakeDetails) {
|
||||
// Block while we try to fill the details
|
||||
try {
|
||||
// We cannot use runPoll() here because it will attempt to persist the state of the machine
|
||||
@@ -395,26 +398,22 @@ public class ComputerManagerService extends Service {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
if (!manuallyAdded) {
|
||||
LimeLog.warning("Auto-discovered PC failed to respond: "+addr);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void removeComputer(String name) {
|
||||
public void removeComputer(ComputerDetails computer) {
|
||||
if (!getLocalDatabaseReference()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove it from the database
|
||||
dbManager.deleteComputer(name);
|
||||
dbManager.deleteComputer(computer);
|
||||
|
||||
synchronized (pollingTuples) {
|
||||
// Remove the computer from the computer list
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
if (tuple.computer.name.equals(name)) {
|
||||
if (tuple.computer.uuid.equals(computer.uuid)) {
|
||||
if (tuple.thread != null) {
|
||||
// Interrupt the thread on this entry
|
||||
tuple.thread.interrupt();
|
||||
@@ -514,14 +513,16 @@ public class ComputerManagerService extends Service {
|
||||
t.start();
|
||||
}
|
||||
|
||||
private String fastPollPc(final String localAddress, final String remoteAddress, final String manualAddress) throws InterruptedException {
|
||||
private String fastPollPc(final String localAddress, final String remoteAddress, final String manualAddress, final String ipv6Address) throws InterruptedException {
|
||||
final boolean[] remoteInfo = new boolean[2];
|
||||
final boolean[] localInfo = new boolean[2];
|
||||
final boolean[] manualInfo = new boolean[2];
|
||||
final boolean[] ipv6Info = new boolean[2];
|
||||
|
||||
startFastPollThread(localAddress, localInfo);
|
||||
startFastPollThread(remoteAddress, remoteInfo);
|
||||
startFastPollThread(manualAddress, manualInfo);
|
||||
startFastPollThread(ipv6Address, ipv6Info);
|
||||
|
||||
// Check local first
|
||||
synchronized (localInfo) {
|
||||
@@ -545,7 +546,7 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
// And finally, remote
|
||||
// Now remote IPv4
|
||||
synchronized (remoteInfo) {
|
||||
while (!remoteInfo[0]) {
|
||||
remoteInfo.wait(500);
|
||||
@@ -556,6 +557,17 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
// Now global IPv6
|
||||
synchronized (ipv6Info) {
|
||||
while (!ipv6Info[0]) {
|
||||
ipv6Info.wait(500);
|
||||
}
|
||||
|
||||
if (ipv6Info[1]) {
|
||||
return ipv6Address;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -566,8 +578,8 @@ public class ComputerManagerService extends Service {
|
||||
// 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("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +", "+details.manualAddress+", "+details.ipv6Address+")");
|
||||
String candidateAddress = fastPollPc(details.localAddress, details.remoteAddress, details.manualAddress, details.ipv6Address);
|
||||
LimeLog.info("Fast poll for "+details.name+" returned candidate address: "+candidateAddress);
|
||||
|
||||
// If no connection could be established to either IP address, there's nothing we can do
|
||||
@@ -582,8 +594,9 @@ public class ComputerManagerService extends Service {
|
||||
// already tried
|
||||
HashSet<String> uniqueAddresses = new HashSet<>();
|
||||
uniqueAddresses.add(details.localAddress);
|
||||
uniqueAddresses.add(details.remoteAddress);
|
||||
uniqueAddresses.add(details.manualAddress);
|
||||
uniqueAddresses.add(details.remoteAddress);
|
||||
uniqueAddresses.add(details.ipv6Address);
|
||||
for (String addr : uniqueAddresses) {
|
||||
if (addr == null || addr.equals(candidateAddress)) {
|
||||
continue;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
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.nvstream.http.ComputerDetails;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class LegacyDatabaseReader2 {
|
||||
private static final String COMPUTER_DB_NAME = "computers2.db";
|
||||
private static final String COMPUTER_TABLE_NAME = "Computers";
|
||||
|
||||
private static ComputerDetails getComputerFromCursor(Cursor c) {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
|
||||
details.uuid = c.getString(0);
|
||||
details.name = c.getString(1);
|
||||
details.localAddress = c.getString(2);
|
||||
details.remoteAddress = c.getString(3);
|
||||
details.manualAddress = c.getString(4);
|
||||
details.macAddress = c.getString(5);
|
||||
|
||||
// This column wasn't always present in the old schema
|
||||
if (c.getColumnCount() >= 7) {
|
||||
try {
|
||||
byte[] derCertData = c.getBlob(6);
|
||||
|
||||
if (derCertData != null) {
|
||||
details.serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")
|
||||
.generateCertificate(new ByteArrayInputStream(derCertData));
|
||||
}
|
||||
} catch (CertificateException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// This signifies we don't have dynamic state (like pair state)
|
||||
details.state = ComputerDetails.State.UNKNOWN;
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
public static List<ComputerDetails> getAllComputers(SQLiteDatabase computerDb) {
|
||||
Cursor c = computerDb.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ public class DiskAssetLoader {
|
||||
}
|
||||
|
||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
||||
File file = CacheHelper.openPath(false, cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
||||
File file = getFile(tuple.computer.uuid, tuple.app.getAppId());
|
||||
|
||||
// Don't bother with anything if it doesn't exist
|
||||
if (!file.exists()) {
|
||||
@@ -133,6 +133,20 @@ public class DiskAssetLoader {
|
||||
return bmp;
|
||||
}
|
||||
|
||||
public File getFile(String computerUuid, int appId) {
|
||||
return CacheHelper.openPath(false, cacheDir, "boxart", computerUuid, appId + ".png");
|
||||
}
|
||||
|
||||
public void deleteAssetsForComputer(String computerUuid) {
|
||||
File dir = CacheHelper.openPath(false, cacheDir, "boxart", computerUuid);
|
||||
File[] files = dir.listFiles();
|
||||
if (files != null) {
|
||||
for (File f : files) {
|
||||
f.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
|
||||
OutputStream out = null;
|
||||
boolean success = false;
|
||||
|
||||
@@ -11,6 +11,7 @@ import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import com.limelight.computers.ComputerManagerService;
|
||||
import com.limelight.R;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.utils.Dialog;
|
||||
import com.limelight.utils.SpinnerDialog;
|
||||
import com.limelight.utils.UiHelper;
|
||||
@@ -98,7 +99,9 @@ public class AddComputerManually extends Activity {
|
||||
getResources().getString(R.string.msg_add_pc), false);
|
||||
|
||||
try {
|
||||
success = managerBinder.addComputerBlocking(host, true);
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
details.manualAddress = host;
|
||||
success = managerBinder.addComputerBlocking(details);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name.
|
||||
// https://github.com/square/okhttp/blob/okhttp_27/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java#L705
|
||||
|
||||
@@ -31,6 +31,7 @@ public class PreferenceConfiguration {
|
||||
private static final String DISABLE_FRAME_DROP_PREF_STRING = "checkbox_disable_frame_drop";
|
||||
private static final String ENABLE_HDR_PREF_STRING = "checkbox_enable_hdr";
|
||||
private static final String ENABLE_PIP_PREF_STRING = "checkbox_enable_pip";
|
||||
private static final String ENABLE_PERF_OVERLAY_STRING = "checkbox_enable_perf_overlay";
|
||||
private static final String BIND_ALL_USB_STRING = "checkbox_usb_bind_all";
|
||||
private static final String MOUSE_EMULATION_STRING = "checkbox_mouse_emulation";
|
||||
private static final String MOUSE_NAV_BUTTONS_STRING = "checkbox_mouse_nav_buttons";
|
||||
@@ -56,6 +57,7 @@ public class PreferenceConfiguration {
|
||||
private static final boolean DEFAULT_DISABLE_FRAME_DROP = false;
|
||||
private static final boolean DEFAULT_ENABLE_HDR = false;
|
||||
private static final boolean DEFAULT_ENABLE_PIP = false;
|
||||
private static final boolean DEFAULT_ENABLE_PERF_OVERLAY = false;
|
||||
private static final boolean DEFAULT_BIND_ALL_USB = false;
|
||||
private static final boolean DEFAULT_MOUSE_EMULATION = true;
|
||||
private static final boolean DEFAULT_MOUSE_NAV_BUTTONS = false;
|
||||
@@ -79,6 +81,7 @@ public class PreferenceConfiguration {
|
||||
public boolean disableFrameDrop;
|
||||
public boolean enableHdr;
|
||||
public boolean enablePip;
|
||||
public boolean enablePerfOverlay;
|
||||
public boolean bindAllUsb;
|
||||
public boolean mouseEmulation;
|
||||
public boolean mouseNavButtons;
|
||||
@@ -331,6 +334,7 @@ public class PreferenceConfiguration {
|
||||
config.disableFrameDrop = prefs.getBoolean(DISABLE_FRAME_DROP_PREF_STRING, DEFAULT_DISABLE_FRAME_DROP);
|
||||
config.enableHdr = prefs.getBoolean(ENABLE_HDR_PREF_STRING, DEFAULT_ENABLE_HDR);
|
||||
config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP);
|
||||
config.enablePerfOverlay = prefs.getBoolean(ENABLE_PERF_OVERLAY_STRING, DEFAULT_ENABLE_PERF_OVERLAY);
|
||||
config.bindAllUsb = prefs.getBoolean(BIND_ALL_USB_STRING, DEFAULT_BIND_ALL_USB);
|
||||
config.mouseEmulation = prefs.getBoolean(MOUSE_EMULATION_STRING, DEFAULT_MOUSE_EMULATION);
|
||||
config.mouseNavButtons = prefs.getBoolean(MOUSE_NAV_BUTTONS_STRING, DEFAULT_MOUSE_NAV_BUTTONS);
|
||||
|
||||
@@ -4,8 +4,10 @@ import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.limelight.AppView;
|
||||
import com.limelight.Game;
|
||||
import com.limelight.R;
|
||||
import com.limelight.ShortcutTrampoline;
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.computers.ComputerManagerService;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
@@ -25,6 +27,25 @@ public class ServerHelper {
|
||||
return computer.activeAddress;
|
||||
}
|
||||
|
||||
public static Intent createPcShortcutIntent(Activity parent, ComputerDetails computer) {
|
||||
Intent i = new Intent(parent, ShortcutTrampoline.class);
|
||||
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
||||
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
|
||||
i.setAction(Intent.ACTION_DEFAULT);
|
||||
return i;
|
||||
}
|
||||
|
||||
public static Intent createAppShortcutIntent(Activity parent, ComputerDetails computer, NvApp app) {
|
||||
Intent i = new Intent(parent, ShortcutTrampoline.class);
|
||||
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
||||
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
|
||||
i.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
||||
i.putExtra(Game.EXTRA_APP_ID, ""+app.getAppId());
|
||||
i.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported());
|
||||
i.setAction(Intent.ACTION_DEFAULT);
|
||||
return i;
|
||||
}
|
||||
|
||||
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
|
||||
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||
Intent intent = new Intent(parent, Game.class);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.limelight.utils;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ShortcutInfo;
|
||||
import android.content.pm.ShortcutManager;
|
||||
@@ -22,9 +22,10 @@ import java.util.List;
|
||||
public class ShortcutHelper {
|
||||
|
||||
private final ShortcutManager sm;
|
||||
private final Context context;
|
||||
private final Activity context;
|
||||
private final TvChannelHelper tvChannelHelper;
|
||||
|
||||
public ShortcutHelper(Context context) {
|
||||
public ShortcutHelper(Activity context) {
|
||||
this.context = context;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
sm = context.getSystemService(ShortcutManager.class);
|
||||
@@ -32,6 +33,7 @@ public class ShortcutHelper {
|
||||
else {
|
||||
sm = null;
|
||||
}
|
||||
this.tvChannelHelper = new TvChannelHelper(context);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||
@@ -80,39 +82,39 @@ public class ShortcutHelper {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void reportShortcutUsed(String id) {
|
||||
public void reportComputerShortcutUsed(ComputerDetails computer) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
if (getInfoForId(id) != null) {
|
||||
sm.reportShortcutUsed(id);
|
||||
if (getInfoForId(computer.uuid) != null) {
|
||||
sm.reportShortcutUsed(computer.uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void createAppViewShortcut(String id, String computerName, String computerUuid, boolean forceAdd) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
Intent i = new Intent(context, ShortcutTrampoline.class);
|
||||
i.putExtra(AppView.NAME_EXTRA, computerName);
|
||||
i.putExtra(AppView.UUID_EXTRA, computerUuid);
|
||||
i.setAction(Intent.ACTION_DEFAULT);
|
||||
public void reportGameLaunched(ComputerDetails computer, NvApp app) {
|
||||
tvChannelHelper.createTvChannel(computer);
|
||||
tvChannelHelper.addGameToChannel(computer, app);
|
||||
}
|
||||
|
||||
ShortcutInfo sinfo = new ShortcutInfo.Builder(context, id)
|
||||
.setIntent(i)
|
||||
.setShortLabel(computerName)
|
||||
.setLongLabel(computerName)
|
||||
public void createAppViewShortcut(ComputerDetails computer, boolean forceAdd, boolean newlyPaired) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
ShortcutInfo sinfo = new ShortcutInfo.Builder(context, computer.uuid)
|
||||
.setIntent(ServerHelper.createPcShortcutIntent(context, computer))
|
||||
.setShortLabel(computer.name)
|
||||
.setLongLabel(computer.name)
|
||||
.setIcon(Icon.createWithResource(context, R.mipmap.ic_pc_scut))
|
||||
.build();
|
||||
|
||||
ShortcutInfo existingSinfo = getInfoForId(id);
|
||||
ShortcutInfo existingSinfo = getInfoForId(computer.uuid);
|
||||
if (existingSinfo != null) {
|
||||
// Update in place
|
||||
sm.updateShortcuts(Collections.singletonList(sinfo));
|
||||
sm.enableShortcuts(Collections.singletonList(id));
|
||||
sm.enableShortcuts(Collections.singletonList(computer.uuid));
|
||||
}
|
||||
|
||||
// Reap shortcuts to make space for this if it's new
|
||||
// NOTE: This CAN'T be an else on the above if, because it's
|
||||
// possible that we have an existing shortcut but it's not a dynamic one.
|
||||
if (!isExistingDynamicShortcut(id)) {
|
||||
if (!isExistingDynamicShortcut(computer.uuid)) {
|
||||
// To avoid a random carousel of shortcuts popping in and out based on polling status,
|
||||
// we only add shortcuts if it's not at the limit or the user made a conscious action
|
||||
// to interact with this PC.
|
||||
@@ -122,22 +124,26 @@ public class ShortcutHelper {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newlyPaired) {
|
||||
// Avoid hammering the channel API for each computer poll because it will throttle us
|
||||
tvChannelHelper.createTvChannel(computer);
|
||||
tvChannelHelper.requestChannelOnHomeScreen(computer);
|
||||
}
|
||||
}
|
||||
|
||||
public void createAppViewShortcut(String id, ComputerDetails details, boolean forceAdd) {
|
||||
createAppViewShortcut(id, details.name, details.uuid, forceAdd);
|
||||
public void createAppViewShortcutForOnlineHost(ComputerDetails details) {
|
||||
createAppViewShortcut(details, false, false);
|
||||
}
|
||||
|
||||
private String getShortcutIdForGame(ComputerDetails computer, NvApp app) {
|
||||
return computer.uuid + app.getAppId();
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
public boolean createPinnedGameShortcut(String id, Bitmap iconBits, String computerName, String computerUuid, String appName, String appId) {
|
||||
public boolean createPinnedGameShortcut(ComputerDetails computer, NvApp app, Bitmap iconBits) {
|
||||
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);
|
||||
@@ -145,9 +151,9 @@ public class ShortcutHelper {
|
||||
appIcon = Icon.createWithResource(context, R.mipmap.ic_pc_scut);
|
||||
}
|
||||
|
||||
ShortcutInfo sInfo = new ShortcutInfo.Builder(context, id)
|
||||
.setIntent(i)
|
||||
.setShortLabel(appName + " (" + computerName + ")")
|
||||
ShortcutInfo sInfo = new ShortcutInfo.Builder(context, getShortcutIdForGame(computer, app))
|
||||
.setIntent(ServerHelper.createAppShortcutIntent(context, computer, app))
|
||||
.setShortLabel(app.getAppName() + " (" + computer.name + ")")
|
||||
.setIcon(appIcon)
|
||||
.build();
|
||||
|
||||
@@ -157,12 +163,30 @@ public class ShortcutHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean createPinnedGameShortcut(String id, Bitmap iconBits, ComputerDetails cDetails, NvApp app) {
|
||||
return createPinnedGameShortcut(id, iconBits, cDetails.name, cDetails.uuid, app.getAppName(), Integer.valueOf(app.getAppId()).toString());
|
||||
public void disableComputerShortcut(ComputerDetails computer, CharSequence reason) {
|
||||
tvChannelHelper.deleteChannel(computer);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
// Delete the computer shortcut itself
|
||||
if (getInfoForId(computer.uuid) != null) {
|
||||
sm.disableShortcuts(Collections.singletonList(computer.uuid), reason);
|
||||
}
|
||||
|
||||
// Delete all associated app shortcuts too
|
||||
List<ShortcutInfo> shortcuts = getAllShortcuts();
|
||||
LinkedList<String> appShortcutIds = new LinkedList<>();
|
||||
for (ShortcutInfo info : shortcuts) {
|
||||
if (info.getId().startsWith(computer.uuid)) {
|
||||
appShortcutIds.add(info.getId());
|
||||
}
|
||||
}
|
||||
sm.disableShortcuts(appShortcutIds, reason);
|
||||
}
|
||||
}
|
||||
|
||||
public void disableShortcut(String id, CharSequence reason) {
|
||||
public void disableAppShortcut(ComputerDetails computer, NvApp app, CharSequence reason) {
|
||||
tvChannelHelper.deleteProgram(computer, app);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
String id = getShortcutIdForGame(computer, app);
|
||||
if (getInfoForId(id) != null) {
|
||||
sm.disableShortcuts(Collections.singletonList(id), reason);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
package com.limelight.utils;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.tv.TvContract;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.PosterContentProvider;
|
||||
import com.limelight.R;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class TvChannelHelper {
|
||||
|
||||
private static final int ASPECT_RATIO_MOVIE_POSTER = 5;
|
||||
private static final int TYPE_GAME = 12;
|
||||
private static final int INTERNAL_PROVIDER_ID_INDEX = 1;
|
||||
private static final int PROGRAM_BROWSABLE_INDEX = 2;
|
||||
private static final int ID_INDEX = 0;
|
||||
private Activity context;
|
||||
|
||||
public TvChannelHelper(Activity context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
void requestChannelOnHomeScreen(ComputerDetails computer) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (!isAndroidTV()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Long channelId = getChannelId(computer.uuid);
|
||||
if (channelId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(TvContract.ACTION_REQUEST_CHANNEL_BROWSABLE);
|
||||
intent.putExtra(TvContract.EXTRA_CHANNEL_ID, getChannelId(computer.uuid));
|
||||
try {
|
||||
context.startActivityForResult(intent, 0);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void createTvChannel(ComputerDetails computer) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (!isAndroidTV()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ChannelBuilder builder = new ChannelBuilder()
|
||||
.setType(TvContract.Channels.TYPE_PREVIEW)
|
||||
.setDisplayName(computer.name)
|
||||
.setInternalProviderId(computer.uuid)
|
||||
.setAppLinkIntent(ServerHelper.createPcShortcutIntent(context, computer));
|
||||
|
||||
Long channelId = getChannelId(computer.uuid);
|
||||
if (channelId != null) {
|
||||
context.getContentResolver().update(TvContract.buildChannelUri(channelId),
|
||||
builder.toContentValues(), null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
Uri channelUri = context.getContentResolver().insert(
|
||||
TvContract.Channels.CONTENT_URI, builder.toContentValues());
|
||||
if (channelUri != null) {
|
||||
long id = ContentUris.parseId(channelUri);
|
||||
updateChannelIcon(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
private void updateChannelIcon(long channelId) {
|
||||
Bitmap logo = drawableToBitmap(context.getResources().getDrawable(R.drawable.ic_channel));
|
||||
try {
|
||||
Uri localUri = TvContract.buildChannelLogoUri(channelId);
|
||||
try (OutputStream outputStream = context.getContentResolver().openOutputStream(localUri)) {
|
||||
logo.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
|
||||
outputStream.flush();
|
||||
} catch (SQLiteException | IOException e) {
|
||||
LimeLog.warning("Failed to store the logo to the system content provider.");
|
||||
e.printStackTrace();
|
||||
}
|
||||
} finally {
|
||||
logo.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
private Bitmap drawableToBitmap(Drawable drawable) {
|
||||
int width = context.getResources().getDimensionPixelSize(R.dimen.tv_channel_logo_width);
|
||||
int height = context.getResources().getDimensionPixelSize(R.dimen.tv_channel_logo_width);
|
||||
|
||||
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
drawable.draw(canvas);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
void addGameToChannel(ComputerDetails computer, NvApp app) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (!isAndroidTV()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Long channelId = getChannelId(computer.uuid);
|
||||
if (channelId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
PreviewProgramBuilder builder = new PreviewProgramBuilder()
|
||||
.setChannelId(channelId)
|
||||
.setType(TYPE_GAME)
|
||||
.setTitle(app.getAppName())
|
||||
.setPosterArtAspectRatio(ASPECT_RATIO_MOVIE_POSTER)
|
||||
.setPosterArtUri(PosterContentProvider.createBoxArtUri(computer.uuid, ""+app.getAppId()))
|
||||
.setIntent(ServerHelper.createAppShortcutIntent(context, computer, app))
|
||||
.setInternalProviderId(""+app.getAppId())
|
||||
// Weight should increase each time we run the game
|
||||
.setWeight((int)((System.currentTimeMillis() - 1500000000000L) / 1000));
|
||||
|
||||
Long programId = getProgramId(channelId, ""+app.getAppId());
|
||||
if (programId != null) {
|
||||
context.getContentResolver().update(TvContract.buildPreviewProgramUri(programId),
|
||||
builder.toContentValues(), null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
context.getContentResolver().insert(TvContract.PreviewPrograms.CONTENT_URI,
|
||||
builder.toContentValues());
|
||||
|
||||
TvContract.requestChannelBrowsable(context, channelId);
|
||||
}
|
||||
}
|
||||
|
||||
void deleteChannel(ComputerDetails computer) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (!isAndroidTV()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Long channelId = getChannelId(computer.uuid);
|
||||
if (channelId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.getContentResolver().delete(TvContract.buildChannelUri(channelId), null, null);
|
||||
}
|
||||
}
|
||||
|
||||
void deleteProgram(ComputerDetails computer, NvApp app) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (!isAndroidTV()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Long channelId = getChannelId(computer.uuid);
|
||||
if (channelId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Long programId = getProgramId(channelId, ""+app.getAppId());
|
||||
if (programId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.getContentResolver().delete(TvContract.buildPreviewProgramUri(programId), null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
private Long getChannelId(String computerUuid) {
|
||||
try (Cursor cursor = context.getContentResolver().query(
|
||||
TvContract.Channels.CONTENT_URI,
|
||||
new String[] {TvContract.Channels._ID, TvContract.Channels.COLUMN_INTERNAL_PROVIDER_ID},
|
||||
null,
|
||||
null,
|
||||
null)) {
|
||||
if (cursor == null || cursor.getCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
while (cursor.moveToNext()) {
|
||||
String internalProviderId = cursor.getString(INTERNAL_PROVIDER_ID_INDEX);
|
||||
if (computerUuid.equals(internalProviderId)) {
|
||||
return cursor.getLong(ID_INDEX);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
private Long getProgramId(long channelId, String appId) {
|
||||
try (Cursor cursor = context.getContentResolver().query(
|
||||
TvContract.buildPreviewProgramsUriForChannel(channelId),
|
||||
new String[] {TvContract.PreviewPrograms._ID, TvContract.PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID, TvContract.PreviewPrograms.COLUMN_BROWSABLE},
|
||||
null,
|
||||
null,
|
||||
null)) {
|
||||
if (cursor == null || cursor.getCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
while (cursor.moveToNext()) {
|
||||
String internalProviderId = cursor.getString(INTERNAL_PROVIDER_ID_INDEX);
|
||||
if (appId.equals(internalProviderId)) {
|
||||
long id = cursor.getLong(ID_INDEX);
|
||||
int browsable = cursor.getInt(PROGRAM_BROWSABLE_INDEX);
|
||||
if (browsable != 0) {
|
||||
return id;
|
||||
} else {
|
||||
int countDeleted = context.getContentResolver().delete(TvContract.buildPreviewProgramUri(id), null, null);
|
||||
if (countDeleted > 0) {
|
||||
LimeLog.info("Preview program has been deleted");
|
||||
} else {
|
||||
LimeLog.warning("Preview program has not been deleted");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> String toValueString(T value) {
|
||||
return value == null ? null : value.toString();
|
||||
}
|
||||
|
||||
private static String toUriString(Intent intent) {
|
||||
return intent == null ? null : intent.toUri(Intent.URI_INTENT_SCHEME);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
private boolean isAndroidTV() {
|
||||
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
private static class PreviewProgramBuilder {
|
||||
|
||||
private ContentValues mValues = new ContentValues();
|
||||
|
||||
|
||||
public PreviewProgramBuilder setChannelId(Long channelId) {
|
||||
mValues.put(TvContract.PreviewPrograms.COLUMN_CHANNEL_ID, channelId);
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreviewProgramBuilder setType(int type) {
|
||||
mValues.put(TvContract.PreviewPrograms.COLUMN_TYPE, type);
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreviewProgramBuilder setTitle(String title) {
|
||||
mValues.put(TvContract.PreviewPrograms.COLUMN_TITLE, title);
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreviewProgramBuilder setPosterArtAspectRatio(int aspectRatio) {
|
||||
mValues.put(TvContract.PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO, aspectRatio);
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreviewProgramBuilder setIntent(Intent intent) {
|
||||
mValues.put(TvContract.PreviewPrograms.COLUMN_INTENT_URI, toUriString(intent));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreviewProgramBuilder setIntentUri(Uri uri) {
|
||||
mValues.put(TvContract.PreviewPrograms.COLUMN_INTENT_URI, toValueString(uri));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreviewProgramBuilder setInternalProviderId(String id) {
|
||||
mValues.put(TvContract.PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID, id);
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreviewProgramBuilder setPosterArtUri(Uri uri) {
|
||||
mValues.put(TvContract.PreviewPrograms.COLUMN_POSTER_ART_URI, toValueString(uri));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreviewProgramBuilder setWeight(int weight) {
|
||||
mValues.put(TvContract.PreviewPrograms.COLUMN_WEIGHT, weight);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContentValues toContentValues() {
|
||||
return new ContentValues(mValues);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
private static class ChannelBuilder {
|
||||
|
||||
private ContentValues mValues = new ContentValues();
|
||||
|
||||
public ChannelBuilder setType(String type) {
|
||||
mValues.put(TvContract.Channels.COLUMN_TYPE, type);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChannelBuilder setDisplayName(String displayName) {
|
||||
mValues.put(TvContract.Channels.COLUMN_DISPLAY_NAME, displayName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChannelBuilder setInternalProviderId(String internalProviderId) {
|
||||
mValues.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_ID, internalProviderId);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChannelBuilder setAppLinkIntent(Intent intent) {
|
||||
mValues.put(TvContract.Channels.COLUMN_APP_LINK_INTENT_URI, toUriString(intent));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContentValues toContentValues() {
|
||||
return new ContentValues(mValues);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,12 @@ package com.limelight.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.UiModeManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import com.limelight.R;
|
||||
@@ -19,10 +18,6 @@ import java.util.Locale;
|
||||
|
||||
public class UiHelper {
|
||||
|
||||
// Values from https://developer.android.com/training/tv/start/layouts.html
|
||||
private static final int TV_VERTICAL_PADDING_DP = 27;
|
||||
private static final int TV_HORIZONTAL_PADDING_DP = 48;
|
||||
|
||||
public static void setLocale(Activity activity)
|
||||
{
|
||||
String locale = PreferenceConfiguration.readPreferences(activity).language;
|
||||
@@ -47,20 +42,6 @@ public class UiHelper {
|
||||
|
||||
public static void notifyNewRootView(Activity activity)
|
||||
{
|
||||
View rootView = activity.findViewById(android.R.id.content);
|
||||
UiModeManager modeMgr = (UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
|
||||
|
||||
if (modeMgr.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION)
|
||||
{
|
||||
// Increase view padding on TVs
|
||||
float scale = activity.getResources().getDisplayMetrics().density;
|
||||
int verticalPaddingPixels = (int) (TV_VERTICAL_PADDING_DP*scale + 0.5f);
|
||||
int horizontalPaddingPixels = (int) (TV_HORIZONTAL_PADDING_DP*scale + 0.5f);
|
||||
|
||||
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
|
||||
horizontalPaddingPixels, verticalPaddingPixels);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
// Allow this non-streaming activity to layout under notches.
|
||||
//
|
||||
@@ -70,6 +51,23 @@ public class UiHelper {
|
||||
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// Draw under the status bar on Android Q devices
|
||||
|
||||
activity.getWindow().getDecorView().setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
|
||||
view.setPadding(windowInsets.getSystemWindowInsetLeft(),
|
||||
windowInsets.getSystemWindowInsetTop(),
|
||||
windowInsets.getSystemWindowInsetRight(),
|
||||
0);
|
||||
return windowInsets;
|
||||
}
|
||||
});
|
||||
|
||||
activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
|
||||
}
|
||||
}
|
||||
|
||||
public static void showDecoderCrashDialog(Activity activity) {
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/ic_launcher_background"/>
|
||||
<item android:drawable="@drawable/ic_lime_layer"
|
||||
android:bottom="10dp"
|
||||
android:left="10dp"
|
||||
android:right="10dp"
|
||||
android:top="10dp"
|
||||
/>
|
||||
</layer-list>
|
||||
@@ -0,0 +1,19 @@
|
||||
<vector android:height="24dp" android:viewportHeight="546.15576"
|
||||
android:viewportWidth="546.08374" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="M252.584,0.356c-58.2,4.8 -112.3,27.1 -156.8,64.6l-8.4,7 86.9,86.9c47.7,47.7 87.1,86.8 87.6,86.8 0.4,-0 0.6,-55.2 0.5,-122.8l-0.3,-122.7 -2.5,-0.1c-1.4,-0 -4.5,0.1 -7,0.3z" android:strokeColor="#00000000"/>
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="M284.284,0.356c-1,0.9 -0.9,245.3 0.1,245.3 0.4,-0 39.8,-39.1 87.5,-86.8l86.9,-86.9 -8.4,-7c-34.4,-29 -74.9,-49.2 -117.8,-58.7 -16.6,-3.6 -46.9,-7.4 -48.3,-5.9z" android:strokeColor="#00000000"/>
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="M64.884,95.856c-24.1,28.5 -41.2,59.5 -52.6,95.3 -6.3,19.6 -11.1,45.8 -11.9,64l-0.3,7 122.8,0.3c67.5,0.1 122.7,-0.1 122.7,-0.5 0,-0.5 -39.1,-39.9 -86.8,-87.6l-86.9,-86.9 -7,8.4z" android:strokeColor="#00000000"/>
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="M387.384,174.356c-47.8,47.7 -86.8,87.1 -86.8,87.6 0,0.4 55.2,0.6 122.8,0.5l122.7,-0.3 -0.3,-7c-1.4,-32.6 -12.4,-72.9 -28.7,-105.5 -9.1,-18.2 -25.9,-43 -38.7,-57.3l-4.3,-4.8 -86.7,86.8z" android:strokeColor="#00000000"/>
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="M0.184,284.556c-0.7,1.1 1,19.8 3,32.1 7.6,48.6 29.1,95.2 61.7,133.8l7,8.4 86.9,-86.9c47.7,-47.7 86.8,-87.1 86.8,-87.5 0,-1.1 -244.8,-1 -245.4,0.1z" android:strokeColor="#00000000"/>
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="M300.584,284.356c0,0.5 39.1,39.9 86.8,87.6l86.9,86.9 7,-8.4c24.1,-28.5 41.2,-59.5 52.6,-95.3 6.3,-19.6 11.1,-45.8 11.9,-64l0.3,-7 -122.7,-0.3c-67.6,-0.1 -122.8,0.1 -122.8,0.5z" android:strokeColor="#00000000"/>
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="M174.284,387.456l-86.9,86.9 8.4,7c28.5,24.1 59.5,41.2 95.3,52.6 19.6,6.3 45.8,11.1 64,11.9l7,0.3 0.3,-122.8c0.1,-67.5 -0.1,-122.7 -0.5,-122.7 -0.5,-0 -39.9,39.1 -87.6,86.8z" android:strokeColor="#00000000"/>
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="M283.784,423.356l0.3,122.8 7,-0.3c18.2,-0.8 44.4,-5.6 64,-11.9 35.8,-11.4 66.8,-28.5 95.3,-52.6l8.4,-7 -86.9,-86.9c-47.7,-47.7 -87.1,-86.8 -87.6,-86.8 -0.4,-0 -0.6,55.2 -0.5,122.7z" android:strokeColor="#00000000"/>
|
||||
</vector>
|
||||
@@ -2,7 +2,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context=".AppView" >
|
||||
|
||||
<FrameLayout
|
||||
@@ -27,8 +23,8 @@
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:gravity="center"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingBottom="5dp"
|
||||
android:textSize="28sp"/>
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -11,11 +11,25 @@
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notificationOverlay"
|
||||
android:id="@+id/performanceOverlay"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_gravity="left"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:gravity="left"
|
||||
android:background="#80000000"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notificationOverlay"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_gravity="right"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:gravity="right"
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:id="@+id/stream_settings"
|
||||
tools:context=".preferences.StreamSettings">
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<GridView
|
||||
android:id="@+id/fragmentView"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:numColumns="auto_fit"
|
||||
android:columnWidth="160dp"
|
||||
android:stretchMode="spacingWidth"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:gravity="center"/>
|
||||
</LinearLayout>
|
||||
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/fragmentView"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:numColumns="auto_fit"
|
||||
android:columnWidth="160dp"
|
||||
android:stretchMode="spacingWidthUniform"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:gravity="center"/>
|
||||
@@ -1,15 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<GridView
|
||||
android:id="@+id/fragmentView"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:numColumns="auto_fit"
|
||||
android:columnWidth="105dp"
|
||||
android:stretchMode="spacingWidth"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:gravity="center"/>
|
||||
</LinearLayout>
|
||||
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/fragmentView"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:numColumns="auto_fit"
|
||||
android:columnWidth="105dp"
|
||||
android:stretchMode="spacingWidthUniform"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:gravity="center"/>
|
||||
@@ -1,15 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<GridView
|
||||
android:id="@+id/fragmentView"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:numColumns="auto_fit"
|
||||
android:columnWidth="160dp"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:nextFocusLeft="@id/settingsButton"
|
||||
android:gravity="center"/>
|
||||
</LinearLayout>
|
||||
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/fragmentView"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:numColumns="auto_fit"
|
||||
android:columnWidth="160dp"
|
||||
android:stretchMode="spacingWidthUniform"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:nextFocusLeft="@id/settingsButton"
|
||||
android:gravity="center"/>
|
||||
@@ -1,15 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<GridView
|
||||
android:id="@+id/fragmentView"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:numColumns="auto_fit"
|
||||
android:columnWidth="105dp"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:nextFocusLeft="@id/settingsButton"
|
||||
android:gravity="center"/>
|
||||
</LinearLayout>
|
||||
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/fragmentView"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:numColumns="auto_fit"
|
||||
android:columnWidth="105dp"
|
||||
android:stretchMode="spacingWidthUniform"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:nextFocusLeft="@id/settingsButton"
|
||||
android:gravity="center"/>
|
||||
@@ -82,6 +82,9 @@
|
||||
<string name="title_details">Détails</string>
|
||||
<string name="help">Aide</string>
|
||||
<string name="delete_pc_msg">Êtes-vous sûr de vouloir supprimer ce PC?</string>
|
||||
<string name="slow_connection_msg">Connexion lente au PC\nRéduisez votre débit</string>
|
||||
<string name="poor_connection_msg">Mauvaise connexion au PC</string>
|
||||
<string name="perf_overlay_text">Video dimensions: %1$s\nDécodeur: %2$s\nEstimation de la fréquence d\'images de l\'ordinateur hôte: %3$.2f FPS\nFréquence d\'images entrantes du réseau: %4$.2f FPS\nTaux de rendu: %5$.2f FPS\nImages envoyé par votre connexion réseau: %6$.2f%%\nTemps moyen de réception: %7$.2f ms\nTemps de décodage moyen: %8$.2f ms</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="applist_connect_msg">Connexion au PC…</string>
|
||||
@@ -91,6 +94,7 @@
|
||||
<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_menu_tv_channel">Ajouter à la chaîne</string>
|
||||
<string name="applist_refresh_title">Liste des applications</string>
|
||||
<string name="applist_refresh_msg">Actualisation des applications…</string>
|
||||
<string name="applist_refresh_error_title">Erreur</string>
|
||||
@@ -181,5 +185,7 @@
|
||||
<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>
|
||||
|
||||
<string name="title_enable_perf_overlay">Activer la superposition de performance</string>
|
||||
<string name="summary_enable_perf_overlay">Afficher une superposition à l\'écran avec des informations de performance en temps réel pendant la lecture en continu</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
<string name="applist_menu_quit">Выйти из сессии</string>
|
||||
<string name="applist_menu_quit_and_start">Выйти из текущей игры и запустить</string>
|
||||
<string name="applist_menu_cancel">Отмена</string>
|
||||
<string name="applist_menu_tv_channel">Добавать на канал</string>
|
||||
<string name="applist_refresh_title">Список приложений</string>
|
||||
<string name="applist_refresh_msg">Обновление приложений…</string>
|
||||
<string name="applist_refresh_error_title">Ошибка</string>
|
||||
@@ -156,4 +157,30 @@
|
||||
<string name="title_enable_hdr">Включить HDR (Экспериментально)</string>
|
||||
<string name="summary_enable_hdr">Транслировать в HDR если игра и GPU компьютера поддерживают это. HDR требует видеокарты GTX 1000 серии или более новой.</string>
|
||||
|
||||
<string name="title_checkbox_vibrate_osc">Включить вибрацию</string>
|
||||
<string name="title_fps_list">Частота кадров</string>
|
||||
<string name="applist_menu_details">Детали</string>
|
||||
<string name="applist_menu_scut">Создать ярлык</string>
|
||||
<string name="category_input_settings">Настройки ввода</string>
|
||||
<string name="delete_pc_msg">Вы уверены что хотите удалить этот PC?</string>
|
||||
<string name="pcview_menu_details">Детали</string>
|
||||
<string name="poor_connection_msg">Слабое соединение с PC</string>
|
||||
<string name="title_details">Детали</string>
|
||||
<string name="title_enable_perf_overlay">Включить отображение статистики</string>
|
||||
<string name="title_unlock_fps">Разблокировать все возможные частоты обновления</string>
|
||||
<string name="applist_details_id">ID приложения:</string>
|
||||
<string name="title_checkbox_vibrate_fallback">Эмуляция виброотдачи</string>
|
||||
<string name="summary_checkbox_vibrate_osc">Вибрировать устройство для эмуляции виброотдачи при экранном управлении</string>
|
||||
<string name="summary_checkbox_vibrate_fallback">Вибрировать устройство для эмуляции виброотдачи для геймпадов без поддержки вибрации</string>
|
||||
<string name="summary_checkbox_mouse_nav_buttons">Включение этой опции может привести к неправильной работе правой кнопки мыши на некоторых устройствах</string>
|
||||
<string name="scut_pc_not_found">PC не найден</string>
|
||||
<string name="unable_to_pin_shortcut">Текущий лаунчер не позволяет создавать pinned ярлыки</string>
|
||||
<string name="title_checkbox_mouse_nav_buttons">Включить кнопки вперед и назад для мыши</string>
|
||||
<string name="slow_connection_msg">Медленное подключение к PC\nУменьшите битрейт</string>
|
||||
<string name="summary_unlock_fps">Трансляция со скоростью 90 или 120 кадров в секунду может уменьшить задержку на устройствах высокого класса, но может вызвать задержки или сбой на устройствах без поддержки этого функционала</string>
|
||||
<string name="summary_enable_perf_overlay">Отображение оверлея на экране с информацией о производительности во время трансляции в режиме реального времени</string>
|
||||
<string name="perf_overlay_text">Разрешение видео: %1$s\nДекодер: %2$s\nРасчетная частота кадров PC-хоста: %3$.2f FPS\nВходящая частота кадров из сети: %4$.2f FPS\nЧастота кадров при рендеринге: %5$.2f FPS\nОтброшеных кадров вашей сетью: %6$.2f%%\nСреднее время получения: %7$.2f ms\nСреднее время декодирования: %8$.2f ms</string>
|
||||
<string name="summary_fps_list">Увеличение для более плавного видео потока. Уменьшите для лучшей производительности на более слабых устройствах.</string>
|
||||
<string name="scut_invalid_uuid">Указанный PC недействителен</string>
|
||||
<string name="scut_invalid_app_id">Указанное приложение недействительно</string>
|
||||
</resources>
|
||||
|
||||
@@ -136,5 +136,28 @@
|
||||
<string name="category_advanced_settings"> 高级设置 </string>
|
||||
<string name="title_video_format"> H.265设置 </string>
|
||||
<string name="summary_video_format">H.265能降低带宽需求,但是需要设备支持 </string>
|
||||
<string name="applist_menu_scut">创建快捷方式</string>
|
||||
<string name="category_input_settings">输入设置</string>
|
||||
<string name="applist_menu_details">查看详情</string>
|
||||
<string name="dialog_text_reset_osc">你确定要删除所保存的按钮布局吗?</string>
|
||||
<string name="dialog_title_reset_osc">重置按钮布局</string>
|
||||
<string name="delete_pc_msg">你确定要删除这台电脑?</string>
|
||||
<string name="pcview_menu_details">查看详情</string>
|
||||
<string name="scut_pc_not_found">电脑无法找到</string>
|
||||
<string name="toast_reset_osc_success">按钮布局已经重置</string>
|
||||
<string name="title_fps_list">视频帧数</string>
|
||||
<string name="title_unlock_fps">解锁所有可用帧数</string>
|
||||
<string name="title_only_l3r3">只显示[L3]和[R3]</string>
|
||||
<string name="title_reset_osc">重置已经保存的触摸按钮布局</string>
|
||||
<string name="title_disable_frame_drop">永不掉帧</string>
|
||||
<string name="title_enable_hdr">启用 HDR (实验)</string>
|
||||
<string name="title_checkbox_vibrate_osc">启动震动</string>
|
||||
<string name="title_details">详情</string>
|
||||
<string name="title_decoding_reset">重置视频设置</string>
|
||||
<string name="title_checkbox_mouse_emulation">通过手柄模拟鼠标</string>
|
||||
<string name="summary_only_l3r3">隐藏所有虚拟按钮除了L3和R3</string>
|
||||
<string name="title_enable_perf_overlay">启用性能信息</string>
|
||||
<string name="summary_enable_perf_overlay">在串流中显示实时性能信息</string>
|
||||
<string name="perf_overlay_text">视频分辨率: %1$s\n解码器: %2$s\n估计主机帧数: %3$.2f FPS\n网络接收帧数: %4$.2f FPS\n渲染帧数: %5$.2f FPS\n网络丢失帧: %6$.2f%%\n平均接收时间: %7$.2f ms\n平均解码时间: %8$.2f ms</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -3,5 +3,7 @@
|
||||
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||
<dimen name="tv_channel_logo_width">80dp</dimen>
|
||||
<dimen name="tv_channel_logo_height">80dp</dimen>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
<string name="delete_pc_msg">Are you sure you want to delete this PC?</string>
|
||||
<string name="slow_connection_msg">Slow connection to PC\nReduce your bitrate</string>
|
||||
<string name="poor_connection_msg">Poor connection to PC</string>
|
||||
<string name="perf_overlay_text">Video dimensions: %1$s\nDecoder: %2$s\nEstimated host PC frame rate: %3$.2f FPS\nIncoming frame rate from network: %4$.2f FPS\nRendering frame rate: %5$.2f FPS\nFrames dropped by your network connection: %6$.2f%%\nAverage receive time: %7$.2f ms\nAverage decoding time: %8$.2f ms</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="applist_connect_msg">Connecting to PC…</string>
|
||||
@@ -95,6 +96,7 @@
|
||||
<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_menu_tv_channel">Add to Channel</string>
|
||||
<string name="applist_refresh_title">App List</string>
|
||||
<string name="applist_refresh_msg">Refreshing apps…</string>
|
||||
<string name="applist_refresh_error_title">Error</string>
|
||||
@@ -185,5 +187,7 @@
|
||||
<string name="summary_video_format">H.265 lowers video bandwidth requirements but requires a very recent device</string>
|
||||
<string name="title_enable_hdr">Enable HDR (Experimental)</string>
|
||||
<string name="summary_enable_hdr">Stream HDR when the game and PC GPU support it. HDR requires a GTX 1000 series GPU or later.</string>
|
||||
<string name="title_enable_perf_overlay">Enable performance overlay</string>
|
||||
<string name="summary_enable_perf_overlay">Display an on-screen overlay with real-time performance information while streaming</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -171,5 +171,10 @@
|
||||
android:title="@string/title_enable_hdr"
|
||||
android:summary="@string/summary_enable_hdr"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="checkbox_enable_perf_overlay"
|
||||
android:title="@string/title_enable_perf_overlay"
|
||||
android:summary="@string/summary_enable_perf_overlay"
|
||||
android:defaultValue="false"/>
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
|
||||
@@ -5,7 +5,7 @@ buildscript {
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.4.1'
|
||||
classpath 'com.android.tools.build:gradle:3.4.2'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
Diese App streamt Spiele, Programme oder den kompletten Desktop von NVIDIA GameStream-kompatiebelen PCs mit NVIDIA GeForce Experience über das Lokale Netzwerk oder Internet. Gleichzeitig werden Maus-, Tastatur- und Gamepad-Eingaben von deinem Android Gerät zum PC übertragen.
|
||||
|
||||
Die Streamingqualität kann aufgrund des verwendeten Android Geräts und der Netzwerkgegebenheiten variieren. HDR Unterstützung setzt ein HRD10-fähiges Gerät, eine GTX-1000-series GPU und HDR10-kompatibles Spiele voraus.
|
||||
|
||||
'''Merkmale'''
|
||||
* Open-source und komplett freie Software (keine Werbung, InApp-Käufe, oder "Pro" Version)
|
||||
* Streamt Spiele unabhängig davon in welchem Store diese gekauft wurden
|
||||
* Funktioniert im Heimnetzwerk oder über eine Internet bzw. LTE-Verbindung
|
||||
* Bis zu 4K HDR Streaming mit 120 FPS und 5.1 Sourround Sound
|
||||
* Tastatur- und Mausznterstützung (mit Android 8.0 oder mit Root-Rechten)
|
||||
* Unterstützt PS3, PS4, Xbox 360, Xbox One und Android Gamepads
|
||||
* Force Feedback Unterstützung
|
||||
* Kooperatives lokales Spielen mit bis zu 4 verbundenen Eingabegeräten
|
||||
* Maussteuerung via Gamepad durch langes gedrückt halten der Starttaste
|
||||
|
||||
'''PC Anforderungen'''
|
||||
* NVIDIA GeForce GTX/RTX Serie GPU (''GT-Serie und AMD GPUs werden nicht von NVIDIA GameStream unterstützt'')
|
||||
* Windows 7 oder neuer
|
||||
* NVIDIA GeForce Experience (GFE) 2.2.2 oder neuer
|
||||
|
||||
'''Anleitung zur Schnellkonfiguration'''
|
||||
# Stellen sie sicher dass GeForce Experience auf ihrem PC installiert ist. Aktivieren sie GameStream in den SHIELD Einstellungen.
|
||||
# Wählen Sie den PC in Moonlight aus und tippen sie den PIN auf ihrem PC ein.
|
||||
# Streamen sie los!
|
||||
|
||||
Für eine gutes Benutzungserlebnis benötigen sie einen mid- bist high-end WLAN-Router mit einer ungestörten Verbindung zu ihrem Android Gerät (5 GHz wird empfohlen) sowie eine gute Verbindung von ihrem PC zum Router (Ethernet empfohlen).
|
||||
|
||||
'''detaillierte Konfigurationsanleitung'''
|
||||
Besuchen die die vollständige Konfigurationsanleitung https://bit.ly/1skHFjN (Englisch) um:
|
||||
* Einen PC manuell hinzuzufügen (falls ihr PC nicht automatisch erkannt werden sollte)
|
||||
* Über das Internet bzw. LTE zu streamen
|
||||
* Eine Eingabegerät das direkt mit dem PC verbunden ist zu nutzen
|
||||
* Den kompletten Desktop zu streamen
|
||||
* Individuelle Apps zum Streamen hinzuzufügen
|
||||
@@ -0,0 +1 @@
|
||||
Spiele vom deinem PC auf Android spielen (nur NVIDIA)
|
||||
@@ -0,0 +1 @@
|
||||
Moonlight Spiele Streaming
|
||||
@@ -0,0 +1,5 @@
|
||||
- Updated to target Android Q SDK
|
||||
- Requested low latency WiFi behavior on Android Q
|
||||
- Fixed mouse capture in multi-window mode on Android Q
|
||||
- Updated visual styles to match gesture navigation on Android Q
|
||||
- Fixed USB driver issue that could cause player numbers to be wrong
|
||||
@@ -0,0 +1,7 @@
|
||||
- Added a performance overlay for real-time performance data
|
||||
- Added support for launching games directly from the Android TV homescreen
|
||||
- Added support for zero-configuration Internet streaming on IPv6 networks
|
||||
- Improved handling of home screen PC and game shortcuts
|
||||
- Fixed streaming from very old versions of GeForce Experience
|
||||
- Fixed deleting PCs with duplicate names
|
||||
- Updated Simplified Chinese translation
|
||||
@@ -0,0 +1,8 @@
|
||||
- Optimized edge-to-edge layout for Android Q
|
||||
- Fixed 5.1 surround sound not always working over the Internet
|
||||
- Fixed Android TV app icon on Android Pie
|
||||
- Improved reliability of public IP address detection
|
||||
- Enabled installation on external storage
|
||||
- Fixed on-screen overlays covering stream when in PiP mode
|
||||
- Fixed games never reappearing on Android TV channel if deleted once
|
||||
- Updated Russian and French translations
|
||||
@@ -0,0 +1,34 @@
|
||||
This app streams games, programs, or your full desktop from an NVIDIA GameStream-compatible PC on your local network or the Internet using NVIDIA GeForce Experience. Mouse, keyboard, and controller input is sent from your Android device to the PC.
|
||||
|
||||
Streaming performance may vary based on your client device and network setup. HDR requires an HDR10-capable device, GTX 1000-series GPU, and HDR10-enabled game.
|
||||
|
||||
'''Features'''
|
||||
* Open-source and completely free (no ads, IAPs, or "Pro")
|
||||
* Streams games purchased from any store
|
||||
* Works on your home network or over the Internet/LTE
|
||||
* Up to 4K 120 FPS HDR streaming with 5.1 surround sound
|
||||
* Keyboard and mouse support (with Android 8.0 or rooted device)
|
||||
* Supports PS3, PS4, Xbox 360, Xbox One, and Android gamepads
|
||||
* Force feedback support
|
||||
* Local co-op with up to 4 connected controllers
|
||||
* Mouse control via gamepad by long-pressing Start
|
||||
|
||||
'''PC Requirements'''
|
||||
* NVIDIA GeForce GTX/RTX series GPU (''GT-series and AMD GPUs aren't supported by NVIDIA GameStream'')
|
||||
* Windows 7 or later
|
||||
* NVIDIA GeForce Experience (GFE) 2.2.2 or later
|
||||
|
||||
'''Quick Setup Instructions'''
|
||||
# Make sure GeForce Experience is open on your PC. Turn on GameStream in the SHIELD settings page.
|
||||
# Tap on the PC in Moonlight and type the PIN on your PC
|
||||
# Start streaming!
|
||||
|
||||
To have a good experience, you need a mid to high-end wireless router with a good wireless connection to your Android device (5 GHz highly recommended) and a good connection from your PC to your router (Ethernet highly recommended).
|
||||
|
||||
'''Detailed Setup Instructions'''
|
||||
See the full setup guide https://bit.ly/1skHFjN for:
|
||||
* Adding a PC manually (if your PC is not detected)
|
||||
* Streaming over the Internet or LTE
|
||||
* Using a controller connected directly to your PC
|
||||
* Streaming your full desktop
|
||||
* Adding custom apps to stream
|
||||
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 97 KiB |
@@ -0,0 +1 @@
|
||||
Play games from your PC on Android (NVIDIA-only)
|
||||
@@ -0,0 +1 @@
|
||||
Moonlight Game Streaming
|
||||