Compare commits
188 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5519d92243 | |||
| 3d95ac1f93 | |||
| 5c938535be | |||
| 2fdecc551a | |||
| 10204afdb4 | |||
| 55c800c2a5 | |||
| 265b3f9963 | |||
| a8bf2cd1cf | |||
| 4fcd8b3dfe | |||
| e1a1a6344d | |||
| a095c10a25 | |||
| b1ea487e22 | |||
| 47265d0d10 | |||
| 6a41b41a38 | |||
| 2247e43a48 | |||
| d3986080a3 | |||
| 07277e1a5b | |||
| 39d7fc748f | |||
| 4d3a69cf6a | |||
| b806522751 | |||
| 256fa897a7 | |||
| 5c812eed6c | |||
| f0b22f9119 | |||
| 7e1884acb5 | |||
| 9512521783 | |||
| da7904a767 | |||
| 3a0c1db168 | |||
| bd21692323 | |||
| 5ae245bdca | |||
| d3052cd97d | |||
| 336f85a31c | |||
| b01f7c796e | |||
| 56f438fe47 | |||
| baa5199b83 | |||
| 23ca62b304 | |||
| 2c3511195c | |||
| d31ef481f3 | |||
| a490da5e5c | |||
| 72d3576257 | |||
| ebd93a55a0 | |||
| 4d01e1afe6 | |||
| 9ff1386751 | |||
| 5fca35f0b1 | |||
| d23c763441 | |||
| fa058c4783 | |||
| e0ddd5f045 | |||
| b7443451a4 | |||
| e90e4a22c4 | |||
| 3a53172145 | |||
| 1dfcb7bc29 | |||
| 897bb76858 | |||
| bcc67269ab | |||
| 4d24c654b9 | |||
| cba44b091b | |||
| f2d8f8a41b | |||
| 4b1c7e7e3c | |||
| 1cba278876 | |||
| 766898fdf9 | |||
| 13e91d594b | |||
| ca0a0da19f | |||
| 82cabce86e | |||
| 51a630995a | |||
| 3a74f0726c | |||
| efa6c7bba0 | |||
| b8141542f8 | |||
| 8fc9a90207 | |||
| 13d707d98d | |||
| aae0ff6e7a | |||
| 69c7b5a0d5 | |||
| d1ad3115fa | |||
| 770af402a4 | |||
| 3236c0b93a | |||
| 51aacc3f38 | |||
| 397c6f46f9 | |||
| d00f78f859 | |||
| 29fec2e0de | |||
| 88d28665ef | |||
| de1f4da258 | |||
| 7985be57ab | |||
| a835e7aaa2 | |||
| 22958cfbb1 | |||
| c4dc5eb9e1 | |||
| db758f386e | |||
| 3fb3eefa94 | |||
| 9340dff45d | |||
| 2d6c756e70 | |||
| 03e965d449 | |||
| 34f72544d8 | |||
| d839ea9781 | |||
| 2b7f13fdbb | |||
| 7557a3a4ae | |||
| fcecba484f | |||
| fa85a0a0bd | |||
| dc64bfeba2 | |||
| 871b73c48d | |||
| 5dcff91d27 | |||
| 0041fc1dab | |||
| 314242ab08 | |||
| 09e8ddfd74 | |||
| 4cea483a87 | |||
| 99aa616188 | |||
| 444c4602c1 | |||
| 5b6eac7140 | |||
| 7cdd184197 | |||
| be153b84cb | |||
| 06c53e2251 | |||
| 695519bdf5 | |||
| bf7d033ab2 | |||
| df67795c4a | |||
| 72c1696f43 | |||
| 8eca3683c9 | |||
| 80c17b4913 | |||
| e5050f10bb | |||
| e912e4de57 | |||
| 8dee1f0d80 | |||
| 53594ada66 | |||
| 848ed1ad72 | |||
| 307e807c8f | |||
| 6a27780d56 | |||
| 57f98dbb4a | |||
| 5af7d83ec1 | |||
| 4a6f77f43a | |||
| c96f9fb635 | |||
| e3a477a243 | |||
| 9fcd641143 | |||
| 6d1cbc5a64 | |||
| ec71060d98 | |||
| 03f706fb85 | |||
| 7ad87bd3ee | |||
| 4e088f6183 | |||
| 1b16ea6f53 | |||
| f262503bc8 | |||
| b2ba216cd1 | |||
| 94ba7f8e45 | |||
| a267cf59c7 | |||
| 79e8bef289 | |||
| 99e3b5f33b | |||
| afbe64f3ff | |||
| 43b1a73ae0 | |||
| d08eeb8a2d | |||
| 7c39e5c974 | |||
| cd49334199 | |||
| dd59f0bc6d | |||
| cf2d83a1ea | |||
| d5b6130936 | |||
| 4ae29b0075 | |||
| 34e35cd493 | |||
| a17af070c5 | |||
| fbe0a26800 | |||
| 25ad99df94 | |||
| 6338e7b8eb | |||
| 1b9846d519 | |||
| a4ece13a1d | |||
| 066b8430a0 | |||
| 2b54a91f3d | |||
| 2d01633372 | |||
| 5dc01069fc | |||
| d450008833 | |||
| a37fff6eb5 | |||
| 6604675bf9 | |||
| 1965cc2347 | |||
| 312ca27906 | |||
| 0bceadbd9a | |||
| dfc3daabcd | |||
| b9ba9adc1f | |||
| f112d45e1a | |||
| 88f139873c | |||
| d317c5bf03 | |||
| 9d72314b9c | |||
| 2cc7243573 | |||
| 269d9a6bc6 | |||
| 244130fc1b | |||
| a67791b8aa | |||
| 21e46a5c3b | |||
| 2df2f850d5 | |||
| 406d26ec1c | |||
| 68c1aaf433 | |||
| 9ef577dbdd | |||
| 982ecbc015 | |||
| 7e44b5abd5 | |||
| 6dbb1a0c1f | |||
| 94b1c04fa6 | |||
| 9758276f1c | |||
| 971263c52d | |||
| 9b58e7bb4d | |||
| 69ecf0251d | |||
| 350a4d8825 | |||
| 44f447df7b |
@@ -8,6 +8,8 @@ whether in your own home or over the internet.
|
||||
|
||||
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development.
|
||||
|
||||
Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for more detailed information or a troubleshooting guide.
|
||||
|
||||
##Features
|
||||
|
||||
* Streams any of your games from your PC to your Android device
|
||||
|
||||
+11
-1
@@ -9,6 +9,7 @@
|
||||
<facet type="android" name="Android">
|
||||
<configuration>
|
||||
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
|
||||
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
|
||||
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
|
||||
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
|
||||
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" />
|
||||
@@ -24,6 +25,7 @@
|
||||
</component>
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="false">
|
||||
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
|
||||
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/test/nonRoot/debug" />
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
|
||||
@@ -31,6 +33,7 @@
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/nonRoot/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/nonRoot/debug" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" />
|
||||
@@ -43,6 +46,7 @@
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/nonRoot/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/nonRoot/debug" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/test/nonRoot/debug" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
|
||||
@@ -102,11 +106,17 @@
|
||||
<orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
|
||||
<orderEntry type="library" exported="" name="androidasync-2.0.5" level="project" />
|
||||
<orderEntry type="library" exported="" name="gson-2.3.1" level="project" />
|
||||
<orderEntry type="library" exported="" name="support-v4-r7" level="project" />
|
||||
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
|
||||
<orderEntry type="library" exported="" name="jcodec-0.1.6-3" level="project" />
|
||||
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" />
|
||||
<orderEntry type="library" exported="" name="tinyrtsp" level="project" />
|
||||
<orderEntry type="library" exported="" name="limelight-common" level="project" />
|
||||
<orderEntry type="library" exported="" name="ion-2.0.5" level="project" />
|
||||
<orderEntry type="library" exported="" name="okhttp-2.2.0" level="project" />
|
||||
<orderEntry type="library" exported="" name="jcodec-0.1.9" level="project" />
|
||||
<orderEntry type="library" exported="" name="okio-1.2.0" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
|
||||
|
||||
+14
-5
@@ -5,14 +5,14 @@ apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 21
|
||||
buildToolsVersion "21.0.2"
|
||||
buildToolsVersion "21.1.2"
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 21
|
||||
|
||||
versionName "2.9"
|
||||
versionCode = 38
|
||||
versionName "3.1"
|
||||
versionCode = 53
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
@@ -27,7 +27,7 @@ android {
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
runProguard false
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
}
|
||||
@@ -62,9 +62,18 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile group: 'org.jcodec', name: 'jcodec', version: '0.1.6-3'
|
||||
compile group: 'org.jcodec', name: 'jcodec', version: '0.1.9'
|
||||
|
||||
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
|
||||
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
|
||||
|
||||
compile group: 'com.google.android', name: 'support-v4', version:'r7'
|
||||
compile group: 'com.koushikdutta.ion', name: 'ion', version:'2.0.5'
|
||||
compile group: 'com.google.code.gson', name: 'gson', version:'2.3.1'
|
||||
|
||||
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.2.0'
|
||||
compile group: 'com.squareup.okio', name:'okio', version:'1.2.0'
|
||||
|
||||
compile files('libs/jmdns-fixed.jar')
|
||||
compile files('libs/limelight-common.jar')
|
||||
compile files('libs/tinyrtsp.jar')
|
||||
|
||||
Binary file not shown.
@@ -64,10 +64,11 @@
|
||||
<activity
|
||||
android:name=".Game"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:theme="@style/StreamTheme"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.Connection" />
|
||||
android:value="com.limelight.AppView" />
|
||||
</activity>
|
||||
<service
|
||||
android:name=".discovery.DiscoveryService"
|
||||
|
||||
@@ -1,295 +1,512 @@
|
||||
package com.limelight;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.List;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.nvstream.http.GfeHttpResponseException;
|
||||
import com.limelight.computers.ComputerManagerListener;
|
||||
import com.limelight.computers.ComputerManagerService;
|
||||
import com.limelight.grid.AppGridAdapter;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
import com.limelight.R;
|
||||
import com.limelight.preferences.PreferenceConfiguration;
|
||||
import com.limelight.ui.AdapterFragment;
|
||||
import com.limelight.ui.AdapterFragmentCallbacks;
|
||||
import com.limelight.utils.CacheHelper;
|
||||
import com.limelight.utils.Dialog;
|
||||
import com.limelight.utils.SpinnerDialog;
|
||||
import com.limelight.utils.UiHelper;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||
|
||||
public class AppView extends Activity {
|
||||
private ListView appList;
|
||||
private ArrayAdapter<AppObject> appListAdapter;
|
||||
private InetAddress ipAddress;
|
||||
private String uniqueId;
|
||||
private boolean remote;
|
||||
|
||||
private final static int RESUME_ID = 1;
|
||||
private final static int QUIT_ID = 2;
|
||||
private final static int CANCEL_ID = 3;
|
||||
|
||||
public final static String ADDRESS_EXTRA = "Address";
|
||||
public final static String UNIQUEID_EXTRA = "UniqueId";
|
||||
public final static String NAME_EXTRA = "Name";
|
||||
public final static String REMOTE_EXTRA = "Remote";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_app_view);
|
||||
|
||||
byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA);
|
||||
uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA);
|
||||
remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false);
|
||||
if (address == null || uniqueId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String labelText = "App List for "+getIntent().getStringExtra(NAME_EXTRA);
|
||||
TextView label = (TextView) findViewById(R.id.appListText);
|
||||
setTitle(labelText);
|
||||
label.setText(labelText);
|
||||
|
||||
try {
|
||||
ipAddress = InetAddress.getByAddress(address);
|
||||
} catch (UnknownHostException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup the list view
|
||||
appList = (ListView)findViewById(R.id.pcListView);
|
||||
appListAdapter = new ArrayAdapter<AppObject>(this, R.layout.simplerow, R.id.rowTextView);
|
||||
appListAdapter.setNotifyOnChange(false);
|
||||
appList.setAdapter(appListAdapter);
|
||||
appList.setItemsCanFocus(true);
|
||||
appList.setOnItemClickListener(new OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||
long id) {
|
||||
AppObject app = appListAdapter.getItem(pos);
|
||||
if (app == null || app.app == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only open the context menu if something is running, otherwise start it
|
||||
if (getRunningAppId() != -1) {
|
||||
openContextMenu(arg1);
|
||||
}
|
||||
else {
|
||||
doStart(app.app);
|
||||
}
|
||||
}
|
||||
});
|
||||
registerForContextMenu(appList);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
SpinnerDialog.closeDialogs(this);
|
||||
Dialog.closeDialogs();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
updateAppList();
|
||||
}
|
||||
|
||||
private int getRunningAppId() {
|
||||
public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
private AppGridAdapter appGridAdapter;
|
||||
private String uuidString;
|
||||
|
||||
private ComputerDetails computer;
|
||||
private ComputerManagerService.ApplistPoller poller;
|
||||
private SpinnerDialog blockingLoadSpinner;
|
||||
private String lastRawApplist;
|
||||
|
||||
private int consecutiveAppListFailures = 0;
|
||||
private final static int CONSECUTIVE_FAILURE_LIMIT = 3;
|
||||
|
||||
private final static int START_OR_RESUME_ID = 1;
|
||||
private final static int QUIT_ID = 2;
|
||||
private final static int CANCEL_ID = 3;
|
||||
private final static int START_WTIH_QUIT = 4;
|
||||
|
||||
public final static String NAME_EXTRA = "Name";
|
||||
public final static String UUID_EXTRA = "UUID";
|
||||
|
||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||
final ComputerManagerService.ComputerManagerBinder localBinder =
|
||||
((ComputerManagerService.ComputerManagerBinder)binder);
|
||||
|
||||
// Wait in a separate thread to avoid stalling the UI
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Wait for the binder to be ready
|
||||
localBinder.waitForReady();
|
||||
|
||||
// Now make the binder visible
|
||||
managerBinder = localBinder;
|
||||
|
||||
// Get the computer object
|
||||
computer = managerBinder.getComputer(UUID.fromString(uuidString));
|
||||
|
||||
try {
|
||||
appGridAdapter = new AppGridAdapter(AppView.this,
|
||||
PreferenceConfiguration.readPreferences(AppView.this).listMode,
|
||||
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
|
||||
computer, managerBinder.getUniqueId());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start updates
|
||||
startComputerUpdates();
|
||||
|
||||
// Load the app grid with cached data (if possible)
|
||||
populateAppGridWithCache();
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.appFragmentContainer, new AdapterFragment())
|
||||
.commitAllowingStateLoss();
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
public void onServiceDisconnected(ComponentName className) {
|
||||
managerBinder = null;
|
||||
}
|
||||
};
|
||||
|
||||
private InetAddress getAddress() {
|
||||
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||
computer.localIp : computer.remoteIp;
|
||||
}
|
||||
|
||||
private void startComputerUpdates() {
|
||||
if (managerBinder == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
managerBinder.startPolling(new ComputerManagerListener() {
|
||||
@Override
|
||||
public void notifyComputerUpdated(ComputerDetails details) {
|
||||
// Don't care about other computers
|
||||
if (details != computer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (details.state != ComputerDetails.State.ONLINE) {
|
||||
consecutiveAppListFailures++;
|
||||
|
||||
if (consecutiveAppListFailures >= CONSECUTIVE_FAILURE_LIMIT) {
|
||||
// The PC is unreachable now
|
||||
AppView.this.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Display a toast to the user and quit the activity
|
||||
Toast.makeText(AppView.this, getResources().getText(R.string.lost_connection), Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
consecutiveAppListFailures = 0;
|
||||
|
||||
// App list is the same or empty; nothing to do
|
||||
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
lastRawApplist = details.rawAppList;
|
||||
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
|
||||
|
||||
if (blockingLoadSpinner != null) {
|
||||
blockingLoadSpinner.dismiss();
|
||||
blockingLoadSpinner = null;
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
});
|
||||
|
||||
if (poller == null) {
|
||||
poller = managerBinder.createAppListPoller(computer);
|
||||
}
|
||||
poller.start();
|
||||
}
|
||||
|
||||
private void stopComputerUpdates() {
|
||||
if (poller != null) {
|
||||
poller.stop();
|
||||
}
|
||||
|
||||
if (managerBinder != null) {
|
||||
managerBinder.stopPolling();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
String locale = PreferenceConfiguration.readPreferences(this).language;
|
||||
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||
Configuration config = new Configuration(getResources().getConfiguration());
|
||||
config.locale = new Locale(locale);
|
||||
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_app_view);
|
||||
|
||||
UiHelper.notifyNewRootView(this);
|
||||
|
||||
uuidString = getIntent().getStringExtra(UUID_EXTRA);
|
||||
|
||||
String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA);
|
||||
TextView label = (TextView) findViewById(R.id.appListText);
|
||||
setTitle(labelText);
|
||||
label.setText(labelText);
|
||||
|
||||
// Bind to the computer manager service
|
||||
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
||||
Service.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
private void populateAppGridWithCache() {
|
||||
try {
|
||||
// Try to load from cache
|
||||
lastRawApplist = CacheHelper.readInputStreamToString(CacheHelper.openCacheFileForInput(getCacheDir(), "applist", uuidString));
|
||||
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
|
||||
updateUiWithAppList(applist);
|
||||
LimeLog.info("Loaded applist from cache");
|
||||
} catch (Exception e) {
|
||||
if (lastRawApplist != null) {
|
||||
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
|
||||
e.printStackTrace();
|
||||
}
|
||||
LimeLog.info("Loading applist from the network");
|
||||
// We'll need to load from the network
|
||||
loadAppsBlocking();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadAppsBlocking() {
|
||||
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
|
||||
getResources().getString(R.string.applist_refresh_msg), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
SpinnerDialog.closeDialogs(this);
|
||||
Dialog.closeDialogs();
|
||||
|
||||
if (managerBinder != null) {
|
||||
unbindService(serviceConnection);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
startComputerUpdates();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
|
||||
stopComputerUpdates();
|
||||
}
|
||||
|
||||
private int getRunningAppId() {
|
||||
int runningAppId = -1;
|
||||
for (int i = 0; i < appListAdapter.getCount(); i++) {
|
||||
AppObject app = appListAdapter.getItem(i);
|
||||
if (app.app == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (app.app.getIsRunning()) {
|
||||
runningAppId = app.app.getAppId();
|
||||
break;
|
||||
}
|
||||
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||
AppObject app = (AppObject) appGridAdapter.getItem(i);
|
||||
if (app.app == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (app.app.getIsRunning()) {
|
||||
runningAppId = app.app.getAppId();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return runningAppId;
|
||||
}
|
||||
|
||||
@Override
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||
super.onCreateContextMenu(menu, v, menuInfo);
|
||||
super.onCreateContextMenu(menu, v, menuInfo);
|
||||
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||
AppObject selectedApp = appListAdapter.getItem(info.position);
|
||||
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
|
||||
if (selectedApp == null || selectedApp.app == null) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
int runningAppId = getRunningAppId();
|
||||
if (runningAppId != -1) {
|
||||
if (runningAppId == selectedApp.app.getAppId()) {
|
||||
menu.add(Menu.NONE, RESUME_ID, 1, "Resume Session");
|
||||
menu.add(Menu.NONE, QUIT_ID, 2, "Quit Session");
|
||||
}
|
||||
else {
|
||||
menu.add(Menu.NONE, RESUME_ID, 1, "Quit Current Game and Start");
|
||||
menu.add(Menu.NONE, CANCEL_ID, 2, "Cancel");
|
||||
}
|
||||
if (runningAppId == selectedApp.app.getAppId()) {
|
||||
menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
|
||||
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
||||
}
|
||||
else {
|
||||
menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
|
||||
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContextMenuClosed(Menu menu) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContextMenuClosed(Menu menu) {
|
||||
}
|
||||
|
||||
private void displayQuitConfirmationDialog(final Runnable onYes, final Runnable onNo) {
|
||||
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which){
|
||||
case DialogInterface.BUTTON_POSITIVE:
|
||||
if (onYes != null) {
|
||||
onYes.run();
|
||||
}
|
||||
break;
|
||||
|
||||
case DialogInterface.BUTTON_NEGATIVE:
|
||||
if (onNo != null) {
|
||||
onNo.run();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setMessage(getResources().getString(R.string.applist_quit_confirmation))
|
||||
.setPositiveButton(getResources().getString(R.string.yes), dialogClickListener)
|
||||
.setNegativeButton(getResources().getString(R.string.no), dialogClickListener)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||
AppObject app = appListAdapter.getItem(info.position);
|
||||
switch (item.getItemId())
|
||||
{
|
||||
case RESUME_ID:
|
||||
// Resume is the same as start for us
|
||||
doStart(app.app);
|
||||
return true;
|
||||
|
||||
case QUIT_ID:
|
||||
doQuit(app.app);
|
||||
return true;
|
||||
|
||||
case CANCEL_ID:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return super.onContextItemSelected(item);
|
||||
final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
|
||||
switch (item.getItemId()) {
|
||||
case START_WTIH_QUIT:
|
||||
// Display a confirmation dialog first
|
||||
displayQuitConfirmationDialog(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
doStart(app.app);
|
||||
}
|
||||
}, null);
|
||||
return true;
|
||||
|
||||
case START_OR_RESUME_ID:
|
||||
// Resume is the same as start for us
|
||||
doStart(app.app);
|
||||
return true;
|
||||
|
||||
case QUIT_ID:
|
||||
// Display a confirmation dialog first
|
||||
displayQuitConfirmationDialog(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
doQuit(app.app);
|
||||
}
|
||||
}, null);
|
||||
return true;
|
||||
|
||||
case CANCEL_ID:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private static String generateString(NvApp app) {
|
||||
StringBuilder str = new StringBuilder();
|
||||
str.append(app.getAppName());
|
||||
if (app.getIsRunning()) {
|
||||
str.append(" - Running");
|
||||
}
|
||||
return str.toString();
|
||||
private void updateUiWithAppList(final List<NvApp> appList) {
|
||||
AppView.this.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
boolean updated = false;
|
||||
|
||||
for (NvApp app : appList) {
|
||||
boolean foundExistingApp = false;
|
||||
|
||||
// Try to update an existing app in the list first
|
||||
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||
if (existingApp.app == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existingApp.app.getAppId() == app.getAppId()) {
|
||||
// Found the app; update its properties
|
||||
if (existingApp.app.getIsRunning() != app.getIsRunning()) {
|
||||
existingApp.app.setIsRunningBoolean(app.getIsRunning());
|
||||
updated = true;
|
||||
}
|
||||
if (!existingApp.app.getAppName().equals(app.getAppName())) {
|
||||
existingApp.app.setAppName(app.getAppName());
|
||||
updated = true;
|
||||
}
|
||||
|
||||
foundExistingApp = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundExistingApp) {
|
||||
// This app must be new
|
||||
appGridAdapter.addApp(new AppObject(app));
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
appGridAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addListPlaceholder() {
|
||||
appListAdapter.add(new AppObject("No apps found. Try rescanning for games in GeForce Experience.", null));
|
||||
|
||||
private void doStart(NvApp app) {
|
||||
Intent intent = new Intent(this, Game.class);
|
||||
intent.putExtra(Game.EXTRA_HOST,
|
||||
computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
|
||||
intent.putExtra(Game.EXTRA_APP, app.getAppName());
|
||||
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
||||
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
||||
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void updateAppList() {
|
||||
final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, "App List", "Refreshing app list...", true);
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
NvHTTP httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
|
||||
|
||||
try {
|
||||
final List<NvApp> appList = httpConn.getAppList();
|
||||
|
||||
AppView.this.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
appListAdapter.clear();
|
||||
if (appList.isEmpty()) {
|
||||
addListPlaceholder();
|
||||
}
|
||||
else {
|
||||
for (NvApp app : appList) {
|
||||
appListAdapter.add(new AppObject(generateString(app), app));
|
||||
}
|
||||
}
|
||||
|
||||
appListAdapter.notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
|
||||
// Success case
|
||||
return;
|
||||
} catch (GfeHttpResponseException ignored) {
|
||||
} catch (IOException ignored) {
|
||||
} catch (XmlPullParserException ignored) {
|
||||
} finally {
|
||||
spinner.dismiss();
|
||||
}
|
||||
|
||||
Dialog.displayDialog(AppView.this, "Error", "Failed to get app list", true);
|
||||
}
|
||||
}.start();
|
||||
|
||||
private void doQuit(final NvApp app) {
|
||||
Toast.makeText(AppView.this, getResources().getString(R.string.applist_quit_app)+" "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
NvHTTP httpConn;
|
||||
String message;
|
||||
try {
|
||||
httpConn = new NvHTTP(getAddress(),
|
||||
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(AppView.this));
|
||||
if (httpConn.quitApp()) {
|
||||
message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName();
|
||||
}
|
||||
else {
|
||||
message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName();
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
message = getResources().getString(R.string.error_unknown_host);
|
||||
} catch (FileNotFoundException e) {
|
||||
message = getResources().getString(R.string.error_404);
|
||||
} catch (Exception e) {
|
||||
message = e.getMessage();
|
||||
} finally {
|
||||
// Trigger a poll immediately
|
||||
if (poller != null) {
|
||||
poller.pollNow();
|
||||
}
|
||||
}
|
||||
|
||||
final String toastMessage = message;
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void doStart(NvApp app) {
|
||||
Intent intent = new Intent(this, Game.class);
|
||||
intent.putExtra(Game.EXTRA_HOST, ipAddress.getHostAddress());
|
||||
intent.putExtra(Game.EXTRA_APP, app.getAppName());
|
||||
intent.putExtra(Game.EXTRA_UNIQUEID, uniqueId);
|
||||
intent.putExtra(Game.EXTRA_STREAMING_REMOTE, remote);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void doQuit(final NvApp app) {
|
||||
Toast.makeText(AppView.this, "Quitting "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
NvHTTP httpConn;
|
||||
String message;
|
||||
try {
|
||||
httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
|
||||
if (httpConn.quitApp()) {
|
||||
message = "Successfully quit "+app.getAppName();
|
||||
}
|
||||
else {
|
||||
message = "Failed to quit "+app.getAppName();
|
||||
}
|
||||
updateAppList();
|
||||
} catch (UnknownHostException e) {
|
||||
message = "Failed to resolve host";
|
||||
} catch (FileNotFoundException e) {
|
||||
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
|
||||
+ "Try rebooting your machine or reinstalling GFE.";
|
||||
} catch (Exception e) {
|
||||
message = e.getMessage();
|
||||
}
|
||||
|
||||
final String toastMessage = message;
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public class AppObject {
|
||||
public String text;
|
||||
public NvApp app;
|
||||
|
||||
public AppObject(String text, NvApp app) {
|
||||
this.text = text;
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAdapterFragmentLayoutId() {
|
||||
return PreferenceConfiguration.readPreferences(this).listMode ?
|
||||
R.layout.list_view : (PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
|
||||
R.layout.app_grid_view_small : R.layout.app_grid_view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveAbsListView(AbsListView listView) {
|
||||
listView.setAdapter(appGridAdapter);
|
||||
listView.setOnItemClickListener(new OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||
long id) {
|
||||
AppObject app = (AppObject) appGridAdapter.getItem(pos);
|
||||
if (app == null || app.app == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only open the context menu if something is running, otherwise start it
|
||||
if (getRunningAppId() != -1) {
|
||||
openContextMenu(arg1);
|
||||
} else {
|
||||
doStart(app.app);
|
||||
}
|
||||
}
|
||||
});
|
||||
registerForContextMenu(listView);
|
||||
listView.requestFocus();
|
||||
}
|
||||
|
||||
public class AppObject {
|
||||
public final NvApp app;
|
||||
|
||||
public AppObject(NvApp app) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return app.getAppName();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,17 +8,17 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||
|
||||
public class PlatformBinding {
|
||||
public static String getDeviceName() {
|
||||
String deviceName = android.os.Build.MODEL;
|
||||
public static String getDeviceName() {
|
||||
String deviceName = android.os.Build.MODEL;
|
||||
deviceName = deviceName.replace(" ", "");
|
||||
return deviceName;
|
||||
}
|
||||
|
||||
public static AudioRenderer getAudioRenderer() {
|
||||
return new AndroidAudioRenderer();
|
||||
}
|
||||
|
||||
public static LimelightCryptoProvider getCryptoProvider(Context c) {
|
||||
return new AndroidCryptoProvider(c);
|
||||
}
|
||||
}
|
||||
|
||||
public static AudioRenderer getAudioRenderer() {
|
||||
return new AndroidAudioRenderer();
|
||||
}
|
||||
|
||||
public static LimelightCryptoProvider getCryptoProvider(Context c) {
|
||||
return new AndroidCryptoProvider(c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,62 +9,89 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||
|
||||
public class AndroidAudioRenderer implements AudioRenderer {
|
||||
|
||||
public static final int FRAME_SIZE = 960;
|
||||
|
||||
private AudioTrack track;
|
||||
private static final int FRAME_SIZE = 960;
|
||||
|
||||
@Override
|
||||
public boolean streamInitialized(int channelCount, int sampleRate) {
|
||||
int channelConfig;
|
||||
int bufferSize;
|
||||
private AudioTrack track;
|
||||
|
||||
switch (channelCount)
|
||||
{
|
||||
case 1:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
|
||||
break;
|
||||
case 2:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||
break;
|
||||
default:
|
||||
LimeLog.severe("Decoder returned unhandled channel count");
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public boolean streamInitialized(int channelCount, int sampleRate) {
|
||||
int channelConfig;
|
||||
int bufferSize;
|
||||
|
||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
|
||||
channelConfig,
|
||||
AudioFormat.ENCODING_PCM_16BIT),
|
||||
FRAME_SIZE * 2);
|
||||
|
||||
// Round to next frame
|
||||
bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE);
|
||||
|
||||
LimeLog.info("Audio track buffer size: "+bufferSize);
|
||||
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||
sampleRate,
|
||||
channelConfig,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
bufferSize,
|
||||
AudioTrack.MODE_STREAM);
|
||||
switch (channelCount)
|
||||
{
|
||||
case 1:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
|
||||
break;
|
||||
case 2:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||
break;
|
||||
default:
|
||||
LimeLog.severe("Decoder returned unhandled channel count");
|
||||
return false;
|
||||
}
|
||||
|
||||
track.play();
|
||||
return true;
|
||||
}
|
||||
// We're not supposed to request less than the minimum
|
||||
// buffer size for our buffer, but it appears that we can
|
||||
// do this on many devices and it lowers audio latency.
|
||||
// We'll try the small buffer size first and if it fails,
|
||||
// use the recommended larger buffer size.
|
||||
try {
|
||||
// Buffer two frames of audio if possible
|
||||
bufferSize = FRAME_SIZE * 2;
|
||||
|
||||
@Override
|
||||
public void playDecodedAudio(byte[] audioData, int offset, int length) {
|
||||
track.write(audioData, offset, length);
|
||||
}
|
||||
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||
sampleRate,
|
||||
channelConfig,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
bufferSize,
|
||||
AudioTrack.MODE_STREAM);
|
||||
track.play();
|
||||
} catch (Exception e) {
|
||||
// Try to release the AudioTrack if we got far enough
|
||||
try {
|
||||
if (track != null) {
|
||||
track.release();
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
@Override
|
||||
public void streamClosing() {
|
||||
if (track != null) {
|
||||
track.release();
|
||||
}
|
||||
}
|
||||
// Now try the larger buffer size
|
||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
|
||||
channelConfig,
|
||||
AudioFormat.ENCODING_PCM_16BIT),
|
||||
FRAME_SIZE * 2);
|
||||
|
||||
@Override
|
||||
public int getCapabilities() {
|
||||
return 0;
|
||||
}
|
||||
// Round to next frame
|
||||
bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE);
|
||||
|
||||
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||
sampleRate,
|
||||
channelConfig,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
bufferSize,
|
||||
AudioTrack.MODE_STREAM);
|
||||
track.play();
|
||||
}
|
||||
|
||||
LimeLog.info("Audio track buffer size: "+bufferSize);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playDecodedAudio(byte[] audioData, int offset, int length) {
|
||||
track.write(audioData, offset, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void streamClosing() {
|
||||
if (track != null) {
|
||||
track.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCapabilities() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,239 +45,239 @@ import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||
|
||||
public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
|
||||
private File certFile;
|
||||
private File keyFile;
|
||||
|
||||
private X509Certificate cert;
|
||||
private RSAPrivateKey key;
|
||||
private byte[] pemCertBytes;
|
||||
|
||||
private static final Object globalCryptoLock = new Object();
|
||||
|
||||
static {
|
||||
// Install the Bouncy Castle provider
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
public AndroidCryptoProvider(Context c) {
|
||||
String dataPath = c.getFilesDir().getAbsolutePath();
|
||||
|
||||
certFile = new File(dataPath + File.separator + "client.crt");
|
||||
keyFile = new File(dataPath + File.separator + "client.key");
|
||||
}
|
||||
|
||||
private byte[] loadFileToBytes(File f) {
|
||||
if (!f.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
FileInputStream fin = new FileInputStream(f);
|
||||
byte[] fileData = new byte[(int) f.length()];
|
||||
if (fin.read(fileData) != f.length()) {
|
||||
private final File certFile;
|
||||
private final File keyFile;
|
||||
|
||||
private X509Certificate cert;
|
||||
private RSAPrivateKey key;
|
||||
private byte[] pemCertBytes;
|
||||
|
||||
private static final Object globalCryptoLock = new Object();
|
||||
|
||||
static {
|
||||
// Install the Bouncy Castle provider
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
public AndroidCryptoProvider(Context c) {
|
||||
String dataPath = c.getFilesDir().getAbsolutePath();
|
||||
|
||||
certFile = new File(dataPath + File.separator + "client.crt");
|
||||
keyFile = new File(dataPath + File.separator + "client.key");
|
||||
}
|
||||
|
||||
private byte[] loadFileToBytes(File f) {
|
||||
if (!f.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
FileInputStream fin = new FileInputStream(f);
|
||||
byte[] fileData = new byte[(int) f.length()];
|
||||
if (fin.read(fileData) != f.length()) {
|
||||
// Failed to read
|
||||
fileData = null;
|
||||
}
|
||||
fin.close();
|
||||
return fileData;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean loadCertKeyPair() {
|
||||
byte[] certBytes = loadFileToBytes(certFile);
|
||||
byte[] keyBytes = loadFileToBytes(keyFile);
|
||||
|
||||
// If either file was missing, we definitely can't succeed
|
||||
if (certBytes == null || keyBytes == null) {
|
||||
LimeLog.info("Missing cert or key; need to generate a new one");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
|
||||
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||
pemCertBytes = certBytes;
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
|
||||
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
||||
} catch (CertificateException e) {
|
||||
// May happen if the cert is corrupt
|
||||
LimeLog.warning("Corrupted certificate");
|
||||
return false;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// Should never happen
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
} catch (InvalidKeySpecException e) {
|
||||
// May happen if the key is corrupt
|
||||
LimeLog.warning("Corrupted key");
|
||||
return false;
|
||||
} catch (NoSuchProviderException e) {
|
||||
// Should never happen
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressLint("TrulyRandom")
|
||||
private boolean generateCertKeyPair() {
|
||||
byte[] snBytes = new byte[8];
|
||||
new SecureRandom().nextBytes(snBytes);
|
||||
|
||||
KeyPair keyPair;
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
||||
keyPairGenerator.initialize(2048);
|
||||
keyPair = keyPairGenerator.generateKeyPair();
|
||||
} catch (NoSuchAlgorithmException e1) {
|
||||
// Should never happen
|
||||
e1.printStackTrace();
|
||||
return false;
|
||||
} catch (NoSuchProviderException e) {
|
||||
// Should never happen
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
|
||||
// Expires in 20 years
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTime(now);
|
||||
calendar.add(Calendar.YEAR, 20);
|
||||
Date expirationDate = calendar.getTime();
|
||||
|
||||
BigInteger serial = new BigInteger(snBytes).abs();
|
||||
|
||||
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
|
||||
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
|
||||
X500Name name = nameBuilder.build();
|
||||
|
||||
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
|
||||
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
||||
|
||||
try {
|
||||
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
|
||||
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
|
||||
key = (RSAPrivateKey) keyPair.getPrivate();
|
||||
} catch (Exception e) {
|
||||
// Nothing should go wrong here
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
LimeLog.info("Generated a new key pair");
|
||||
|
||||
// Save the resulting pair
|
||||
saveCertKeyPair();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void saveCertKeyPair() {
|
||||
try {
|
||||
FileOutputStream certOut = new FileOutputStream(certFile);
|
||||
FileOutputStream keyOut = new FileOutputStream(keyFile);
|
||||
|
||||
// Write the certificate in OpenSSL PEM format (important for the server)
|
||||
StringWriter strWriter = new StringWriter();
|
||||
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
|
||||
pemWriter.writeObject(cert);
|
||||
pemWriter.close();
|
||||
|
||||
// Line endings MUST be UNIX for the PC to accept the cert properly
|
||||
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
|
||||
String pemStr = strWriter.getBuffer().toString();
|
||||
for (int i = 0; i < pemStr.length(); i++) {
|
||||
char c = pemStr.charAt(i);
|
||||
if (c != '\r')
|
||||
certWriter.append(c);
|
||||
}
|
||||
certWriter.close();
|
||||
|
||||
// Write the private out in PKCS8 format
|
||||
keyOut.write(key.getEncoded());
|
||||
|
||||
certOut.close();
|
||||
keyOut.close();
|
||||
|
||||
LimeLog.info("Saved generated key pair to disk");
|
||||
} catch (IOException e) {
|
||||
// This isn't good because it means we'll have
|
||||
// to re-pair next time
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public X509Certificate getClientCertificate() {
|
||||
// Use a lock here to ensure only one guy will be generating or loading
|
||||
// the certificate and key at a time
|
||||
synchronized (globalCryptoLock) {
|
||||
// Return a loaded cert if we have one
|
||||
if (cert != null) {
|
||||
return cert;
|
||||
}
|
||||
|
||||
// No loaded cert yet, let's see if we have one on disk
|
||||
if (loadCertKeyPair()) {
|
||||
// Got one
|
||||
return cert;
|
||||
}
|
||||
|
||||
// Try to generate a new key pair
|
||||
if (!generateCertKeyPair()) {
|
||||
// Failed
|
||||
return null;
|
||||
}
|
||||
|
||||
// Load the generated pair
|
||||
loadCertKeyPair();
|
||||
return cert;
|
||||
}
|
||||
}
|
||||
fin.close();
|
||||
return fileData;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public RSAPrivateKey getClientPrivateKey() {
|
||||
// Use a lock here to ensure only one guy will be generating or loading
|
||||
// the certificate and key at a time
|
||||
synchronized (globalCryptoLock) {
|
||||
// Return a loaded key if we have one
|
||||
if (key != null) {
|
||||
return key;
|
||||
}
|
||||
|
||||
// No loaded key yet, let's see if we have one on disk
|
||||
if (loadCertKeyPair()) {
|
||||
// Got one
|
||||
return key;
|
||||
}
|
||||
|
||||
// Try to generate a new key pair
|
||||
if (!generateCertKeyPair()) {
|
||||
// Failed
|
||||
return null;
|
||||
}
|
||||
|
||||
// Load the generated pair
|
||||
loadCertKeyPair();
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getPemEncodedClientCertificate() {
|
||||
synchronized (globalCryptoLock) {
|
||||
// Call our helper function to do the cert loading/generation for us
|
||||
getClientCertificate();
|
||||
|
||||
// Return a cached value if we have it
|
||||
return pemCertBytes;
|
||||
}
|
||||
}
|
||||
private boolean loadCertKeyPair() {
|
||||
byte[] certBytes = loadFileToBytes(certFile);
|
||||
byte[] keyBytes = loadFileToBytes(keyFile);
|
||||
|
||||
@Override
|
||||
public String encodeBase64String(byte[] data) {
|
||||
return Base64.encodeToString(data, Base64.NO_WRAP);
|
||||
}
|
||||
// If either file was missing, we definitely can't succeed
|
||||
if (certBytes == null || keyBytes == null) {
|
||||
LimeLog.info("Missing cert or key; need to generate a new one");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
|
||||
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||
pemCertBytes = certBytes;
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
|
||||
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
||||
} catch (CertificateException e) {
|
||||
// May happen if the cert is corrupt
|
||||
LimeLog.warning("Corrupted certificate");
|
||||
return false;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// Should never happen
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
} catch (InvalidKeySpecException e) {
|
||||
// May happen if the key is corrupt
|
||||
LimeLog.warning("Corrupted key");
|
||||
return false;
|
||||
} catch (NoSuchProviderException e) {
|
||||
// Should never happen
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressLint("TrulyRandom")
|
||||
private boolean generateCertKeyPair() {
|
||||
byte[] snBytes = new byte[8];
|
||||
new SecureRandom().nextBytes(snBytes);
|
||||
|
||||
KeyPair keyPair;
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
||||
keyPairGenerator.initialize(2048);
|
||||
keyPair = keyPairGenerator.generateKeyPair();
|
||||
} catch (NoSuchAlgorithmException e1) {
|
||||
// Should never happen
|
||||
e1.printStackTrace();
|
||||
return false;
|
||||
} catch (NoSuchProviderException e) {
|
||||
// Should never happen
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
|
||||
// Expires in 20 years
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTime(now);
|
||||
calendar.add(Calendar.YEAR, 20);
|
||||
Date expirationDate = calendar.getTime();
|
||||
|
||||
BigInteger serial = new BigInteger(snBytes).abs();
|
||||
|
||||
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
|
||||
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
|
||||
X500Name name = nameBuilder.build();
|
||||
|
||||
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
|
||||
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
||||
|
||||
try {
|
||||
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
|
||||
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
|
||||
key = (RSAPrivateKey) keyPair.getPrivate();
|
||||
} catch (Exception e) {
|
||||
// Nothing should go wrong here
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
LimeLog.info("Generated a new key pair");
|
||||
|
||||
// Save the resulting pair
|
||||
saveCertKeyPair();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void saveCertKeyPair() {
|
||||
try {
|
||||
FileOutputStream certOut = new FileOutputStream(certFile);
|
||||
FileOutputStream keyOut = new FileOutputStream(keyFile);
|
||||
|
||||
// Write the certificate in OpenSSL PEM format (important for the server)
|
||||
StringWriter strWriter = new StringWriter();
|
||||
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
|
||||
pemWriter.writeObject(cert);
|
||||
pemWriter.close();
|
||||
|
||||
// Line endings MUST be UNIX for the PC to accept the cert properly
|
||||
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
|
||||
String pemStr = strWriter.getBuffer().toString();
|
||||
for (int i = 0; i < pemStr.length(); i++) {
|
||||
char c = pemStr.charAt(i);
|
||||
if (c != '\r')
|
||||
certWriter.append(c);
|
||||
}
|
||||
certWriter.close();
|
||||
|
||||
// Write the private out in PKCS8 format
|
||||
keyOut.write(key.getEncoded());
|
||||
|
||||
certOut.close();
|
||||
keyOut.close();
|
||||
|
||||
LimeLog.info("Saved generated key pair to disk");
|
||||
} catch (IOException e) {
|
||||
// This isn't good because it means we'll have
|
||||
// to re-pair next time
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public X509Certificate getClientCertificate() {
|
||||
// Use a lock here to ensure only one guy will be generating or loading
|
||||
// the certificate and key at a time
|
||||
synchronized (globalCryptoLock) {
|
||||
// Return a loaded cert if we have one
|
||||
if (cert != null) {
|
||||
return cert;
|
||||
}
|
||||
|
||||
// No loaded cert yet, let's see if we have one on disk
|
||||
if (loadCertKeyPair()) {
|
||||
// Got one
|
||||
return cert;
|
||||
}
|
||||
|
||||
// Try to generate a new key pair
|
||||
if (!generateCertKeyPair()) {
|
||||
// Failed
|
||||
return null;
|
||||
}
|
||||
|
||||
// Load the generated pair
|
||||
loadCertKeyPair();
|
||||
return cert;
|
||||
}
|
||||
}
|
||||
|
||||
public RSAPrivateKey getClientPrivateKey() {
|
||||
// Use a lock here to ensure only one guy will be generating or loading
|
||||
// the certificate and key at a time
|
||||
synchronized (globalCryptoLock) {
|
||||
// Return a loaded key if we have one
|
||||
if (key != null) {
|
||||
return key;
|
||||
}
|
||||
|
||||
// No loaded key yet, let's see if we have one on disk
|
||||
if (loadCertKeyPair()) {
|
||||
// Got one
|
||||
return key;
|
||||
}
|
||||
|
||||
// Try to generate a new key pair
|
||||
if (!generateCertKeyPair()) {
|
||||
// Failed
|
||||
return null;
|
||||
}
|
||||
|
||||
// Load the generated pair
|
||||
loadCertKeyPair();
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getPemEncodedClientCertificate() {
|
||||
synchronized (globalCryptoLock) {
|
||||
// Call our helper function to do the cert loading/generation for us
|
||||
getClientCertificate();
|
||||
|
||||
// Return a cached value if we have it
|
||||
return pemCertBytes;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encodeBase64String(byte[] data) {
|
||||
return Base64.encodeToString(data, Base64.NO_WRAP);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
||||
/**
|
||||
* GFE's prefix for every key code
|
||||
*/
|
||||
public static final short KEY_PREFIX = (short) 0x80;
|
||||
private static final short KEY_PREFIX = (short) 0x80;
|
||||
|
||||
public static final int VK_0 = 48;
|
||||
public static final int VK_9 = 57;
|
||||
@@ -23,8 +23,8 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
||||
public static final int VK_Z = 90;
|
||||
public static final int VK_ALT = 18;
|
||||
public static final int VK_NUMPAD0 = 96;
|
||||
public static final int VK_BACK_SLASH = 92;
|
||||
public static final int VK_CAPS_LOCK = 20;
|
||||
public static final int VK_BACK_SLASH = 92;
|
||||
public static final int VK_CAPS_LOCK = 20;
|
||||
public static final int VK_CLEAR = 12;
|
||||
public static final int VK_COMMA = 44;
|
||||
public static final int VK_CONTROL = 17;
|
||||
|
||||
@@ -4,90 +4,117 @@ import com.limelight.nvstream.NvConnection;
|
||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||
|
||||
public class TouchContext {
|
||||
private int lastTouchX = 0;
|
||||
private int lastTouchY = 0;
|
||||
private int originalTouchX = 0;
|
||||
private int originalTouchY = 0;
|
||||
private long originalTouchTime = 0;
|
||||
|
||||
private NvConnection conn;
|
||||
private int actionIndex;
|
||||
|
||||
private static final int TAP_MOVEMENT_THRESHOLD = 10;
|
||||
private static final int TAP_TIME_THRESHOLD = 250;
|
||||
|
||||
public TouchContext(NvConnection conn, int actionIndex)
|
||||
{
|
||||
this.conn = conn;
|
||||
this.actionIndex = actionIndex;
|
||||
}
|
||||
|
||||
private boolean isTap()
|
||||
{
|
||||
int xDelta = Math.abs(lastTouchX - originalTouchX);
|
||||
int yDelta = Math.abs(lastTouchY - originalTouchY);
|
||||
long timeDelta = System.currentTimeMillis() - originalTouchTime;
|
||||
|
||||
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
|
||||
yDelta <= TAP_MOVEMENT_THRESHOLD &&
|
||||
timeDelta <= TAP_TIME_THRESHOLD;
|
||||
}
|
||||
|
||||
private byte getMouseButtonIndex()
|
||||
{
|
||||
if (actionIndex == 1) {
|
||||
return MouseButtonPacket.BUTTON_RIGHT;
|
||||
}
|
||||
else {
|
||||
return MouseButtonPacket.BUTTON_LEFT;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean touchDownEvent(int eventX, int eventY)
|
||||
{
|
||||
originalTouchX = lastTouchX = eventX;
|
||||
originalTouchY = lastTouchY = eventY;
|
||||
originalTouchTime = System.currentTimeMillis();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void touchUpEvent(int eventX, int eventY)
|
||||
{
|
||||
if (isTap())
|
||||
{
|
||||
byte buttonIndex = getMouseButtonIndex();
|
||||
|
||||
// Lower the mouse button
|
||||
conn.sendMouseButtonDown(buttonIndex);
|
||||
|
||||
// We need to sleep a bit here because some games
|
||||
// do input detection by polling
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException ignored) {}
|
||||
|
||||
// Raise the mouse button
|
||||
conn.sendMouseButtonUp(buttonIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean touchMoveEvent(int eventX, int eventY)
|
||||
{
|
||||
if (eventX != lastTouchX || eventY != lastTouchY)
|
||||
{
|
||||
// We only send moves for the primary touch point
|
||||
if (actionIndex == 0) {
|
||||
conn.sendMouseMove((short)(eventX - lastTouchX),
|
||||
(short)(eventY - lastTouchY));
|
||||
}
|
||||
|
||||
lastTouchX = eventX;
|
||||
lastTouchY = eventY;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
private int lastTouchX = 0;
|
||||
private int lastTouchY = 0;
|
||||
private int originalTouchX = 0;
|
||||
private int originalTouchY = 0;
|
||||
private long originalTouchTime = 0;
|
||||
private boolean cancelled;
|
||||
|
||||
private final NvConnection conn;
|
||||
private final int actionIndex;
|
||||
private final double xFactor;
|
||||
private final double yFactor;
|
||||
|
||||
private static final int TAP_MOVEMENT_THRESHOLD = 10;
|
||||
private static final int TAP_TIME_THRESHOLD = 250;
|
||||
|
||||
public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor)
|
||||
{
|
||||
this.conn = conn;
|
||||
this.actionIndex = actionIndex;
|
||||
this.xFactor = xFactor;
|
||||
this.yFactor = yFactor;
|
||||
}
|
||||
|
||||
public int getActionIndex()
|
||||
{
|
||||
return actionIndex;
|
||||
}
|
||||
|
||||
private boolean isTap()
|
||||
{
|
||||
int xDelta = Math.abs(lastTouchX - originalTouchX);
|
||||
int yDelta = Math.abs(lastTouchY - originalTouchY);
|
||||
long timeDelta = System.currentTimeMillis() - originalTouchTime;
|
||||
|
||||
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
|
||||
yDelta <= TAP_MOVEMENT_THRESHOLD &&
|
||||
timeDelta <= TAP_TIME_THRESHOLD;
|
||||
}
|
||||
|
||||
private byte getMouseButtonIndex()
|
||||
{
|
||||
if (actionIndex == 1) {
|
||||
return MouseButtonPacket.BUTTON_RIGHT;
|
||||
}
|
||||
else {
|
||||
return MouseButtonPacket.BUTTON_LEFT;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean touchDownEvent(int eventX, int eventY)
|
||||
{
|
||||
originalTouchX = lastTouchX = eventX;
|
||||
originalTouchY = lastTouchY = eventY;
|
||||
originalTouchTime = System.currentTimeMillis();
|
||||
cancelled = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void touchUpEvent(int eventX, int eventY)
|
||||
{
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTap())
|
||||
{
|
||||
byte buttonIndex = getMouseButtonIndex();
|
||||
|
||||
// Lower the mouse button
|
||||
conn.sendMouseButtonDown(buttonIndex);
|
||||
|
||||
// We need to sleep a bit here because some games
|
||||
// do input detection by polling
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException ignored) {}
|
||||
|
||||
// Raise the mouse button
|
||||
conn.sendMouseButtonUp(buttonIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean touchMoveEvent(int eventX, int eventY)
|
||||
{
|
||||
if (eventX != lastTouchX || eventY != lastTouchY)
|
||||
{
|
||||
// We only send moves for the primary touch point
|
||||
if (actionIndex == 0) {
|
||||
int deltaX = eventX - lastTouchX;
|
||||
int deltaY = eventY - lastTouchY;
|
||||
|
||||
// Scale the deltas based on the factors passed to our constructor
|
||||
deltaX = (int)Math.round((double)deltaX * xFactor);
|
||||
deltaY = (int)Math.round((double)deltaY * yFactor);
|
||||
|
||||
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
||||
}
|
||||
|
||||
lastTouchX = eventX;
|
||||
lastTouchY = eventY;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void cancelTouch() {
|
||||
cancelled = true;
|
||||
}
|
||||
|
||||
public boolean isCancelled() {
|
||||
return cancelled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
package com.limelight.binding.input.evdev;
|
||||
|
||||
public class EvdevEvent {
|
||||
public static final int EVDEV_MIN_EVENT_SIZE = 16;
|
||||
public static final int EVDEV_MAX_EVENT_SIZE = 24;
|
||||
|
||||
/* Event types */
|
||||
public static final short EV_SYN = 0x00;
|
||||
public static final short EV_KEY = 0x01;
|
||||
public static final short EV_REL = 0x02;
|
||||
public static final short EV_MSC = 0x04;
|
||||
|
||||
/* Relative axes */
|
||||
public static final short REL_X = 0x00;
|
||||
public static final short REL_Y = 0x01;
|
||||
public static final short REL_WHEEL = 0x08;
|
||||
|
||||
/* Buttons */
|
||||
public static final short BTN_LEFT = 0x110;
|
||||
public static final short BTN_RIGHT = 0x111;
|
||||
public static final short BTN_MIDDLE = 0x112;
|
||||
public static final short BTN_SIDE = 0x113;
|
||||
public static final short BTN_EXTRA = 0x114;
|
||||
public static final short BTN_FORWARD = 0x115;
|
||||
public static final short BTN_BACK = 0x116;
|
||||
public static final short BTN_TASK = 0x117;
|
||||
public static final short BTN_GAMEPAD = 0x130;
|
||||
|
||||
/* Keys */
|
||||
public static final short KEY_Q = 16;
|
||||
|
||||
public short type;
|
||||
public short code;
|
||||
public int value;
|
||||
|
||||
public EvdevEvent(short type, short code, int value) {
|
||||
this.type = type;
|
||||
this.code = code;
|
||||
this.value = value;
|
||||
}
|
||||
public static final int EVDEV_MIN_EVENT_SIZE = 16;
|
||||
public static final int EVDEV_MAX_EVENT_SIZE = 24;
|
||||
|
||||
/* Event types */
|
||||
public static final short EV_SYN = 0x00;
|
||||
public static final short EV_KEY = 0x01;
|
||||
public static final short EV_REL = 0x02;
|
||||
public static final short EV_MSC = 0x04;
|
||||
|
||||
/* Relative axes */
|
||||
public static final short REL_X = 0x00;
|
||||
public static final short REL_Y = 0x01;
|
||||
public static final short REL_WHEEL = 0x08;
|
||||
|
||||
/* Buttons */
|
||||
public static final short BTN_LEFT = 0x110;
|
||||
public static final short BTN_RIGHT = 0x111;
|
||||
public static final short BTN_MIDDLE = 0x112;
|
||||
public static final short BTN_SIDE = 0x113;
|
||||
public static final short BTN_EXTRA = 0x114;
|
||||
public static final short BTN_FORWARD = 0x115;
|
||||
public static final short BTN_BACK = 0x116;
|
||||
public static final short BTN_TASK = 0x117;
|
||||
public static final short BTN_GAMEPAD = 0x130;
|
||||
|
||||
/* Keys */
|
||||
public static final short KEY_Q = 16;
|
||||
|
||||
public final short type;
|
||||
public final short code;
|
||||
public final int value;
|
||||
|
||||
public EvdevEvent(short type, short code, int value) {
|
||||
this.type = type;
|
||||
this.code = code;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,161 +7,161 @@ import com.limelight.LimeLog;
|
||||
|
||||
public class EvdevHandler {
|
||||
|
||||
private String absolutePath;
|
||||
private EvdevListener listener;
|
||||
private boolean shutdown = false;
|
||||
private int fd = -1;
|
||||
|
||||
private Thread handlerThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// All the finally blocks here make this code look like a mess
|
||||
// but it's important that we get this right to avoid causing
|
||||
// system-wide input problems.
|
||||
|
||||
// Open the /dev/input/eventX file
|
||||
fd = EvdevReader.open(absolutePath);
|
||||
if (fd == -1) {
|
||||
LimeLog.warning("Unable to open "+absolutePath);
|
||||
return;
|
||||
}
|
||||
private final String absolutePath;
|
||||
private final EvdevListener listener;
|
||||
private boolean shutdown = false;
|
||||
private int fd = -1;
|
||||
|
||||
try {
|
||||
// Check if it's a mouse or keyboard, but not a gamepad
|
||||
if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) ||
|
||||
EvdevReader.isGamepad(fd)) {
|
||||
// We only handle keyboards and mice
|
||||
return;
|
||||
}
|
||||
private final Thread handlerThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// All the finally blocks here make this code look like a mess
|
||||
// but it's important that we get this right to avoid causing
|
||||
// system-wide input problems.
|
||||
|
||||
// Grab it for ourselves
|
||||
if (!EvdevReader.grab(fd)) {
|
||||
LimeLog.warning("Unable to grab "+absolutePath);
|
||||
return;
|
||||
}
|
||||
// Open the /dev/input/eventX file
|
||||
fd = EvdevReader.open(absolutePath);
|
||||
if (fd == -1) {
|
||||
LimeLog.warning("Unable to open "+absolutePath);
|
||||
return;
|
||||
}
|
||||
|
||||
LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath);
|
||||
try {
|
||||
// Check if it's a mouse or keyboard, but not a gamepad
|
||||
if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) ||
|
||||
EvdevReader.isGamepad(fd)) {
|
||||
// We only handle keyboards and mice
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder());
|
||||
// Grab it for ourselves
|
||||
if (!EvdevReader.grab(fd)) {
|
||||
LimeLog.warning("Unable to grab "+absolutePath);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
int deltaX = 0;
|
||||
int deltaY = 0;
|
||||
byte deltaScroll = 0;
|
||||
LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath);
|
||||
|
||||
while (!isInterrupted() && !shutdown) {
|
||||
EvdevEvent event = EvdevReader.read(fd, buffer);
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder());
|
||||
|
||||
switch (event.type)
|
||||
{
|
||||
case EvdevEvent.EV_SYN:
|
||||
if (deltaX != 0 || deltaY != 0) {
|
||||
listener.mouseMove(deltaX, deltaY);
|
||||
deltaX = deltaY = 0;
|
||||
}
|
||||
if (deltaScroll != 0) {
|
||||
listener.mouseScroll(deltaScroll);
|
||||
deltaScroll = 0;
|
||||
}
|
||||
break;
|
||||
try {
|
||||
int deltaX = 0;
|
||||
int deltaY = 0;
|
||||
byte deltaScroll = 0;
|
||||
|
||||
case EvdevEvent.EV_REL:
|
||||
switch (event.code)
|
||||
{
|
||||
case EvdevEvent.REL_X:
|
||||
deltaX = event.value;
|
||||
break;
|
||||
case EvdevEvent.REL_Y:
|
||||
deltaY = event.value;
|
||||
break;
|
||||
case EvdevEvent.REL_WHEEL:
|
||||
deltaScroll = (byte) event.value;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
while (!isInterrupted() && !shutdown) {
|
||||
EvdevEvent event = EvdevReader.read(fd, buffer);
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
case EvdevEvent.EV_KEY:
|
||||
switch (event.code)
|
||||
{
|
||||
case EvdevEvent.BTN_LEFT:
|
||||
listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT,
|
||||
event.value != 0);
|
||||
break;
|
||||
case EvdevEvent.BTN_MIDDLE:
|
||||
listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE,
|
||||
event.value != 0);
|
||||
break;
|
||||
case EvdevEvent.BTN_RIGHT:
|
||||
listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT,
|
||||
event.value != 0);
|
||||
break;
|
||||
|
||||
case EvdevEvent.BTN_SIDE:
|
||||
case EvdevEvent.BTN_EXTRA:
|
||||
case EvdevEvent.BTN_FORWARD:
|
||||
case EvdevEvent.BTN_BACK:
|
||||
case EvdevEvent.BTN_TASK:
|
||||
// Other unhandled mouse buttons
|
||||
break;
|
||||
|
||||
default:
|
||||
// We got some unrecognized button. This means
|
||||
// someone is trying to use the other device in this
|
||||
// "combination" input device. We'll try to handle
|
||||
// it via keyboard, but we're not going to disconnect
|
||||
// if we can't
|
||||
short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code);
|
||||
if (keyCode != 0) {
|
||||
listener.keyboardEvent(event.value != 0, keyCode);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case EvdevEvent.EV_MSC:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// Release our grab
|
||||
EvdevReader.ungrab(fd);
|
||||
}
|
||||
} finally {
|
||||
// Close the file
|
||||
EvdevReader.close(fd);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public EvdevHandler(String absolutePath, EvdevListener listener) {
|
||||
this.absolutePath = absolutePath;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
handlerThread.start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
// Close the fd. It doesn't matter if this races
|
||||
// with the handler thread. We'll close this out from
|
||||
// under the thread to wake it up
|
||||
if (fd != -1) {
|
||||
EvdevReader.close(fd);
|
||||
}
|
||||
|
||||
shutdown = true;
|
||||
handlerThread.interrupt();
|
||||
|
||||
try {
|
||||
handlerThread.join();
|
||||
} catch (InterruptedException ignored) {}
|
||||
}
|
||||
|
||||
public void notifyDeleted() {
|
||||
stop();
|
||||
}
|
||||
switch (event.type)
|
||||
{
|
||||
case EvdevEvent.EV_SYN:
|
||||
if (deltaX != 0 || deltaY != 0) {
|
||||
listener.mouseMove(deltaX, deltaY);
|
||||
deltaX = deltaY = 0;
|
||||
}
|
||||
if (deltaScroll != 0) {
|
||||
listener.mouseScroll(deltaScroll);
|
||||
deltaScroll = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case EvdevEvent.EV_REL:
|
||||
switch (event.code)
|
||||
{
|
||||
case EvdevEvent.REL_X:
|
||||
deltaX = event.value;
|
||||
break;
|
||||
case EvdevEvent.REL_Y:
|
||||
deltaY = event.value;
|
||||
break;
|
||||
case EvdevEvent.REL_WHEEL:
|
||||
deltaScroll = (byte) event.value;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case EvdevEvent.EV_KEY:
|
||||
switch (event.code)
|
||||
{
|
||||
case EvdevEvent.BTN_LEFT:
|
||||
listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT,
|
||||
event.value != 0);
|
||||
break;
|
||||
case EvdevEvent.BTN_MIDDLE:
|
||||
listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE,
|
||||
event.value != 0);
|
||||
break;
|
||||
case EvdevEvent.BTN_RIGHT:
|
||||
listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT,
|
||||
event.value != 0);
|
||||
break;
|
||||
|
||||
case EvdevEvent.BTN_SIDE:
|
||||
case EvdevEvent.BTN_EXTRA:
|
||||
case EvdevEvent.BTN_FORWARD:
|
||||
case EvdevEvent.BTN_BACK:
|
||||
case EvdevEvent.BTN_TASK:
|
||||
// Other unhandled mouse buttons
|
||||
break;
|
||||
|
||||
default:
|
||||
// We got some unrecognized button. This means
|
||||
// someone is trying to use the other device in this
|
||||
// "combination" input device. We'll try to handle
|
||||
// it via keyboard, but we're not going to disconnect
|
||||
// if we can't
|
||||
short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code);
|
||||
if (keyCode != 0) {
|
||||
listener.keyboardEvent(event.value != 0, keyCode);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case EvdevEvent.EV_MSC:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// Release our grab
|
||||
EvdevReader.ungrab(fd);
|
||||
}
|
||||
} finally {
|
||||
// Close the file
|
||||
EvdevReader.close(fd);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public EvdevHandler(String absolutePath, EvdevListener listener) {
|
||||
this.absolutePath = absolutePath;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
handlerThread.start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
// Close the fd. It doesn't matter if this races
|
||||
// with the handler thread. We'll close this out from
|
||||
// under the thread to wake it up
|
||||
if (fd != -1) {
|
||||
EvdevReader.close(fd);
|
||||
}
|
||||
|
||||
shutdown = true;
|
||||
handlerThread.interrupt();
|
||||
|
||||
try {
|
||||
handlerThread.join();
|
||||
} catch (InterruptedException ignored) {}
|
||||
}
|
||||
|
||||
public void notifyDeleted() {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.limelight.binding.input.evdev;
|
||||
|
||||
public interface EvdevListener {
|
||||
public static final int BUTTON_LEFT = 1;
|
||||
public static final int BUTTON_MIDDLE = 2;
|
||||
public static final int BUTTON_RIGHT = 3;
|
||||
|
||||
public void mouseMove(int deltaX, int deltaY);
|
||||
public void mouseButtonEvent(int buttonId, boolean down);
|
||||
public void mouseScroll(byte amount);
|
||||
public void keyboardEvent(boolean buttonDown, short keyCode);
|
||||
public static final int BUTTON_LEFT = 1;
|
||||
public static final int BUTTON_MIDDLE = 2;
|
||||
public static final int BUTTON_RIGHT = 3;
|
||||
|
||||
public void mouseMove(int deltaX, int deltaY);
|
||||
public void mouseButtonEvent(int buttonId, boolean down);
|
||||
public void mouseScroll(byte amount);
|
||||
public void keyboardEvent(boolean buttonDown, short keyCode);
|
||||
}
|
||||
|
||||
@@ -1,103 +1,105 @@
|
||||
package com.limelight.binding.input.evdev;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import android.os.Build;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Locale;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
public class EvdevReader {
|
||||
static {
|
||||
System.loadLibrary("evdev_reader");
|
||||
}
|
||||
|
||||
// Requires root to chmod /dev/input/eventX
|
||||
public static boolean setPermissions(String[] files, int octalPermissions) {
|
||||
ProcessBuilder builder = new ProcessBuilder("su");
|
||||
|
||||
try {
|
||||
Process p = builder.start();
|
||||
|
||||
OutputStream stdin = p.getOutputStream();
|
||||
for (String file : files) {
|
||||
stdin.write(String.format((Locale)null, "chmod %o %s\n", octalPermissions, file).getBytes("UTF-8"));
|
||||
}
|
||||
stdin.write("exit\n".getBytes("UTF-8"));
|
||||
stdin.flush();
|
||||
|
||||
p.waitFor();
|
||||
p.destroy();
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns the fd to be passed to other function or -1 on error
|
||||
public static native int open(String fileName);
|
||||
|
||||
// Prevent other apps (including Android itself) from using the device while "grabbed"
|
||||
public static native boolean grab(int fd);
|
||||
public static native boolean ungrab(int fd);
|
||||
|
||||
// Used for checking device capabilities
|
||||
public static native boolean hasRelAxis(int fd, short axis);
|
||||
public static native boolean hasAbsAxis(int fd, short axis);
|
||||
public static native boolean hasKey(int fd, short key);
|
||||
|
||||
public static boolean isMouse(int fd) {
|
||||
// This is the same check that Android does in EventHub.cpp
|
||||
return hasRelAxis(fd, EvdevEvent.REL_X) &&
|
||||
hasRelAxis(fd, EvdevEvent.REL_Y) &&
|
||||
hasKey(fd, EvdevEvent.BTN_LEFT);
|
||||
}
|
||||
|
||||
public static boolean isAlphaKeyboard(int fd) {
|
||||
// This is the same check that Android does in EventHub.cpp
|
||||
return hasKey(fd, EvdevEvent.KEY_Q);
|
||||
}
|
||||
|
||||
public static boolean isGamepad(int fd) {
|
||||
return hasKey(fd, EvdevEvent.BTN_GAMEPAD);
|
||||
}
|
||||
|
||||
// Returns the bytes read or -1 on error
|
||||
private static native int read(int fd, byte[] buffer);
|
||||
|
||||
// Takes a byte buffer to use to read the output into.
|
||||
// This buffer MUST be in native byte order and at least
|
||||
// EVDEV_MAX_EVENT_SIZE bytes long.
|
||||
public static EvdevEvent read(int fd, ByteBuffer buffer) {
|
||||
int bytesRead = read(fd, buffer.array());
|
||||
if (bytesRead < 0) {
|
||||
LimeLog.warning("Failed to read: "+bytesRead);
|
||||
return null;
|
||||
}
|
||||
else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
|
||||
LimeLog.warning("Short read: "+bytesRead);
|
||||
return null;
|
||||
}
|
||||
|
||||
buffer.limit(bytesRead);
|
||||
buffer.rewind();
|
||||
|
||||
// Throw away the time stamp
|
||||
if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
|
||||
buffer.getLong();
|
||||
buffer.getLong();
|
||||
} else {
|
||||
buffer.getInt();
|
||||
buffer.getInt();
|
||||
}
|
||||
|
||||
return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt());
|
||||
}
|
||||
|
||||
// Closes the fd from open()
|
||||
public static native int close(int fd);
|
||||
static {
|
||||
System.loadLibrary("evdev_reader");
|
||||
}
|
||||
|
||||
public static void patchSeLinuxPolicies() {
|
||||
//
|
||||
// FIXME: We REALLY shouldn't being changing permissions on the input devices like this.
|
||||
// We should probably do something clever with a separate daemon and talk via a localhost
|
||||
// socket. We don't return the SELinux policies back to default after we're done which I feel
|
||||
// bad about, but we do chmod the input devices back so I don't think any additional attack surface
|
||||
// remains opened after streaming other than listing the /dev/input directory which you wouldn't
|
||||
// normally be able to do with SELinux enforcing on Lollipop.
|
||||
//
|
||||
// We need to modify SELinux policies to allow us to capture input devices on Lollipop and possibly other
|
||||
// more restrictive ROMs. Per Chainfire's SuperSU documentation, the supolicy binary is provided on
|
||||
// 4.4 and later to do live SELinux policy changes.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
EvdevShell shell = EvdevShell.getInstance();
|
||||
shell.runCommand("supolicy --live \"allow untrusted_app input_device dir { open getattr read search }\" " +
|
||||
"\"allow untrusted_app input_device chr_file { open read write ioctl }\"");
|
||||
}
|
||||
}
|
||||
|
||||
// Requires root to chmod /dev/input/eventX
|
||||
public static void setPermissions(String[] files, int octalPermissions) {
|
||||
EvdevShell shell = EvdevShell.getInstance();
|
||||
|
||||
for (String file : files) {
|
||||
shell.runCommand(String.format((Locale)null, "chmod %o %s", octalPermissions, file));
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the fd to be passed to other function or -1 on error
|
||||
public static native int open(String fileName);
|
||||
|
||||
// Prevent other apps (including Android itself) from using the device while "grabbed"
|
||||
public static native boolean grab(int fd);
|
||||
public static native boolean ungrab(int fd);
|
||||
|
||||
// Used for checking device capabilities
|
||||
public static native boolean hasRelAxis(int fd, short axis);
|
||||
public static native boolean hasAbsAxis(int fd, short axis);
|
||||
public static native boolean hasKey(int fd, short key);
|
||||
|
||||
public static boolean isMouse(int fd) {
|
||||
// This is the same check that Android does in EventHub.cpp
|
||||
return hasRelAxis(fd, EvdevEvent.REL_X) &&
|
||||
hasRelAxis(fd, EvdevEvent.REL_Y) &&
|
||||
hasKey(fd, EvdevEvent.BTN_LEFT);
|
||||
}
|
||||
|
||||
public static boolean isAlphaKeyboard(int fd) {
|
||||
// This is the same check that Android does in EventHub.cpp
|
||||
return hasKey(fd, EvdevEvent.KEY_Q);
|
||||
}
|
||||
|
||||
public static boolean isGamepad(int fd) {
|
||||
return hasKey(fd, EvdevEvent.BTN_GAMEPAD);
|
||||
}
|
||||
|
||||
// Returns the bytes read or -1 on error
|
||||
private static native int read(int fd, byte[] buffer);
|
||||
|
||||
// Takes a byte buffer to use to read the output into.
|
||||
// This buffer MUST be in native byte order and at least
|
||||
// EVDEV_MAX_EVENT_SIZE bytes long.
|
||||
public static EvdevEvent read(int fd, ByteBuffer buffer) {
|
||||
int bytesRead = read(fd, buffer.array());
|
||||
if (bytesRead < 0) {
|
||||
LimeLog.warning("Failed to read: "+bytesRead);
|
||||
return null;
|
||||
}
|
||||
else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
|
||||
LimeLog.warning("Short read: "+bytesRead);
|
||||
return null;
|
||||
}
|
||||
|
||||
buffer.limit(bytesRead);
|
||||
buffer.rewind();
|
||||
|
||||
// Throw away the time stamp
|
||||
if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
|
||||
buffer.getLong();
|
||||
buffer.getLong();
|
||||
} else {
|
||||
buffer.getInt();
|
||||
buffer.getInt();
|
||||
}
|
||||
|
||||
return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt());
|
||||
}
|
||||
|
||||
// Closes the fd from open()
|
||||
public static native int close(int fd);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.limelight.binding.input.evdev;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Scanner;
|
||||
import java.util.UUID;
|
||||
|
||||
public class EvdevShell {
|
||||
private OutputStream stdin;
|
||||
private InputStream stdout;
|
||||
private Process shell;
|
||||
private final String uuidString = UUID.randomUUID().toString();
|
||||
|
||||
private static final EvdevShell globalShell = new EvdevShell();
|
||||
|
||||
public static EvdevShell getInstance() {
|
||||
return globalShell;
|
||||
}
|
||||
|
||||
public void startShell() {
|
||||
ProcessBuilder builder = new ProcessBuilder("su");
|
||||
|
||||
try {
|
||||
// Redirect stderr to stdout
|
||||
builder.redirectErrorStream(true);
|
||||
shell = builder.start();
|
||||
|
||||
stdin = shell.getOutputStream();
|
||||
stdout = shell.getInputStream();
|
||||
} catch (IOException e) {
|
||||
// This is unexpected
|
||||
e.printStackTrace();
|
||||
|
||||
// Kill the shell if it spawned
|
||||
if (stdin != null) {
|
||||
try {
|
||||
stdin.close();
|
||||
} catch (IOException e1) {
|
||||
e1.printStackTrace();
|
||||
} finally {
|
||||
stdin = null;
|
||||
}
|
||||
}
|
||||
if (stdout != null) {
|
||||
try {
|
||||
stdout.close();
|
||||
} catch (IOException e1) {
|
||||
e1.printStackTrace();
|
||||
} finally {
|
||||
stdout = null;
|
||||
}
|
||||
}
|
||||
if (shell != null) {
|
||||
shell.destroy();
|
||||
shell = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void runCommand(String command) {
|
||||
if (shell == null) {
|
||||
// Shell never started
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Write the command followed by an echo with our UUID
|
||||
stdin.write((command+'\n').getBytes("UTF-8"));
|
||||
stdin.write(("echo "+uuidString+'\n').getBytes("UTF-8"));
|
||||
stdin.flush();
|
||||
|
||||
// This is the only command in flight so we can use a scanner
|
||||
// without worrying about it eating too many characters
|
||||
Scanner scanner = new Scanner(stdout);
|
||||
while (scanner.hasNext()) {
|
||||
if (scanner.next().contains(uuidString)) {
|
||||
// Our command ran
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void stopShell() throws InterruptedException {
|
||||
boolean exitWritten = false;
|
||||
|
||||
if (shell == null) {
|
||||
// Shell never started
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
stdin.write("exit\n".getBytes("UTF-8"));
|
||||
exitWritten = true;
|
||||
} catch (IOException e) {
|
||||
// We'll destroy the process without
|
||||
// waiting for it to terminate since
|
||||
// we don't know whether our exit command made it
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
if (exitWritten) {
|
||||
try {
|
||||
shell.waitFor();
|
||||
} finally {
|
||||
shell.destroy();
|
||||
}
|
||||
}
|
||||
else {
|
||||
shell.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,136 +4,136 @@ import android.view.KeyEvent;
|
||||
|
||||
public class EvdevTranslator {
|
||||
|
||||
public static final short EVDEV_KEY_CODES[] = {
|
||||
0, //KeyEvent.VK_RESERVED
|
||||
KeyEvent.KEYCODE_ESCAPE,
|
||||
KeyEvent.KEYCODE_1,
|
||||
KeyEvent.KEYCODE_2,
|
||||
KeyEvent.KEYCODE_3,
|
||||
KeyEvent.KEYCODE_4,
|
||||
KeyEvent.KEYCODE_5,
|
||||
KeyEvent.KEYCODE_6,
|
||||
KeyEvent.KEYCODE_7,
|
||||
KeyEvent.KEYCODE_8,
|
||||
KeyEvent.KEYCODE_9,
|
||||
KeyEvent.KEYCODE_0,
|
||||
KeyEvent.KEYCODE_MINUS,
|
||||
KeyEvent.KEYCODE_EQUALS,
|
||||
KeyEvent.KEYCODE_DEL,
|
||||
KeyEvent.KEYCODE_TAB,
|
||||
KeyEvent.KEYCODE_Q,
|
||||
KeyEvent.KEYCODE_W,
|
||||
KeyEvent.KEYCODE_E,
|
||||
KeyEvent.KEYCODE_R,
|
||||
KeyEvent.KEYCODE_T,
|
||||
KeyEvent.KEYCODE_Y,
|
||||
KeyEvent.KEYCODE_U,
|
||||
KeyEvent.KEYCODE_I,
|
||||
KeyEvent.KEYCODE_O,
|
||||
KeyEvent.KEYCODE_P,
|
||||
KeyEvent.KEYCODE_LEFT_BRACKET,
|
||||
KeyEvent.KEYCODE_RIGHT_BRACKET,
|
||||
KeyEvent.KEYCODE_ENTER,
|
||||
KeyEvent.KEYCODE_CTRL_LEFT,
|
||||
KeyEvent.KEYCODE_A,
|
||||
KeyEvent.KEYCODE_S,
|
||||
KeyEvent.KEYCODE_D,
|
||||
KeyEvent.KEYCODE_F,
|
||||
KeyEvent.KEYCODE_G,
|
||||
KeyEvent.KEYCODE_H,
|
||||
KeyEvent.KEYCODE_J,
|
||||
KeyEvent.KEYCODE_K,
|
||||
KeyEvent.KEYCODE_L,
|
||||
KeyEvent.KEYCODE_SEMICOLON,
|
||||
KeyEvent.KEYCODE_APOSTROPHE,
|
||||
KeyEvent.KEYCODE_GRAVE,
|
||||
KeyEvent.KEYCODE_SHIFT_LEFT,
|
||||
KeyEvent.KEYCODE_BACKSLASH,
|
||||
KeyEvent.KEYCODE_Z,
|
||||
KeyEvent.KEYCODE_X,
|
||||
KeyEvent.KEYCODE_C,
|
||||
KeyEvent.KEYCODE_V,
|
||||
KeyEvent.KEYCODE_B,
|
||||
KeyEvent.KEYCODE_N,
|
||||
KeyEvent.KEYCODE_M,
|
||||
KeyEvent.KEYCODE_COMMA,
|
||||
KeyEvent.KEYCODE_PERIOD,
|
||||
KeyEvent.KEYCODE_SLASH,
|
||||
KeyEvent.KEYCODE_SHIFT_RIGHT,
|
||||
KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
|
||||
KeyEvent.KEYCODE_ALT_LEFT,
|
||||
KeyEvent.KEYCODE_SPACE,
|
||||
KeyEvent.KEYCODE_CAPS_LOCK,
|
||||
KeyEvent.KEYCODE_F1,
|
||||
KeyEvent.KEYCODE_F2,
|
||||
KeyEvent.KEYCODE_F3,
|
||||
KeyEvent.KEYCODE_F4,
|
||||
KeyEvent.KEYCODE_F5,
|
||||
KeyEvent.KEYCODE_F6,
|
||||
KeyEvent.KEYCODE_F7,
|
||||
KeyEvent.KEYCODE_F8,
|
||||
KeyEvent.KEYCODE_F9,
|
||||
KeyEvent.KEYCODE_F10,
|
||||
KeyEvent.KEYCODE_NUM_LOCK,
|
||||
KeyEvent.KEYCODE_SCROLL_LOCK,
|
||||
KeyEvent.KEYCODE_NUMPAD_7,
|
||||
KeyEvent.KEYCODE_NUMPAD_8,
|
||||
KeyEvent.KEYCODE_NUMPAD_9,
|
||||
KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
|
||||
KeyEvent.KEYCODE_NUMPAD_4,
|
||||
KeyEvent.KEYCODE_NUMPAD_5,
|
||||
KeyEvent.KEYCODE_NUMPAD_6,
|
||||
KeyEvent.KEYCODE_NUMPAD_ADD,
|
||||
KeyEvent.KEYCODE_NUMPAD_1,
|
||||
KeyEvent.KEYCODE_NUMPAD_2,
|
||||
KeyEvent.KEYCODE_NUMPAD_3,
|
||||
KeyEvent.KEYCODE_NUMPAD_0,
|
||||
KeyEvent.KEYCODE_NUMPAD_DOT,
|
||||
0,
|
||||
0, //KeyEvent.VK_ZENKAKUHANKAKU,
|
||||
0, //KeyEvent.VK_102ND,
|
||||
KeyEvent.KEYCODE_F11,
|
||||
KeyEvent.KEYCODE_F12,
|
||||
0, //KeyEvent.VK_RO,
|
||||
0, //KeyEvent.VK_KATAKANA,
|
||||
0, //KeyEvent.VK_HIRAGANA,
|
||||
0, //KeyEvent.VK_HENKAN,
|
||||
0, //KeyEvent.VK_KATAKANAHIRAGANA,
|
||||
0, //KeyEvent.VK_MUHENKAN,
|
||||
0, //KeyEvent.VK_KPJPCOMMA,
|
||||
KeyEvent.KEYCODE_NUMPAD_ENTER,
|
||||
KeyEvent.KEYCODE_CTRL_RIGHT,
|
||||
KeyEvent.KEYCODE_NUMPAD_DIVIDE,
|
||||
KeyEvent.KEYCODE_SYSRQ,
|
||||
KeyEvent.KEYCODE_ALT_RIGHT,
|
||||
0, //KeyEvent.VK_LINEFEED,
|
||||
KeyEvent.KEYCODE_HOME,
|
||||
KeyEvent.KEYCODE_DPAD_UP,
|
||||
KeyEvent.KEYCODE_PAGE_UP,
|
||||
KeyEvent.KEYCODE_DPAD_LEFT,
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT,
|
||||
KeyEvent.KEYCODE_MOVE_END,
|
||||
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||
KeyEvent.KEYCODE_PAGE_DOWN,
|
||||
KeyEvent.KEYCODE_INSERT,
|
||||
KeyEvent.KEYCODE_FORWARD_DEL,
|
||||
0, //KeyEvent.VK_MACRO,
|
||||
0, //KeyEvent.VK_MUTE,
|
||||
0, //KeyEvent.VK_VOLUMEDOWN,
|
||||
0, //KeyEvent.VK_VOLUMEUP,
|
||||
0, //KeyEvent.VK_POWER, /* SC System Power Down */
|
||||
KeyEvent.KEYCODE_NUMPAD_EQUALS,
|
||||
0, //KeyEvent.VK_KPPLUSMINUS,
|
||||
KeyEvent.KEYCODE_BREAK,
|
||||
0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */
|
||||
};
|
||||
|
||||
public static short translateEvdevKeyCode(short evdevKeyCode) {
|
||||
if (evdevKeyCode < EVDEV_KEY_CODES.length) {
|
||||
return EVDEV_KEY_CODES[evdevKeyCode];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
private static final short[] EVDEV_KEY_CODES = {
|
||||
0, //KeyEvent.VK_RESERVED
|
||||
KeyEvent.KEYCODE_ESCAPE,
|
||||
KeyEvent.KEYCODE_1,
|
||||
KeyEvent.KEYCODE_2,
|
||||
KeyEvent.KEYCODE_3,
|
||||
KeyEvent.KEYCODE_4,
|
||||
KeyEvent.KEYCODE_5,
|
||||
KeyEvent.KEYCODE_6,
|
||||
KeyEvent.KEYCODE_7,
|
||||
KeyEvent.KEYCODE_8,
|
||||
KeyEvent.KEYCODE_9,
|
||||
KeyEvent.KEYCODE_0,
|
||||
KeyEvent.KEYCODE_MINUS,
|
||||
KeyEvent.KEYCODE_EQUALS,
|
||||
KeyEvent.KEYCODE_DEL,
|
||||
KeyEvent.KEYCODE_TAB,
|
||||
KeyEvent.KEYCODE_Q,
|
||||
KeyEvent.KEYCODE_W,
|
||||
KeyEvent.KEYCODE_E,
|
||||
KeyEvent.KEYCODE_R,
|
||||
KeyEvent.KEYCODE_T,
|
||||
KeyEvent.KEYCODE_Y,
|
||||
KeyEvent.KEYCODE_U,
|
||||
KeyEvent.KEYCODE_I,
|
||||
KeyEvent.KEYCODE_O,
|
||||
KeyEvent.KEYCODE_P,
|
||||
KeyEvent.KEYCODE_LEFT_BRACKET,
|
||||
KeyEvent.KEYCODE_RIGHT_BRACKET,
|
||||
KeyEvent.KEYCODE_ENTER,
|
||||
KeyEvent.KEYCODE_CTRL_LEFT,
|
||||
KeyEvent.KEYCODE_A,
|
||||
KeyEvent.KEYCODE_S,
|
||||
KeyEvent.KEYCODE_D,
|
||||
KeyEvent.KEYCODE_F,
|
||||
KeyEvent.KEYCODE_G,
|
||||
KeyEvent.KEYCODE_H,
|
||||
KeyEvent.KEYCODE_J,
|
||||
KeyEvent.KEYCODE_K,
|
||||
KeyEvent.KEYCODE_L,
|
||||
KeyEvent.KEYCODE_SEMICOLON,
|
||||
KeyEvent.KEYCODE_APOSTROPHE,
|
||||
KeyEvent.KEYCODE_GRAVE,
|
||||
KeyEvent.KEYCODE_SHIFT_LEFT,
|
||||
KeyEvent.KEYCODE_BACKSLASH,
|
||||
KeyEvent.KEYCODE_Z,
|
||||
KeyEvent.KEYCODE_X,
|
||||
KeyEvent.KEYCODE_C,
|
||||
KeyEvent.KEYCODE_V,
|
||||
KeyEvent.KEYCODE_B,
|
||||
KeyEvent.KEYCODE_N,
|
||||
KeyEvent.KEYCODE_M,
|
||||
KeyEvent.KEYCODE_COMMA,
|
||||
KeyEvent.KEYCODE_PERIOD,
|
||||
KeyEvent.KEYCODE_SLASH,
|
||||
KeyEvent.KEYCODE_SHIFT_RIGHT,
|
||||
KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
|
||||
KeyEvent.KEYCODE_ALT_LEFT,
|
||||
KeyEvent.KEYCODE_SPACE,
|
||||
KeyEvent.KEYCODE_CAPS_LOCK,
|
||||
KeyEvent.KEYCODE_F1,
|
||||
KeyEvent.KEYCODE_F2,
|
||||
KeyEvent.KEYCODE_F3,
|
||||
KeyEvent.KEYCODE_F4,
|
||||
KeyEvent.KEYCODE_F5,
|
||||
KeyEvent.KEYCODE_F6,
|
||||
KeyEvent.KEYCODE_F7,
|
||||
KeyEvent.KEYCODE_F8,
|
||||
KeyEvent.KEYCODE_F9,
|
||||
KeyEvent.KEYCODE_F10,
|
||||
KeyEvent.KEYCODE_NUM_LOCK,
|
||||
KeyEvent.KEYCODE_SCROLL_LOCK,
|
||||
KeyEvent.KEYCODE_NUMPAD_7,
|
||||
KeyEvent.KEYCODE_NUMPAD_8,
|
||||
KeyEvent.KEYCODE_NUMPAD_9,
|
||||
KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
|
||||
KeyEvent.KEYCODE_NUMPAD_4,
|
||||
KeyEvent.KEYCODE_NUMPAD_5,
|
||||
KeyEvent.KEYCODE_NUMPAD_6,
|
||||
KeyEvent.KEYCODE_NUMPAD_ADD,
|
||||
KeyEvent.KEYCODE_NUMPAD_1,
|
||||
KeyEvent.KEYCODE_NUMPAD_2,
|
||||
KeyEvent.KEYCODE_NUMPAD_3,
|
||||
KeyEvent.KEYCODE_NUMPAD_0,
|
||||
KeyEvent.KEYCODE_NUMPAD_DOT,
|
||||
0,
|
||||
0, //KeyEvent.VK_ZENKAKUHANKAKU,
|
||||
0, //KeyEvent.VK_102ND,
|
||||
KeyEvent.KEYCODE_F11,
|
||||
KeyEvent.KEYCODE_F12,
|
||||
0, //KeyEvent.VK_RO,
|
||||
0, //KeyEvent.VK_KATAKANA,
|
||||
0, //KeyEvent.VK_HIRAGANA,
|
||||
0, //KeyEvent.VK_HENKAN,
|
||||
0, //KeyEvent.VK_KATAKANAHIRAGANA,
|
||||
0, //KeyEvent.VK_MUHENKAN,
|
||||
0, //KeyEvent.VK_KPJPCOMMA,
|
||||
KeyEvent.KEYCODE_NUMPAD_ENTER,
|
||||
KeyEvent.KEYCODE_CTRL_RIGHT,
|
||||
KeyEvent.KEYCODE_NUMPAD_DIVIDE,
|
||||
KeyEvent.KEYCODE_SYSRQ,
|
||||
KeyEvent.KEYCODE_ALT_RIGHT,
|
||||
0, //KeyEvent.VK_LINEFEED,
|
||||
KeyEvent.KEYCODE_HOME,
|
||||
KeyEvent.KEYCODE_DPAD_UP,
|
||||
KeyEvent.KEYCODE_PAGE_UP,
|
||||
KeyEvent.KEYCODE_DPAD_LEFT,
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT,
|
||||
KeyEvent.KEYCODE_MOVE_END,
|
||||
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||
KeyEvent.KEYCODE_PAGE_DOWN,
|
||||
KeyEvent.KEYCODE_INSERT,
|
||||
KeyEvent.KEYCODE_FORWARD_DEL,
|
||||
0, //KeyEvent.VK_MACRO,
|
||||
0, //KeyEvent.VK_MUTE,
|
||||
0, //KeyEvent.VK_VOLUMEDOWN,
|
||||
0, //KeyEvent.VK_VOLUMEUP,
|
||||
0, //KeyEvent.VK_POWER, /* SC System Power Down */
|
||||
KeyEvent.KEYCODE_NUMPAD_EQUALS,
|
||||
0, //KeyEvent.VK_KPPLUSMINUS,
|
||||
KeyEvent.KEYCODE_BREAK,
|
||||
0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */
|
||||
};
|
||||
|
||||
public static short translateEvdevKeyCode(short evdevKeyCode) {
|
||||
if (evdevKeyCode < EVDEV_KEY_CODES.length) {
|
||||
return EVDEV_KEY_CODES[evdevKeyCode];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,163 +10,179 @@ import android.os.FileObserver;
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
public class EvdevWatcher {
|
||||
private static final String PATH = "/dev/input";
|
||||
private static final String REQUIRED_FILE_PREFIX = "event";
|
||||
|
||||
private final HashMap<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>();
|
||||
private boolean shutdown = false;
|
||||
private boolean init = false;
|
||||
private boolean ungrabbed = false;
|
||||
private EvdevListener listener;
|
||||
private Thread startThread;
|
||||
|
||||
private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) {
|
||||
@Override
|
||||
public void onEvent(int event, String fileName) {
|
||||
if (fileName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) {
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (handlers) {
|
||||
if (shutdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((event & FileObserver.CREATE) != 0) {
|
||||
LimeLog.info("Starting evdev handler for "+fileName);
|
||||
|
||||
if (!init) {
|
||||
// If this a real new device, update permissions again so we can read it
|
||||
EvdevReader.setPermissions(new String[]{PATH + "/" + fileName}, 0666);
|
||||
}
|
||||
|
||||
EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener);
|
||||
|
||||
// If we're ungrabbed now, don't start the handler
|
||||
if (!ungrabbed) {
|
||||
handler.start();
|
||||
}
|
||||
|
||||
handlers.put(fileName, handler);
|
||||
}
|
||||
|
||||
if ((event & FileObserver.DELETE) != 0) {
|
||||
LimeLog.info("Halting evdev handler for "+fileName);
|
||||
|
||||
EvdevHandler handler = handlers.remove(fileName);
|
||||
if (handler != null) {
|
||||
handler.notifyDeleted();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public EvdevWatcher(EvdevListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
private File[] rundownWithPermissionsChange(int newPermissions) {
|
||||
// Rundown existing files
|
||||
File devInputDir = new File(PATH);
|
||||
File[] files = devInputDir.listFiles();
|
||||
if (files == null) {
|
||||
return new File[0];
|
||||
}
|
||||
|
||||
// Set desired permissions
|
||||
String[] filePaths = new String[files.length];
|
||||
for (int i = 0; i < files.length; i++) {
|
||||
filePaths[i] = files[i].getAbsolutePath();
|
||||
}
|
||||
EvdevReader.setPermissions(filePaths, newPermissions);
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
public void ungrabAll() {
|
||||
synchronized (handlers) {
|
||||
// Note that we're ungrabbed for now
|
||||
ungrabbed = true;
|
||||
|
||||
// Stop all handlers
|
||||
for (EvdevHandler handler : handlers.values()) {
|
||||
handler.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void regrabAll() {
|
||||
synchronized (handlers) {
|
||||
// We're regrabbing everything now
|
||||
ungrabbed = false;
|
||||
|
||||
for (Map.Entry<String, EvdevHandler> entry : handlers.entrySet()) {
|
||||
// We need to recreate each entry since we can't reuse a stopped one
|
||||
entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener));
|
||||
entry.getValue().start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
startThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// List all files and allow us access
|
||||
File[] files = rundownWithPermissionsChange(0666);
|
||||
|
||||
init = true;
|
||||
for (File f : files) {
|
||||
observer.onEvent(FileObserver.CREATE, f.getName());
|
||||
}
|
||||
|
||||
// Done with initial onEvent calls
|
||||
init = false;
|
||||
|
||||
// Start watching for new files
|
||||
observer.startWatching();
|
||||
|
||||
synchronized (startThread) {
|
||||
// Wait to be awoken again by shutdown()
|
||||
try {
|
||||
startThread.wait();
|
||||
} catch (InterruptedException e) {}
|
||||
}
|
||||
|
||||
// Giveup eventX permissions
|
||||
rundownWithPermissionsChange(066);
|
||||
}
|
||||
};
|
||||
startThread.start();
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
// Let start thread cleanup on it's own sweet time
|
||||
synchronized (startThread) {
|
||||
startThread.notify();
|
||||
}
|
||||
|
||||
// Stop the observer
|
||||
observer.stopWatching();
|
||||
|
||||
synchronized (handlers) {
|
||||
// Stop creating new handlers
|
||||
shutdown = true;
|
||||
|
||||
// If we've already ungrabbed, there's nothing else to do
|
||||
if (ungrabbed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop all handlers
|
||||
for (EvdevHandler handler : handlers.values()) {
|
||||
handler.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
private static final String PATH = "/dev/input";
|
||||
private static final String REQUIRED_FILE_PREFIX = "event";
|
||||
|
||||
private final HashMap<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>();
|
||||
private boolean shutdown = false;
|
||||
private boolean init = false;
|
||||
private boolean ungrabbed = false;
|
||||
private EvdevListener listener;
|
||||
private Thread startThread;
|
||||
|
||||
private static boolean patchedSeLinuxPolicies = false;
|
||||
|
||||
private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) {
|
||||
@Override
|
||||
public void onEvent(int event, String fileName) {
|
||||
if (fileName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) {
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (handlers) {
|
||||
if (shutdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((event & FileObserver.CREATE) != 0) {
|
||||
LimeLog.info("Starting evdev handler for "+fileName);
|
||||
|
||||
if (!init) {
|
||||
// If this a real new device, update permissions again so we can read it
|
||||
EvdevReader.setPermissions(new String[]{PATH + "/" + fileName}, 0666);
|
||||
}
|
||||
|
||||
EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener);
|
||||
|
||||
// If we're ungrabbed now, don't start the handler
|
||||
if (!ungrabbed) {
|
||||
handler.start();
|
||||
}
|
||||
|
||||
handlers.put(fileName, handler);
|
||||
}
|
||||
|
||||
if ((event & FileObserver.DELETE) != 0) {
|
||||
LimeLog.info("Halting evdev handler for "+fileName);
|
||||
|
||||
EvdevHandler handler = handlers.remove(fileName);
|
||||
if (handler != null) {
|
||||
handler.notifyDeleted();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public EvdevWatcher(EvdevListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
private File[] rundownWithPermissionsChange(int newPermissions) {
|
||||
// Rundown existing files
|
||||
File devInputDir = new File(PATH);
|
||||
File[] files = devInputDir.listFiles();
|
||||
if (files == null) {
|
||||
return new File[0];
|
||||
}
|
||||
|
||||
// Set desired permissions
|
||||
String[] filePaths = new String[files.length];
|
||||
for (int i = 0; i < files.length; i++) {
|
||||
filePaths[i] = files[i].getAbsolutePath();
|
||||
}
|
||||
EvdevReader.setPermissions(filePaths, newPermissions);
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
public void ungrabAll() {
|
||||
synchronized (handlers) {
|
||||
// Note that we're ungrabbed for now
|
||||
ungrabbed = true;
|
||||
|
||||
// Stop all handlers
|
||||
for (EvdevHandler handler : handlers.values()) {
|
||||
handler.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void regrabAll() {
|
||||
synchronized (handlers) {
|
||||
// We're regrabbing everything now
|
||||
ungrabbed = false;
|
||||
|
||||
for (Map.Entry<String, EvdevHandler> entry : handlers.entrySet()) {
|
||||
// We need to recreate each entry since we can't reuse a stopped one
|
||||
entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener));
|
||||
entry.getValue().start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
startThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Initialize the root shell
|
||||
EvdevShell.getInstance().startShell();
|
||||
|
||||
// Patch SELinux policies (if needed)
|
||||
if (!patchedSeLinuxPolicies) {
|
||||
EvdevReader.patchSeLinuxPolicies();
|
||||
patchedSeLinuxPolicies = true;
|
||||
}
|
||||
|
||||
// List all files and allow us access
|
||||
File[] files = rundownWithPermissionsChange(0666);
|
||||
|
||||
init = true;
|
||||
for (File f : files) {
|
||||
observer.onEvent(FileObserver.CREATE, f.getName());
|
||||
}
|
||||
|
||||
// Done with initial onEvent calls
|
||||
init = false;
|
||||
|
||||
// Start watching for new files
|
||||
observer.startWatching();
|
||||
|
||||
synchronized (startThread) {
|
||||
// Wait to be awoken again by shutdown()
|
||||
try {
|
||||
startThread.wait();
|
||||
} catch (InterruptedException e) {}
|
||||
}
|
||||
|
||||
// Giveup eventX permissions
|
||||
rundownWithPermissionsChange(0660);
|
||||
|
||||
// Kill the root shell
|
||||
try {
|
||||
EvdevShell.getInstance().stopShell();
|
||||
} catch (InterruptedException e) {}
|
||||
}
|
||||
};
|
||||
startThread.start();
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
// Let start thread cleanup on it's own sweet time
|
||||
synchronized (startThread) {
|
||||
startThread.notify();
|
||||
}
|
||||
|
||||
// Stop the observer
|
||||
observer.stopWatching();
|
||||
|
||||
synchronized (handlers) {
|
||||
// Stop creating new handlers
|
||||
shutdown = true;
|
||||
|
||||
// If we've already ungrabbed, there's nothing else to do
|
||||
if (ungrabbed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop all handlers
|
||||
for (EvdevHandler handler : handlers.values()) {
|
||||
handler.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
import android.graphics.PixelFormat;
|
||||
import android.os.Build;
|
||||
@@ -19,245 +18,271 @@ import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||
import com.limelight.nvstream.av.video.cpu.AvcDecoder;
|
||||
|
||||
@SuppressWarnings("EmptyCatchBlock")
|
||||
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
||||
public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
|
||||
|
||||
private Thread rendererThread;
|
||||
private int targetFps;
|
||||
|
||||
private static final int DECODER_BUFFER_SIZE = 92*1024;
|
||||
private ByteBuffer decoderBuffer;
|
||||
|
||||
// Only sleep if the difference is above this value
|
||||
private static final int WAIT_CEILING_MS = 8;
|
||||
|
||||
private static final int LOW_PERF = 1;
|
||||
private static final int MED_PERF = 2;
|
||||
private static final int HIGH_PERF = 3;
|
||||
|
||||
private int totalFrames;
|
||||
private long totalTimeMs;
|
||||
|
||||
private int cpuCount = Runtime.getRuntime().availableProcessors();
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private int findOptimalPerformanceLevel() {
|
||||
StringBuilder cpuInfo = new StringBuilder();
|
||||
BufferedReader br = null;
|
||||
try {
|
||||
br = new BufferedReader(new FileReader(new File("/proc/cpuinfo")));
|
||||
for (;;) {
|
||||
int ch = br.read();
|
||||
if (ch == -1)
|
||||
break;
|
||||
cpuInfo.append((char)ch);
|
||||
}
|
||||
|
||||
// Here we're doing very simple heuristics based on CPU model
|
||||
String cpuInfoStr = cpuInfo.toString();
|
||||
private Thread rendererThread, decoderThread;
|
||||
private int targetFps;
|
||||
|
||||
// We order them from greatest to least for proper detection
|
||||
// of devices with multiple sets of cores (like Exynos 5 Octa)
|
||||
// TODO Make this better (only even kind of works on ARM)
|
||||
if (Build.FINGERPRINT.contains("generic")) {
|
||||
// Emulator
|
||||
return LOW_PERF;
|
||||
}
|
||||
else if (cpuInfoStr.contains("0xc0f")) {
|
||||
// Cortex-A15
|
||||
return MED_PERF;
|
||||
}
|
||||
else if (cpuInfoStr.contains("0xc09")) {
|
||||
// Cortex-A9
|
||||
return LOW_PERF;
|
||||
}
|
||||
else if (cpuInfoStr.contains("0xc07")) {
|
||||
// Cortex-A7
|
||||
return LOW_PERF;
|
||||
}
|
||||
else {
|
||||
// Didn't have anything we're looking for
|
||||
return MED_PERF;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
} finally {
|
||||
if (br != null) {
|
||||
try {
|
||||
br.close();
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Couldn't read cpuinfo, so assume medium
|
||||
return MED_PERF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||
this.targetFps = redrawRate;
|
||||
|
||||
int perfLevel = LOW_PERF; //findOptimalPerformanceLevel();
|
||||
int threadCount;
|
||||
|
||||
int avcFlags = 0;
|
||||
switch (perfLevel) {
|
||||
case HIGH_PERF:
|
||||
// Single threaded low latency decode is ideal but hard to acheive
|
||||
avcFlags = AvcDecoder.LOW_LATENCY_DECODE;
|
||||
threadCount = 1;
|
||||
break;
|
||||
private static final int DECODER_BUFFER_SIZE = 92*1024;
|
||||
private ByteBuffer decoderBuffer;
|
||||
|
||||
case LOW_PERF:
|
||||
// Disable the loop filter for performance reasons
|
||||
avcFlags = AvcDecoder.DISABLE_LOOP_FILTER |
|
||||
AvcDecoder.FAST_BILINEAR_FILTERING |
|
||||
AvcDecoder.FAST_DECODE;
|
||||
|
||||
// Use plenty of threads to try to utilize the CPU as best we can
|
||||
threadCount = cpuCount - 1;
|
||||
break;
|
||||
// Only sleep if the difference is above this value
|
||||
private static final int WAIT_CEILING_MS = 5;
|
||||
|
||||
default:
|
||||
case MED_PERF:
|
||||
avcFlags = AvcDecoder.BILINEAR_FILTERING |
|
||||
AvcDecoder.FAST_DECODE;
|
||||
|
||||
// Only use 2 threads to minimize frame processing latency
|
||||
threadCount = 2;
|
||||
break;
|
||||
}
|
||||
|
||||
// If the user wants quality, we'll remove the low IQ flags
|
||||
if ((drFlags & VideoDecoderRenderer.FLAG_PREFER_QUALITY) != 0) {
|
||||
// Make sure the loop filter is enabled
|
||||
avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER;
|
||||
|
||||
// Disable the non-compliant speed optimizations
|
||||
avcFlags &= ~AvcDecoder.FAST_DECODE;
|
||||
|
||||
LimeLog.info("Using high quality decoding");
|
||||
}
|
||||
|
||||
SurfaceHolder sh = (SurfaceHolder)renderTarget;
|
||||
sh.setFormat(PixelFormat.RGBX_8888);
|
||||
|
||||
int err = AvcDecoder.init(width, height, avcFlags, threadCount);
|
||||
if (err != 0) {
|
||||
throw new IllegalStateException("AVC decoder initialization failure: "+err);
|
||||
}
|
||||
|
||||
AvcDecoder.setRenderTarget(sh.getSurface());
|
||||
|
||||
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
|
||||
|
||||
LimeLog.info("Using software decoding (performance level: "+perfLevel+")");
|
||||
|
||||
return true;
|
||||
}
|
||||
private static final int LOW_PERF = 1;
|
||||
private static final int MED_PERF = 2;
|
||||
private static final int HIGH_PERF = 3;
|
||||
|
||||
@Override
|
||||
public boolean start(final VideoDepacketizer depacketizer) {
|
||||
rendererThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
long nextFrameTime = System.currentTimeMillis();
|
||||
DecodeUnit du;
|
||||
while (!isInterrupted())
|
||||
{
|
||||
du = depacketizer.pollNextDecodeUnit();
|
||||
if (du != null) {
|
||||
submitDecodeUnit(du);
|
||||
depacketizer.freeDecodeUnit(du);
|
||||
}
|
||||
|
||||
long diff = nextFrameTime - System.currentTimeMillis();
|
||||
private int totalFrames;
|
||||
private long totalTimeMs;
|
||||
|
||||
if (diff > WAIT_CEILING_MS) {
|
||||
LockSupport.parkNanos(1);
|
||||
continue;
|
||||
}
|
||||
private final int cpuCount = Runtime.getRuntime().availableProcessors();
|
||||
|
||||
nextFrameTime = computePresentationTimeMs(targetFps);
|
||||
AvcDecoder.redraw();
|
||||
}
|
||||
}
|
||||
};
|
||||
rendererThread.setName("Video - Renderer (CPU)");
|
||||
rendererThread.setPriority(Thread.MAX_PRIORITY);
|
||||
rendererThread.start();
|
||||
return true;
|
||||
}
|
||||
|
||||
private long computePresentationTimeMs(int frameRate) {
|
||||
return System.currentTimeMillis() + (1000 / frameRate);
|
||||
}
|
||||
@SuppressWarnings("unused")
|
||||
private int findOptimalPerformanceLevel() {
|
||||
StringBuilder cpuInfo = new StringBuilder();
|
||||
BufferedReader br = null;
|
||||
try {
|
||||
br = new BufferedReader(new FileReader(new File("/proc/cpuinfo")));
|
||||
for (;;) {
|
||||
int ch = br.read();
|
||||
if (ch == -1)
|
||||
break;
|
||||
cpuInfo.append((char)ch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
rendererThread.interrupt();
|
||||
|
||||
try {
|
||||
rendererThread.join();
|
||||
} catch (InterruptedException e) { }
|
||||
}
|
||||
// Here we're doing very simple heuristics based on CPU model
|
||||
String cpuInfoStr = cpuInfo.toString();
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
AvcDecoder.destroy();
|
||||
}
|
||||
// We order them from greatest to least for proper detection
|
||||
// of devices with multiple sets of cores (like Exynos 5 Octa)
|
||||
// TODO Make this better (only even kind of works on ARM)
|
||||
if (Build.FINGERPRINT.contains("generic")) {
|
||||
// Emulator
|
||||
return LOW_PERF;
|
||||
}
|
||||
else if (cpuInfoStr.contains("0xc0f")) {
|
||||
// Cortex-A15
|
||||
return MED_PERF;
|
||||
}
|
||||
else if (cpuInfoStr.contains("0xc09")) {
|
||||
// Cortex-A9
|
||||
return LOW_PERF;
|
||||
}
|
||||
else if (cpuInfoStr.contains("0xc07")) {
|
||||
// Cortex-A7
|
||||
return LOW_PERF;
|
||||
}
|
||||
else {
|
||||
// Didn't have anything we're looking for
|
||||
return MED_PERF;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
} finally {
|
||||
if (br != null) {
|
||||
try {
|
||||
br.close();
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
||||
byte[] data;
|
||||
|
||||
// Use the reserved decoder buffer if this decode unit will fit
|
||||
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
|
||||
decoderBuffer.clear();
|
||||
|
||||
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
|
||||
decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
|
||||
}
|
||||
|
||||
data = decoderBuffer.array();
|
||||
}
|
||||
else {
|
||||
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
|
||||
|
||||
int offset = 0;
|
||||
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
|
||||
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
|
||||
offset += bbd.length;
|
||||
}
|
||||
}
|
||||
|
||||
boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
|
||||
if (success) {
|
||||
long timeAfterDecode = System.currentTimeMillis();
|
||||
|
||||
// Add delta time to the totals (excluding probable outliers)
|
||||
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
|
||||
if (delta >= 0 && delta < 300) {
|
||||
totalTimeMs += delta;
|
||||
totalFrames++;
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
// Couldn't read cpuinfo, so assume medium
|
||||
return MED_PERF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCapabilities() {
|
||||
return 0;
|
||||
}
|
||||
@Override
|
||||
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||
this.targetFps = redrawRate;
|
||||
|
||||
@Override
|
||||
public int getAverageDecoderLatency() {
|
||||
return 0;
|
||||
}
|
||||
int perfLevel = LOW_PERF; //findOptimalPerformanceLevel();
|
||||
int threadCount;
|
||||
|
||||
@Override
|
||||
public int getAverageEndToEndLatency() {
|
||||
if (totalFrames == 0) {
|
||||
return 0;
|
||||
}
|
||||
return (int)(totalTimeMs / totalFrames);
|
||||
}
|
||||
int avcFlags = 0;
|
||||
switch (perfLevel) {
|
||||
case HIGH_PERF:
|
||||
// Single threaded low latency decode is ideal but hard to acheive
|
||||
avcFlags = AvcDecoder.LOW_LATENCY_DECODE;
|
||||
threadCount = 1;
|
||||
break;
|
||||
|
||||
case LOW_PERF:
|
||||
// Disable the loop filter for performance reasons
|
||||
avcFlags = AvcDecoder.FAST_BILINEAR_FILTERING;
|
||||
|
||||
// Use plenty of threads to try to utilize the CPU as best we can
|
||||
threadCount = cpuCount - 1;
|
||||
break;
|
||||
|
||||
default:
|
||||
case MED_PERF:
|
||||
avcFlags = AvcDecoder.BILINEAR_FILTERING;
|
||||
|
||||
// Only use 2 threads to minimize frame processing latency
|
||||
threadCount = 2;
|
||||
break;
|
||||
}
|
||||
|
||||
// If the user wants quality, we'll remove the low IQ flags
|
||||
if ((drFlags & VideoDecoderRenderer.FLAG_PREFER_QUALITY) != 0) {
|
||||
// Make sure the loop filter is enabled
|
||||
avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER;
|
||||
|
||||
// Disable the non-compliant speed optimizations
|
||||
avcFlags &= ~AvcDecoder.FAST_DECODE;
|
||||
|
||||
LimeLog.info("Using high quality decoding");
|
||||
}
|
||||
|
||||
SurfaceHolder sh = (SurfaceHolder)renderTarget;
|
||||
sh.setFormat(PixelFormat.RGBX_8888);
|
||||
|
||||
int err = AvcDecoder.init(width, height, avcFlags, threadCount);
|
||||
if (err != 0) {
|
||||
throw new IllegalStateException("AVC decoder initialization failure: "+err);
|
||||
}
|
||||
|
||||
if (!AvcDecoder.setRenderTarget(sh.getSurface())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
|
||||
|
||||
LimeLog.info("Using software decoding (performance level: "+perfLevel+")");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean start(final VideoDepacketizer depacketizer) {
|
||||
decoderThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
DecodeUnit du;
|
||||
while (!isInterrupted()) {
|
||||
try {
|
||||
du = depacketizer.takeNextDecodeUnit();
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
|
||||
submitDecodeUnit(du);
|
||||
depacketizer.freeDecodeUnit(du);
|
||||
}
|
||||
}
|
||||
};
|
||||
decoderThread.setName("Video - Decoder (CPU)");
|
||||
decoderThread.setPriority(Thread.MAX_PRIORITY - 1);
|
||||
decoderThread.start();
|
||||
|
||||
rendererThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
long nextFrameTime = System.currentTimeMillis();
|
||||
DecodeUnit du;
|
||||
while (!isInterrupted())
|
||||
{
|
||||
long diff = nextFrameTime - System.currentTimeMillis();
|
||||
|
||||
if (diff > WAIT_CEILING_MS) {
|
||||
try {
|
||||
Thread.sleep(diff - WAIT_CEILING_MS);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
nextFrameTime = computePresentationTimeMs(targetFps);
|
||||
AvcDecoder.redraw();
|
||||
}
|
||||
}
|
||||
};
|
||||
rendererThread.setName("Video - Renderer (CPU)");
|
||||
rendererThread.setPriority(Thread.MAX_PRIORITY);
|
||||
rendererThread.start();
|
||||
return true;
|
||||
}
|
||||
|
||||
private long computePresentationTimeMs(int frameRate) {
|
||||
return System.currentTimeMillis() + (1000 / frameRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
rendererThread.interrupt();
|
||||
decoderThread.interrupt();
|
||||
|
||||
try {
|
||||
rendererThread.join();
|
||||
} catch (InterruptedException e) { }
|
||||
try {
|
||||
decoderThread.join();
|
||||
} catch (InterruptedException e) { }
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
AvcDecoder.destroy();
|
||||
}
|
||||
|
||||
private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
||||
byte[] data;
|
||||
|
||||
// Use the reserved decoder buffer if this decode unit will fit
|
||||
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
|
||||
decoderBuffer.clear();
|
||||
|
||||
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
|
||||
decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
|
||||
}
|
||||
|
||||
data = decoderBuffer.array();
|
||||
}
|
||||
else {
|
||||
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
|
||||
|
||||
int offset = 0;
|
||||
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
|
||||
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
|
||||
offset += bbd.length;
|
||||
}
|
||||
}
|
||||
|
||||
boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
|
||||
if (success) {
|
||||
long timeAfterDecode = System.currentTimeMillis();
|
||||
|
||||
// Add delta time to the totals (excluding probable outliers)
|
||||
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
|
||||
if (delta >= 0 && delta < 1000) {
|
||||
totalTimeMs += delta;
|
||||
totalFrames++;
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCapabilities() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAverageDecoderLatency() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAverageEndToEndLatency() {
|
||||
if (totalFrames == 0) {
|
||||
return 0;
|
||||
}
|
||||
return (int)(totalTimeMs / totalFrames);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDecoderName() {
|
||||
return "CPU decoding";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,75 +3,85 @@ package com.limelight.binding.video;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||
|
||||
public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
|
||||
public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer {
|
||||
|
||||
private VideoDecoderRenderer decoderRenderer;
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (decoderRenderer != null) {
|
||||
decoderRenderer.release();
|
||||
}
|
||||
}
|
||||
private EnhancedDecoderRenderer decoderRenderer;
|
||||
|
||||
@Override
|
||||
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||
if (decoderRenderer == null) {
|
||||
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
|
||||
}
|
||||
return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
|
||||
}
|
||||
|
||||
public void initializeWithFlags(int drFlags) {
|
||||
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
|
||||
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
|
||||
MediaCodecHelper.findProbableSafeDecoder() != null)) {
|
||||
decoderRenderer = new MediaCodecDecoderRenderer();
|
||||
}
|
||||
else {
|
||||
decoderRenderer = new AndroidCpuDecoderRenderer();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isHardwareAccelerated() {
|
||||
if (decoderRenderer == null) {
|
||||
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
|
||||
}
|
||||
return (decoderRenderer instanceof MediaCodecDecoderRenderer);
|
||||
}
|
||||
@Override
|
||||
public void release() {
|
||||
if (decoderRenderer != null) {
|
||||
decoderRenderer.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean start(VideoDepacketizer depacketizer) {
|
||||
return decoderRenderer.start(depacketizer);
|
||||
}
|
||||
@Override
|
||||
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||
if (decoderRenderer == null) {
|
||||
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
|
||||
}
|
||||
return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
decoderRenderer.stop();
|
||||
}
|
||||
public void initializeWithFlags(int drFlags) {
|
||||
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
|
||||
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
|
||||
MediaCodecHelper.findProbableSafeDecoder() != null)) {
|
||||
decoderRenderer = new MediaCodecDecoderRenderer();
|
||||
}
|
||||
else {
|
||||
decoderRenderer = new AndroidCpuDecoderRenderer();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCapabilities() {
|
||||
return decoderRenderer.getCapabilities();
|
||||
}
|
||||
public boolean isHardwareAccelerated() {
|
||||
if (decoderRenderer == null) {
|
||||
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
|
||||
}
|
||||
return (decoderRenderer instanceof MediaCodecDecoderRenderer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAverageDecoderLatency() {
|
||||
if (decoderRenderer != null) {
|
||||
return decoderRenderer.getAverageDecoderLatency();
|
||||
}
|
||||
else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public boolean start(VideoDepacketizer depacketizer) {
|
||||
return decoderRenderer.start(depacketizer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAverageEndToEndLatency() {
|
||||
if (decoderRenderer != null) {
|
||||
return decoderRenderer.getAverageEndToEndLatency();
|
||||
}
|
||||
else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void stop() {
|
||||
decoderRenderer.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCapabilities() {
|
||||
return decoderRenderer.getCapabilities();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAverageDecoderLatency() {
|
||||
if (decoderRenderer != null) {
|
||||
return decoderRenderer.getAverageDecoderLatency();
|
||||
}
|
||||
else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAverageEndToEndLatency() {
|
||||
if (decoderRenderer != null) {
|
||||
return decoderRenderer.getAverageEndToEndLatency();
|
||||
}
|
||||
else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDecoderName() {
|
||||
if (decoderRenderer != null) {
|
||||
return decoderRenderer.getDecoderName();
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.limelight.binding.video;
|
||||
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
|
||||
public abstract class EnhancedDecoderRenderer implements VideoDecoderRenderer {
|
||||
public abstract String getDecoderName();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,11 +20,12 @@ import com.limelight.LimeLog;
|
||||
|
||||
public class MediaCodecHelper {
|
||||
|
||||
public static final List<String> preferredDecoders;
|
||||
private static final List<String> preferredDecoders;
|
||||
|
||||
public static final List<String> blacklistedDecoderPrefixes;
|
||||
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
||||
public static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
||||
private static final List<String> blacklistedDecoderPrefixes;
|
||||
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
||||
private static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
||||
private static final List<String> baselineProfileHackPrefixes;
|
||||
|
||||
static {
|
||||
preferredDecoders = new LinkedList<String>();
|
||||
@@ -43,7 +44,11 @@ public class MediaCodecHelper {
|
||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
|
||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
|
||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk");
|
||||
|
||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
|
||||
|
||||
baselineProfileHackPrefixes = new LinkedList<String>();
|
||||
baselineProfileHackPrefixes.add("omx.intel");
|
||||
|
||||
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
|
||||
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
|
||||
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
||||
@@ -66,6 +71,10 @@ public class MediaCodecHelper {
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) {
|
||||
/*
|
||||
FIXME: Intel's decoder on Nexus Player forces the high latency path if adaptive playback is enabled
|
||||
so we'll keep it off for now, since we don't know whether other devices also do the same
|
||||
|
||||
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
|
||||
LimeLog.info("Adaptive playback supported (whitelist)");
|
||||
return true;
|
||||
@@ -84,7 +93,7 @@ public class MediaCodecHelper {
|
||||
} catch (Exception e) {
|
||||
// Tolerate buggy codecs
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -92,6 +101,10 @@ public class MediaCodecHelper {
|
||||
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
|
||||
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
|
||||
}
|
||||
|
||||
public static boolean decoderNeedsBaselineSpsHack(String decoderName, MediaCodecInfo decoderInfo) {
|
||||
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressLint("NewApi")
|
||||
@@ -133,7 +146,7 @@ public class MediaCodecHelper {
|
||||
return str;
|
||||
}
|
||||
|
||||
public static MediaCodecInfo findPreferredDecoder() {
|
||||
private static MediaCodecInfo findPreferredDecoder() {
|
||||
// This is a different algorithm than the other findXXXDecoder functions,
|
||||
// because we want to evaluate the decoders in our list's order
|
||||
// rather than MediaCodecList's order
|
||||
@@ -204,7 +217,7 @@ public class MediaCodecHelper {
|
||||
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
|
||||
// and we want to be sure all callers are handling this possibility
|
||||
@SuppressWarnings("RedundantThrows")
|
||||
public static MediaCodecInfo findKnownSafeDecoder() throws Exception {
|
||||
private static MediaCodecInfo findKnownSafeDecoder() throws Exception {
|
||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||
// Skip encoders
|
||||
if (codecInfo.isEncoder()) {
|
||||
|
||||
@@ -17,148 +17,153 @@ import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
|
||||
public class ComputerDatabaseManager {
|
||||
private static final String COMPUTER_DB_NAME = "computers.db";
|
||||
private static final String COMPUTER_TABLE_NAME = "Computers";
|
||||
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
|
||||
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
|
||||
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
|
||||
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
|
||||
private static final String MAC_COLUMN_NAME = "Mac";
|
||||
|
||||
private SQLiteDatabase computerDb;
|
||||
|
||||
public ComputerDatabaseManager(Context c) {
|
||||
try {
|
||||
// Create or open an existing DB
|
||||
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
||||
} catch (SQLiteException e) {
|
||||
// Delete the DB and try again
|
||||
c.deleteDatabase(COMPUTER_DB_NAME);
|
||||
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
||||
}
|
||||
initializeDb();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
computerDb.close();
|
||||
}
|
||||
|
||||
private void initializeDb() {
|
||||
// Create tables if they aren't already there
|
||||
computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," +
|
||||
" %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
|
||||
COMPUTER_TABLE_NAME,
|
||||
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
|
||||
REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME));
|
||||
}
|
||||
|
||||
public void deleteComputer(String name) {
|
||||
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
||||
}
|
||||
|
||||
public boolean updateComputer(ComputerDetails details) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
||||
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
|
||||
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
|
||||
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
|
||||
values.put(MAC_COLUMN_NAME, details.macAddress);
|
||||
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
|
||||
public List<ComputerDetails> getAllComputers() {
|
||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
||||
LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>();
|
||||
while (c.moveToNext()) {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
|
||||
details.name = c.getString(0);
|
||||
|
||||
String uuidStr = c.getString(1);
|
||||
try {
|
||||
details.uuid = UUID.fromString(uuidStr);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
||||
}
|
||||
|
||||
try {
|
||||
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
||||
} catch (UnknownHostException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
||||
}
|
||||
|
||||
try {
|
||||
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
||||
} catch (UnknownHostException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
||||
}
|
||||
|
||||
details.macAddress = c.getString(4);
|
||||
|
||||
// This signifies we don't have dynamic state (like pair state)
|
||||
details.state = ComputerDetails.State.UNKNOWN;
|
||||
|
||||
// If a field is corrupt or missing, skip the database entry
|
||||
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
||||
details.macAddress == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
computerList.add(details);
|
||||
}
|
||||
|
||||
c.close();
|
||||
|
||||
return computerList;
|
||||
}
|
||||
|
||||
public ComputerDetails getComputerByName(String name) {
|
||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
if (!c.moveToFirst()) {
|
||||
// No matching computer
|
||||
c.close();
|
||||
return null;
|
||||
}
|
||||
private static final String COMPUTER_DB_NAME = "computers.db";
|
||||
private static final String COMPUTER_TABLE_NAME = "Computers";
|
||||
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
|
||||
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
|
||||
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
|
||||
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
|
||||
private static final String MAC_COLUMN_NAME = "Mac";
|
||||
|
||||
details.name = c.getString(0);
|
||||
|
||||
String uuidStr = c.getString(1);
|
||||
try {
|
||||
details.uuid = UUID.fromString(uuidStr);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
||||
}
|
||||
|
||||
try {
|
||||
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
||||
} catch (UnknownHostException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
||||
}
|
||||
|
||||
try {
|
||||
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
||||
} catch (UnknownHostException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
||||
}
|
||||
|
||||
details.macAddress = c.getString(4);
|
||||
|
||||
c.close();
|
||||
|
||||
// If a field is corrupt or missing, delete the database entry
|
||||
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
||||
details.macAddress == null) {
|
||||
deleteComputer(details.name);
|
||||
return null;
|
||||
}
|
||||
|
||||
return details;
|
||||
}
|
||||
private SQLiteDatabase computerDb;
|
||||
|
||||
public ComputerDatabaseManager(Context c) {
|
||||
try {
|
||||
// Create or open an existing DB
|
||||
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
||||
} catch (SQLiteException e) {
|
||||
// Delete the DB and try again
|
||||
c.deleteDatabase(COMPUTER_DB_NAME);
|
||||
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
||||
}
|
||||
initializeDb();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
computerDb.close();
|
||||
}
|
||||
|
||||
private void initializeDb() {
|
||||
// Create tables if they aren't already there
|
||||
computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," +
|
||||
" %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
|
||||
COMPUTER_TABLE_NAME,
|
||||
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
|
||||
REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME));
|
||||
}
|
||||
|
||||
public void deleteComputer(String name) {
|
||||
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
||||
}
|
||||
|
||||
public boolean updateComputer(ComputerDetails details) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
||||
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
|
||||
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
|
||||
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
|
||||
values.put(MAC_COLUMN_NAME, details.macAddress);
|
||||
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
|
||||
public List<ComputerDetails> getAllComputers() {
|
||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
||||
LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>();
|
||||
while (c.moveToNext()) {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
|
||||
details.name = c.getString(0);
|
||||
|
||||
String uuidStr = c.getString(1);
|
||||
try {
|
||||
details.uuid = UUID.fromString(uuidStr);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
||||
}
|
||||
|
||||
try {
|
||||
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
||||
} catch (UnknownHostException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
||||
}
|
||||
|
||||
try {
|
||||
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
||||
} catch (UnknownHostException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
||||
}
|
||||
|
||||
details.macAddress = c.getString(4);
|
||||
|
||||
// This signifies we don't have dynamic state (like pair state)
|
||||
details.state = ComputerDetails.State.UNKNOWN;
|
||||
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||
|
||||
// If a field is corrupt or missing, skip the database entry
|
||||
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
||||
details.macAddress == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
computerList.add(details);
|
||||
}
|
||||
|
||||
c.close();
|
||||
|
||||
return computerList;
|
||||
}
|
||||
|
||||
public ComputerDetails getComputerByName(String name) {
|
||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
if (!c.moveToFirst()) {
|
||||
// No matching computer
|
||||
c.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
details.name = c.getString(0);
|
||||
|
||||
String uuidStr = c.getString(1);
|
||||
try {
|
||||
details.uuid = UUID.fromString(uuidStr);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
||||
}
|
||||
|
||||
try {
|
||||
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
||||
} catch (UnknownHostException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
||||
}
|
||||
|
||||
try {
|
||||
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
||||
} catch (UnknownHostException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
||||
}
|
||||
|
||||
details.macAddress = c.getString(4);
|
||||
|
||||
c.close();
|
||||
|
||||
details.state = ComputerDetails.State.UNKNOWN;
|
||||
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||
|
||||
// If a field is corrupt or missing, delete the database entry
|
||||
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
||||
details.macAddress == null) {
|
||||
deleteComputer(details.name);
|
||||
return null;
|
||||
}
|
||||
|
||||
return details;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@ package com.limelight.computers;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
|
||||
public interface ComputerManagerListener {
|
||||
public void notifyComputerUpdated(ComputerDetails details);
|
||||
public void notifyComputerUpdated(ComputerDetails details);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
package com.limelight.computers;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.net.InetAddress;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.discovery.DiscoveryService;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
import com.limelight.nvstream.mdns.MdnsComputer;
|
||||
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
||||
import com.limelight.utils.CacheHelper;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
@@ -25,45 +26,46 @@ import android.content.ServiceConnection;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
public class ComputerManagerService extends Service {
|
||||
private static final int POLLING_PERIOD_MS = 5000;
|
||||
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||
|
||||
private ComputerManagerBinder binder = new ComputerManagerBinder();
|
||||
|
||||
private ComputerDatabaseManager dbManager;
|
||||
private AtomicInteger dbRefCount = new AtomicInteger(0);
|
||||
|
||||
private IdentityManager idManager;
|
||||
private HashMap<ComputerDetails, Thread> pollingThreads;
|
||||
private ComputerManagerListener listener = null;
|
||||
private AtomicInteger activePolls = new AtomicInteger(0);
|
||||
private static final int POLLING_PERIOD_MS = 3000;
|
||||
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||
|
||||
private DiscoveryService.DiscoveryBinder discoveryBinder;
|
||||
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||
synchronized (discoveryServiceConnection) {
|
||||
DiscoveryService.DiscoveryBinder privateBinder = ((DiscoveryService.DiscoveryBinder)binder);
|
||||
|
||||
// Set us as the event listener
|
||||
privateBinder.setListener(createDiscoveryListener());
|
||||
|
||||
// Signal a possible waiter that we're all setup
|
||||
discoveryBinder = privateBinder;
|
||||
discoveryServiceConnection.notifyAll();
|
||||
}
|
||||
}
|
||||
private final ComputerManagerBinder binder = new ComputerManagerBinder();
|
||||
|
||||
public void onServiceDisconnected(ComponentName className) {
|
||||
discoveryBinder = null;
|
||||
}
|
||||
};
|
||||
private ComputerDatabaseManager dbManager;
|
||||
private final AtomicInteger dbRefCount = new AtomicInteger(0);
|
||||
|
||||
private IdentityManager idManager;
|
||||
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
|
||||
private ComputerManagerListener listener = null;
|
||||
private final AtomicInteger activePolls = new AtomicInteger(0);
|
||||
private boolean pollingActive = false;
|
||||
|
||||
private DiscoveryService.DiscoveryBinder discoveryBinder;
|
||||
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||
synchronized (discoveryServiceConnection) {
|
||||
DiscoveryService.DiscoveryBinder privateBinder = ((DiscoveryService.DiscoveryBinder)binder);
|
||||
|
||||
// Set us as the event listener
|
||||
privateBinder.setListener(createDiscoveryListener());
|
||||
|
||||
// Signal a possible waiter that we're all setup
|
||||
discoveryBinder = privateBinder;
|
||||
discoveryServiceConnection.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
public void onServiceDisconnected(ComponentName className) {
|
||||
discoveryBinder = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Returns true if the details object was modified
|
||||
private boolean runPoll(ComputerDetails details)
|
||||
private boolean runPoll(ComputerDetails details, boolean newPc)
|
||||
{
|
||||
boolean newPc = (details.name == null);
|
||||
|
||||
if (!getLocalDatabaseReference()) {
|
||||
return false;
|
||||
}
|
||||
@@ -106,18 +108,9 @@ public class ComputerManagerService extends Service {
|
||||
Thread t = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
while (!isInterrupted()) {
|
||||
ComputerDetails originalDetails = new ComputerDetails();
|
||||
originalDetails.update(details);
|
||||
|
||||
while (!isInterrupted() && pollingActive) {
|
||||
// Check if this poll has modified the details
|
||||
if (runPoll(details) && !originalDetails.equals(details)) {
|
||||
// Replace our thread entry with the new one
|
||||
synchronized (pollingThreads) {
|
||||
pollingThreads.remove(originalDetails);
|
||||
pollingThreads.put(details, this);
|
||||
}
|
||||
}
|
||||
runPoll(details, false);
|
||||
|
||||
// Wait until the next polling interval
|
||||
try {
|
||||
@@ -131,258 +124,456 @@ public class ComputerManagerService extends Service {
|
||||
t.setName("Polling thread for "+details.localIp.getHostAddress());
|
||||
return t;
|
||||
}
|
||||
|
||||
public class ComputerManagerBinder extends Binder {
|
||||
public void startPolling(ComputerManagerListener listener) {
|
||||
// Set the listener
|
||||
ComputerManagerService.this.listener = listener;
|
||||
|
||||
// Start mDNS autodiscovery too
|
||||
discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS);
|
||||
|
||||
// Start polling known machines
|
||||
if (!getLocalDatabaseReference()) {
|
||||
return;
|
||||
}
|
||||
List<ComputerDetails> computerList = dbManager.getAllComputers();
|
||||
releaseLocalDatabaseReference();
|
||||
|
||||
synchronized (pollingThreads) {
|
||||
for (ComputerDetails computer : computerList) {
|
||||
public class ComputerManagerBinder extends Binder {
|
||||
public void startPolling(ComputerManagerListener listener) {
|
||||
// Polling is active
|
||||
pollingActive = true;
|
||||
|
||||
// Set the listener
|
||||
ComputerManagerService.this.listener = listener;
|
||||
|
||||
// Start mDNS autodiscovery too
|
||||
discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS);
|
||||
|
||||
synchronized (pollingTuples) {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
// This polling thread might already be there
|
||||
if (!pollingThreads.containsKey(computer)) {
|
||||
Thread t = createPollingThread(computer);
|
||||
pollingThreads.put(computer, t);
|
||||
t.start();
|
||||
if (tuple.thread == null) {
|
||||
// Report this computer initially
|
||||
listener.notifyComputerUpdated(tuple.computer);
|
||||
|
||||
tuple.thread = createPollingThread(tuple.computer);
|
||||
tuple.thread.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void waitForReady() {
|
||||
synchronized (discoveryServiceConnection) {
|
||||
try {
|
||||
while (discoveryBinder == null) {
|
||||
// Wait for the bind notification
|
||||
discoveryServiceConnection.wait(1000);
|
||||
}
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void waitForPollingStopped() {
|
||||
while (activePolls.get() != 0) {
|
||||
try {
|
||||
Thread.sleep(250);
|
||||
} catch (InterruptedException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean addComputerBlocking(InetAddress addr) {
|
||||
return ComputerManagerService.this.addComputerBlocking(addr);
|
||||
}
|
||||
|
||||
public void addComputer(InetAddress addr) {
|
||||
ComputerManagerService.this.addComputer(addr);
|
||||
}
|
||||
|
||||
public void removeComputer(String name) {
|
||||
ComputerManagerService.this.removeComputer(name);
|
||||
}
|
||||
|
||||
public void stopPolling() {
|
||||
// Just call the unbind handler to cleanup
|
||||
ComputerManagerService.this.onUnbind(null);
|
||||
}
|
||||
|
||||
public String getUniqueId() {
|
||||
return idManager.getUniqueId();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onUnbind(Intent intent) {
|
||||
// Stop mDNS autodiscovery
|
||||
discoveryBinder.stopDiscovery();
|
||||
|
||||
// Stop polling
|
||||
synchronized (pollingThreads) {
|
||||
for (Thread t : pollingThreads.values()) {
|
||||
t.interrupt();
|
||||
}
|
||||
pollingThreads.clear();
|
||||
}
|
||||
|
||||
// Remove the listener
|
||||
listener = null;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private MdnsDiscoveryListener createDiscoveryListener() {
|
||||
return new MdnsDiscoveryListener() {
|
||||
@Override
|
||||
public void notifyComputerAdded(MdnsComputer computer) {
|
||||
// Kick off a serverinfo poll on this machine
|
||||
addComputer(computer.getAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyComputerRemoved(MdnsComputer computer) {
|
||||
// Nothing to do here
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyDiscoveryFailure(Exception e) {
|
||||
LimeLog.severe("mDNS discovery failed");
|
||||
e.printStackTrace();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void addComputer(InetAddress addr) {
|
||||
// Setup a placeholder
|
||||
ComputerDetails fakeDetails = new ComputerDetails();
|
||||
fakeDetails.localIp = addr;
|
||||
fakeDetails.remoteIp = addr;
|
||||
|
||||
// Spawn a thread for this computer
|
||||
synchronized (pollingThreads) {
|
||||
// This polling thread might already be there
|
||||
if (!pollingThreads.containsKey(fakeDetails)) {
|
||||
Thread t = createPollingThread(fakeDetails);
|
||||
pollingThreads.put(fakeDetails, t);
|
||||
t.start();
|
||||
public void waitForReady() {
|
||||
synchronized (discoveryServiceConnection) {
|
||||
try {
|
||||
while (discoveryBinder == null) {
|
||||
// Wait for the bind notification
|
||||
discoveryServiceConnection.wait(1000);
|
||||
}
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean addComputerBlocking(InetAddress addr) {
|
||||
// Setup a placeholder
|
||||
ComputerDetails fakeDetails = new ComputerDetails();
|
||||
fakeDetails.localIp = addr;
|
||||
fakeDetails.remoteIp = addr;
|
||||
|
||||
// Block while we try to fill the details
|
||||
runPoll(fakeDetails);
|
||||
|
||||
// If the machine is reachable, it was successful
|
||||
return fakeDetails.state == ComputerDetails.State.ONLINE;
|
||||
}
|
||||
|
||||
public void removeComputer(String name) {
|
||||
if (!getLocalDatabaseReference()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove it from the database
|
||||
dbManager.deleteComputer(name);
|
||||
|
||||
releaseLocalDatabaseReference();
|
||||
}
|
||||
|
||||
private boolean getLocalDatabaseReference() {
|
||||
if (dbRefCount.get() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
dbRefCount.incrementAndGet();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void releaseLocalDatabaseReference() {
|
||||
if (dbRefCount.decrementAndGet() == 0) {
|
||||
dbManager.close();
|
||||
}
|
||||
}
|
||||
|
||||
private ComputerDetails tryPollIp(InetAddress ipAddr) {
|
||||
try {
|
||||
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
|
||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
public void waitForPollingStopped() {
|
||||
while (activePolls.get() != 0) {
|
||||
try {
|
||||
Thread.sleep(250);
|
||||
} catch (InterruptedException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
return http.getComputerDetails();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean pollComputer(ComputerDetails details, boolean localFirst) {
|
||||
ComputerDetails polledDetails;
|
||||
|
||||
if (localFirst) {
|
||||
polledDetails = tryPollIp(details.localIp);
|
||||
}
|
||||
else {
|
||||
polledDetails = tryPollIp(details.remoteIp);
|
||||
}
|
||||
|
||||
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
|
||||
// Failed, so let's try the fallback
|
||||
if (!localFirst) {
|
||||
polledDetails = tryPollIp(details.localIp);
|
||||
}
|
||||
else {
|
||||
polledDetails = tryPollIp(details.remoteIp);
|
||||
}
|
||||
|
||||
// The fallback poll worked
|
||||
if (polledDetails != null) {
|
||||
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL :
|
||||
ComputerDetails.Reachability.REMOTE;
|
||||
}
|
||||
}
|
||||
else if (polledDetails != null) {
|
||||
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
|
||||
ComputerDetails.Reachability.REMOTE;
|
||||
}
|
||||
|
||||
// Machine was unreachable both tries
|
||||
if (polledDetails == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we got here, it's reachable
|
||||
details.update(polledDetails);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean doPollMachine(ComputerDetails details) {
|
||||
return pollComputer(details, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
// Bind to the discovery service
|
||||
bindService(new Intent(this, DiscoveryService.class),
|
||||
discoveryServiceConnection, Service.BIND_AUTO_CREATE);
|
||||
public boolean addComputerBlocking(InetAddress addr) {
|
||||
return ComputerManagerService.this.addComputerBlocking(addr);
|
||||
}
|
||||
|
||||
pollingThreads = new HashMap<ComputerDetails, Thread>();
|
||||
|
||||
// Lookup or generate this device's UID
|
||||
idManager = new IdentityManager(this);
|
||||
|
||||
// Initialize the DB
|
||||
dbManager = new ComputerDatabaseManager(this);
|
||||
dbRefCount.set(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (discoveryBinder != null) {
|
||||
// Unbind from the discovery service
|
||||
unbindService(discoveryServiceConnection);
|
||||
}
|
||||
|
||||
// FIXME: Should await termination here but we have timeout issues in HttpURLConnection
|
||||
|
||||
// Remove the initial DB reference
|
||||
releaseLocalDatabaseReference();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return binder;
|
||||
}
|
||||
public void removeComputer(String name) {
|
||||
ComputerManagerService.this.removeComputer(name);
|
||||
}
|
||||
|
||||
public void stopPolling() {
|
||||
// Just call the unbind handler to cleanup
|
||||
ComputerManagerService.this.onUnbind(null);
|
||||
}
|
||||
|
||||
public ApplistPoller createAppListPoller(ComputerDetails computer) {
|
||||
return new ApplistPoller(computer);
|
||||
}
|
||||
|
||||
public String getUniqueId() {
|
||||
return idManager.getUniqueId();
|
||||
}
|
||||
|
||||
public ComputerDetails getComputer(UUID uuid) {
|
||||
synchronized (pollingTuples) {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
if (uuid.equals(tuple.computer.uuid)) {
|
||||
return tuple.computer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onUnbind(Intent intent) {
|
||||
// Stop mDNS autodiscovery
|
||||
discoveryBinder.stopDiscovery();
|
||||
|
||||
// Stop polling
|
||||
pollingActive = false;
|
||||
synchronized (pollingTuples) {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
if (tuple.thread != null) {
|
||||
// Interrupt and remove the thread
|
||||
tuple.thread.interrupt();
|
||||
tuple.thread = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the listener
|
||||
listener = null;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private MdnsDiscoveryListener createDiscoveryListener() {
|
||||
return new MdnsDiscoveryListener() {
|
||||
@Override
|
||||
public void notifyComputerAdded(MdnsComputer computer) {
|
||||
// Kick off a serverinfo poll on this machine
|
||||
addComputerBlocking(computer.getAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyComputerRemoved(MdnsComputer computer) {
|
||||
// Nothing to do here
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyDiscoveryFailure(Exception e) {
|
||||
LimeLog.severe("mDNS discovery failed");
|
||||
e.printStackTrace();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void addTuple(ComputerDetails details) {
|
||||
synchronized (pollingTuples) {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
// Check if this is the same computer
|
||||
if (tuple.computer.uuid.equals(details.uuid)) {
|
||||
// Update details anyway in case this machine has been re-added by IP
|
||||
// after not being reachable by our existing information
|
||||
tuple.computer.localIp = details.localIp;
|
||||
tuple.computer.remoteIp = details.remoteIp;
|
||||
|
||||
// Start a polling thread if polling is active
|
||||
if (pollingActive && tuple.thread == null) {
|
||||
tuple.thread = createPollingThread(details);
|
||||
tuple.thread.start();
|
||||
}
|
||||
|
||||
// Found an entry so we're done
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we got here, we didn't find an entry
|
||||
PollingTuple tuple = new PollingTuple(details, pollingActive ? createPollingThread(details) : null);
|
||||
pollingTuples.add(tuple);
|
||||
if (tuple.thread != null) {
|
||||
tuple.thread.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean addComputerBlocking(InetAddress addr) {
|
||||
// Setup a placeholder
|
||||
ComputerDetails fakeDetails = new ComputerDetails();
|
||||
fakeDetails.localIp = addr;
|
||||
fakeDetails.remoteIp = addr;
|
||||
|
||||
// Block while we try to fill the details
|
||||
runPoll(fakeDetails, true);
|
||||
|
||||
// If the machine is reachable, it was successful
|
||||
if (fakeDetails.state == ComputerDetails.State.ONLINE) {
|
||||
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
|
||||
|
||||
// Start a polling thread for this machine
|
||||
addTuple(fakeDetails);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void removeComputer(String name) {
|
||||
if (!getLocalDatabaseReference()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove it from the database
|
||||
dbManager.deleteComputer(name);
|
||||
|
||||
synchronized (pollingTuples) {
|
||||
// Remove the computer from the computer list
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
if (tuple.computer.name.equals(name)) {
|
||||
if (tuple.thread != null) {
|
||||
// Interrupt the thread on this entry
|
||||
tuple.thread.interrupt();
|
||||
}
|
||||
pollingTuples.remove(tuple);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
releaseLocalDatabaseReference();
|
||||
}
|
||||
|
||||
private boolean getLocalDatabaseReference() {
|
||||
if (dbRefCount.get() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
dbRefCount.incrementAndGet();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void releaseLocalDatabaseReference() {
|
||||
if (dbRefCount.decrementAndGet() == 0) {
|
||||
dbManager.close();
|
||||
}
|
||||
}
|
||||
|
||||
private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) {
|
||||
try {
|
||||
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
|
||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
|
||||
ComputerDetails newDetails = http.getComputerDetails();
|
||||
|
||||
// Check if this is the PC we expected
|
||||
if (details.uuid != null && newDetails.uuid != null &&
|
||||
!details.uuid.equals(newDetails.uuid)) {
|
||||
// We got the wrong PC!
|
||||
LimeLog.info("Polling returned the wrong PC!");
|
||||
return null;
|
||||
}
|
||||
|
||||
return newDetails;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean pollComputer(ComputerDetails details, boolean localFirst) {
|
||||
ComputerDetails polledDetails;
|
||||
|
||||
// If the local address is routable across the Internet,
|
||||
// always consider this PC remote to be conservative
|
||||
if (details.localIp.equals(details.remoteIp)) {
|
||||
localFirst = false;
|
||||
}
|
||||
|
||||
if (localFirst) {
|
||||
polledDetails = tryPollIp(details, details.localIp);
|
||||
}
|
||||
else {
|
||||
polledDetails = tryPollIp(details, details.remoteIp);
|
||||
}
|
||||
|
||||
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
|
||||
// Failed, so let's try the fallback
|
||||
if (!localFirst) {
|
||||
polledDetails = tryPollIp(details, details.localIp);
|
||||
}
|
||||
else {
|
||||
polledDetails = tryPollIp(details, details.remoteIp);
|
||||
}
|
||||
|
||||
// The fallback poll worked
|
||||
if (polledDetails != null) {
|
||||
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL :
|
||||
ComputerDetails.Reachability.REMOTE;
|
||||
}
|
||||
}
|
||||
else if (polledDetails != null) {
|
||||
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
|
||||
ComputerDetails.Reachability.REMOTE;
|
||||
}
|
||||
|
||||
// Machine was unreachable both tries
|
||||
if (polledDetails == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we got here, it's reachable
|
||||
details.update(polledDetails);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean doPollMachine(ComputerDetails details) {
|
||||
if (details.reachability == ComputerDetails.Reachability.UNKNOWN ||
|
||||
details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||
// Always try local first to avoid potential UDP issues when
|
||||
// attempting to stream via the router's external IP address
|
||||
// behind its NAT
|
||||
return pollComputer(details, true);
|
||||
}
|
||||
else {
|
||||
// If we're already reached a machine via a particular IP address,
|
||||
// always try that one first
|
||||
return pollComputer(details, details.reachability == ComputerDetails.Reachability.LOCAL);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
// Bind to the discovery service
|
||||
bindService(new Intent(this, DiscoveryService.class),
|
||||
discoveryServiceConnection, Service.BIND_AUTO_CREATE);
|
||||
|
||||
// Lookup or generate this device's UID
|
||||
idManager = new IdentityManager(this);
|
||||
|
||||
// Initialize the DB
|
||||
dbManager = new ComputerDatabaseManager(this);
|
||||
dbRefCount.set(1);
|
||||
|
||||
// Grab known machines into our computer list
|
||||
if (!getLocalDatabaseReference()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (ComputerDetails computer : dbManager.getAllComputers()) {
|
||||
// Add tuples for each computer
|
||||
addTuple(computer);
|
||||
}
|
||||
|
||||
releaseLocalDatabaseReference();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (discoveryBinder != null) {
|
||||
// Unbind from the discovery service
|
||||
unbindService(discoveryServiceConnection);
|
||||
}
|
||||
|
||||
// FIXME: Should await termination here but we have timeout issues in HttpURLConnection
|
||||
|
||||
// Remove the initial DB reference
|
||||
releaseLocalDatabaseReference();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return binder;
|
||||
}
|
||||
|
||||
public class ApplistPoller {
|
||||
private Thread thread;
|
||||
private final ComputerDetails computer;
|
||||
private final Object pollEvent = new Object();
|
||||
|
||||
public ApplistPoller(ComputerDetails computer) {
|
||||
this.computer = computer;
|
||||
}
|
||||
|
||||
public void pollNow() {
|
||||
synchronized (pollEvent) {
|
||||
pollEvent.notify();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean waitPollingDelay() {
|
||||
try {
|
||||
synchronized (pollEvent) {
|
||||
pollEvent.wait(POLLING_PERIOD_MS);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return thread != null && !thread.isInterrupted();
|
||||
}
|
||||
|
||||
public void start() {
|
||||
thread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
do {
|
||||
InetAddress selectedAddr;
|
||||
|
||||
// Can't poll if it's not online
|
||||
if (computer.state != ComputerDetails.State.ONLINE) {
|
||||
if (listener != null) {
|
||||
listener.notifyComputerUpdated(computer);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Can't poll if there's no UUID yet
|
||||
if (computer.uuid == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||
selectedAddr = computer.localIp;
|
||||
}
|
||||
else {
|
||||
selectedAddr = computer.remoteIp;
|
||||
}
|
||||
|
||||
NvHTTP http = new NvHTTP(selectedAddr, idManager.getUniqueId(),
|
||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
|
||||
try {
|
||||
// Query the app list from the server
|
||||
String appList = http.getAppListRaw();
|
||||
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
|
||||
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
|
||||
// Open the cache file
|
||||
FileOutputStream cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
|
||||
CacheHelper.writeStringToOutputStream(cacheOut, appList);
|
||||
cacheOut.close();
|
||||
|
||||
// Update the computer
|
||||
computer.rawAppList = appList;
|
||||
|
||||
// Notify that the app list has been updated
|
||||
// and ensure that the thread is still active
|
||||
if (listener != null && thread != null) {
|
||||
listener.notifyComputerUpdated(computer);
|
||||
}
|
||||
}
|
||||
else {
|
||||
LimeLog.warning("Empty app list received from "+computer.uuid);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} catch (XmlPullParserException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} while (waitPollingDelay());
|
||||
}
|
||||
};
|
||||
thread.start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (thread != null) {
|
||||
thread.interrupt();
|
||||
|
||||
// Don't join here because we might be blocked on network I/O
|
||||
|
||||
thread = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PollingTuple {
|
||||
public Thread thread;
|
||||
public final ComputerDetails computer;
|
||||
|
||||
public PollingTuple(ComputerDetails computer, Thread thread) {
|
||||
this.computer = computer;
|
||||
this.thread = thread;
|
||||
}
|
||||
}
|
||||
@@ -12,75 +12,75 @@ import com.limelight.LimeLog;
|
||||
import android.content.Context;
|
||||
|
||||
public class IdentityManager {
|
||||
private static final String UNIQUE_ID_FILE_NAME = "uniqueid";
|
||||
private static final int UID_SIZE_IN_BYTES = 8;
|
||||
|
||||
private String uniqueId;
|
||||
|
||||
public IdentityManager(Context c) {
|
||||
uniqueId = loadUniqueId(c);
|
||||
if (uniqueId == null) {
|
||||
uniqueId = generateNewUniqueId(c);
|
||||
}
|
||||
|
||||
LimeLog.info("UID is now: "+uniqueId);
|
||||
}
|
||||
|
||||
public String getUniqueId() {
|
||||
return uniqueId;
|
||||
}
|
||||
|
||||
private static String loadUniqueId(Context c) {
|
||||
// 2 Hex digits per byte
|
||||
char[] uid = new char[UID_SIZE_IN_BYTES * 2];
|
||||
InputStreamReader reader = null;
|
||||
LimeLog.info("Reading UID from disk");
|
||||
try {
|
||||
reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME));
|
||||
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2)
|
||||
{
|
||||
LimeLog.severe("UID file data is truncated");
|
||||
return null;
|
||||
}
|
||||
return new String(uid);
|
||||
} catch (FileNotFoundException e) {
|
||||
LimeLog.info("No UID file found");
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
LimeLog.severe("Error while reading UID file");
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
} finally {
|
||||
if (reader != null) {
|
||||
try {
|
||||
reader.close();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String generateNewUniqueId(Context c) {
|
||||
// Generate a new UID hex string
|
||||
LimeLog.info("Generating new UID");
|
||||
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
|
||||
|
||||
OutputStreamWriter writer = null;
|
||||
try {
|
||||
writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0));
|
||||
writer.write(uidStr);
|
||||
LimeLog.info("UID written to disk");
|
||||
} catch (IOException e) {
|
||||
LimeLog.severe("Error while writing UID file");
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (writer != null) {
|
||||
try {
|
||||
writer.close();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
// We can return a UID even if I/O fails
|
||||
return uidStr;
|
||||
}
|
||||
private static final String UNIQUE_ID_FILE_NAME = "uniqueid";
|
||||
private static final int UID_SIZE_IN_BYTES = 8;
|
||||
|
||||
private String uniqueId;
|
||||
|
||||
public IdentityManager(Context c) {
|
||||
uniqueId = loadUniqueId(c);
|
||||
if (uniqueId == null) {
|
||||
uniqueId = generateNewUniqueId(c);
|
||||
}
|
||||
|
||||
LimeLog.info("UID is now: "+uniqueId);
|
||||
}
|
||||
|
||||
public String getUniqueId() {
|
||||
return uniqueId;
|
||||
}
|
||||
|
||||
private static String loadUniqueId(Context c) {
|
||||
// 2 Hex digits per byte
|
||||
char[] uid = new char[UID_SIZE_IN_BYTES * 2];
|
||||
InputStreamReader reader = null;
|
||||
LimeLog.info("Reading UID from disk");
|
||||
try {
|
||||
reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME));
|
||||
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2)
|
||||
{
|
||||
LimeLog.severe("UID file data is truncated");
|
||||
return null;
|
||||
}
|
||||
return new String(uid);
|
||||
} catch (FileNotFoundException e) {
|
||||
LimeLog.info("No UID file found");
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
LimeLog.severe("Error while reading UID file");
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
} finally {
|
||||
if (reader != null) {
|
||||
try {
|
||||
reader.close();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String generateNewUniqueId(Context c) {
|
||||
// Generate a new UID hex string
|
||||
LimeLog.info("Generating new UID");
|
||||
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
|
||||
|
||||
OutputStreamWriter writer = null;
|
||||
try {
|
||||
writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0));
|
||||
writer.write(uidStr);
|
||||
LimeLog.info("UID written to disk");
|
||||
} catch (IOException e) {
|
||||
LimeLog.severe("Error while writing UID file");
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (writer != null) {
|
||||
try {
|
||||
writer.close();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
// We can return a UID even if I/O fails
|
||||
return uidStr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,76 +15,76 @@ import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
|
||||
public class DiscoveryService extends Service {
|
||||
|
||||
private MdnsDiscoveryAgent discoveryAgent;
|
||||
private MdnsDiscoveryListener boundListener;
|
||||
private MulticastLock multicastLock;
|
||||
|
||||
public class DiscoveryBinder extends Binder {
|
||||
public void setListener(MdnsDiscoveryListener listener) {
|
||||
boundListener = listener;
|
||||
}
|
||||
|
||||
public void startDiscovery(int queryIntervalMs) {
|
||||
multicastLock.acquire();
|
||||
discoveryAgent.startDiscovery(queryIntervalMs);
|
||||
}
|
||||
|
||||
public void stopDiscovery() {
|
||||
discoveryAgent.stopDiscovery();
|
||||
multicastLock.release();
|
||||
}
|
||||
|
||||
public List<MdnsComputer> getComputerSet() {
|
||||
return discoveryAgent.getComputerSet();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
|
||||
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
|
||||
multicastLock.setReferenceCounted(false);
|
||||
|
||||
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
|
||||
@Override
|
||||
public void notifyComputerAdded(MdnsComputer computer) {
|
||||
if (boundListener != null) {
|
||||
boundListener.notifyComputerAdded(computer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyComputerRemoved(MdnsComputer computer) {
|
||||
if (boundListener != null) {
|
||||
boundListener.notifyComputerRemoved(computer);
|
||||
}
|
||||
}
|
||||
private MdnsDiscoveryAgent discoveryAgent;
|
||||
private MdnsDiscoveryListener boundListener;
|
||||
private MulticastLock multicastLock;
|
||||
|
||||
@Override
|
||||
public void notifyDiscoveryFailure(Exception e) {
|
||||
if (boundListener != null) {
|
||||
boundListener.notifyDiscoveryFailure(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private DiscoveryBinder binder = new DiscoveryBinder();
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return binder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onUnbind(Intent intent) {
|
||||
// Stop any discovery session
|
||||
discoveryAgent.stopDiscovery();
|
||||
multicastLock.release();
|
||||
|
||||
// Unbind the listener
|
||||
boundListener = null;
|
||||
return false;
|
||||
}
|
||||
public class DiscoveryBinder extends Binder {
|
||||
public void setListener(MdnsDiscoveryListener listener) {
|
||||
boundListener = listener;
|
||||
}
|
||||
|
||||
public void startDiscovery(int queryIntervalMs) {
|
||||
multicastLock.acquire();
|
||||
discoveryAgent.startDiscovery(queryIntervalMs);
|
||||
}
|
||||
|
||||
public void stopDiscovery() {
|
||||
discoveryAgent.stopDiscovery();
|
||||
multicastLock.release();
|
||||
}
|
||||
|
||||
public List<MdnsComputer> getComputerSet() {
|
||||
return discoveryAgent.getComputerSet();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
|
||||
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
|
||||
multicastLock.setReferenceCounted(false);
|
||||
|
||||
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
|
||||
@Override
|
||||
public void notifyComputerAdded(MdnsComputer computer) {
|
||||
if (boundListener != null) {
|
||||
boundListener.notifyComputerAdded(computer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyComputerRemoved(MdnsComputer computer) {
|
||||
if (boundListener != null) {
|
||||
boundListener.notifyComputerRemoved(computer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyDiscoveryFailure(Exception e) {
|
||||
if (boundListener != null) {
|
||||
boundListener.notifyDiscoveryFailure(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private final DiscoveryBinder binder = new DiscoveryBinder();
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return binder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onUnbind(Intent intent) {
|
||||
// Stop any discovery session
|
||||
discoveryAgent.stopDiscovery();
|
||||
multicastLock.release();
|
||||
|
||||
// Unbind the listener
|
||||
boundListener = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
package com.limelight.grid;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.AsyncTask;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.koushikdutta.async.future.FutureCallback;
|
||||
import com.koushikdutta.ion.Ion;
|
||||
import com.limelight.AppView;
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.R;
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||
import com.limelight.utils.CacheHelper;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Principal;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509KeyManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
|
||||
private final ComputerDetails computer;
|
||||
private final String uniqueId;
|
||||
private final LimelightCryptoProvider cryptoProvider;
|
||||
private final SSLContext sslContext;
|
||||
private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>();
|
||||
|
||||
public AppGridAdapter(Context context, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException {
|
||||
super(context, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
|
||||
|
||||
this.computer = computer;
|
||||
this.uniqueId = uniqueId;
|
||||
|
||||
cryptoProvider = PlatformBinding.getCryptoProvider(context);
|
||||
|
||||
sslContext = SSLContext.getInstance("SSL");
|
||||
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom());
|
||||
}
|
||||
|
||||
private final TrustManager[] trustAllCerts = new TrustManager[] {
|
||||
new X509TrustManager() {
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
|
||||
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
|
||||
}};
|
||||
|
||||
private final KeyManager[] ourKeyman = new KeyManager[] {
|
||||
new X509KeyManager() {
|
||||
public String chooseClientAlias(String[] keyTypes,
|
||||
Principal[] issuers, Socket socket) {
|
||||
return "Limelight-RSA";
|
||||
}
|
||||
|
||||
public String chooseServerAlias(String keyType, Principal[] issuers,
|
||||
Socket socket) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public X509Certificate[] getCertificateChain(String alias) {
|
||||
return new X509Certificate[] {cryptoProvider.getClientCertificate()};
|
||||
}
|
||||
|
||||
public String[] getClientAliases(String keyType, Principal[] issuers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public PrivateKey getPrivateKey(String alias) {
|
||||
return cryptoProvider.getClientPrivateKey();
|
||||
}
|
||||
|
||||
public String[] getServerAliases(String keyType, Principal[] issuers) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Ignore differences between given hostname and certificate hostname
|
||||
HostnameVerifier hv = new HostnameVerifier() {
|
||||
public boolean verify(String hostname, SSLSession session) { return true; }
|
||||
};
|
||||
|
||||
private void sortList() {
|
||||
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
||||
@Override
|
||||
public int compare(AppView.AppObject lhs, AppView.AppObject rhs) {
|
||||
return lhs.app.getAppName().compareTo(rhs.app.getAppName());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private InetAddress getCurrentAddress() {
|
||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||
return computer.localIp;
|
||||
}
|
||||
else {
|
||||
return computer.remoteIp;
|
||||
}
|
||||
}
|
||||
|
||||
public void addApp(AppView.AppObject app) {
|
||||
itemList.add(app);
|
||||
sortList();
|
||||
}
|
||||
|
||||
public void abortPendingRequests() {
|
||||
HashMap<ImageView, Future> tempMap;
|
||||
|
||||
synchronized (pendingRequests) {
|
||||
// Copy the pending requests under a lock
|
||||
tempMap = new HashMap<ImageView, Future>(pendingRequests);
|
||||
}
|
||||
|
||||
for (Future f : tempMap.values()) {
|
||||
if (!f.isCancelled() && !f.isDone()) {
|
||||
f.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
synchronized (pendingRequests) {
|
||||
// Remove cancelled requests
|
||||
for (ImageView v : tempMap.keySet()) {
|
||||
pendingRequests.remove(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Handle pruning of bitmap cache
|
||||
private void populateBitmapCache(UUID uuid, int appId, Bitmap bitmap) {
|
||||
try {
|
||||
// PNG ignores quality setting
|
||||
FileOutputStream out = CacheHelper.openCacheFileForOutput(context.getCacheDir(), "boxart", uuid.toString(), appId+".png");
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
|
||||
out.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) {
|
||||
// Clear existing contents of the image view
|
||||
imgView.setAlpha(0.0f);
|
||||
|
||||
// Check the on-disk cache
|
||||
new ImageCacheRequest(imgView, obj.app.getAppId()).execute();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean populateTextView(TextView txtView, AppView.AppObject obj) {
|
||||
// Select the text view so it starts marquee mode
|
||||
txtView.setSelected(true);
|
||||
|
||||
// Return false to use the app's toString method
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) {
|
||||
if (obj.app.getIsRunning()) {
|
||||
// Show the play button overlay
|
||||
overlayView.setImageResource(R.drawable.play);
|
||||
return true;
|
||||
}
|
||||
|
||||
// No overlay
|
||||
return false;
|
||||
}
|
||||
|
||||
private class ImageCacheRequest extends AsyncTask<Void, Void, Bitmap> {
|
||||
private final ImageView view;
|
||||
private final int appId;
|
||||
|
||||
public ImageCacheRequest(ImageView view, int appId) {
|
||||
this.view = view;
|
||||
this.appId = appId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitmap doInBackground(Void... v) {
|
||||
InputStream in = null;
|
||||
try {
|
||||
in = CacheHelper.openCacheFileForInput(context.getCacheDir(), "boxart", computer.uuid.toString(), appId + ".png");
|
||||
return BitmapFactory.decodeStream(in);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (in != null) {
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void fadeInImage(ImageView view) {
|
||||
view.animate().alpha(1.0f).setDuration(250).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Bitmap result) {
|
||||
if (result != null) {
|
||||
// Disk cache was read successfully
|
||||
LimeLog.info("Image disk cache hit for (" + computer.uuid + ", " + appId + ")");
|
||||
view.setImageBitmap(result);
|
||||
fadeInImage(view);
|
||||
}
|
||||
else {
|
||||
LimeLog.info("Image disk cache miss for ("+computer.uuid+", "+appId+")");
|
||||
LimeLog.info("Requesting: "+"https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
|
||||
appId + "&AssetType=2&AssetIdx=0");
|
||||
|
||||
// Load the placeholder image
|
||||
view.setImageResource(defaultImageRes);
|
||||
fadeInImage(view);
|
||||
|
||||
// Set SSL contexts correctly to allow us to authenticate
|
||||
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts);
|
||||
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext);
|
||||
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setHostnameVerifier(hv);
|
||||
|
||||
// Kick off the deferred image load
|
||||
synchronized (pendingRequests) {
|
||||
Future<Bitmap> f = Ion.with(context)
|
||||
.load("https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
|
||||
appId + "&AssetType=2&AssetIdx=0")
|
||||
.asBitmap()
|
||||
.setCallback(new FutureCallback<Bitmap>() {
|
||||
@Override
|
||||
public void onCompleted(Exception e, final Bitmap result) {
|
||||
synchronized (pendingRequests) {
|
||||
pendingRequests.remove(view);
|
||||
}
|
||||
|
||||
if (result != null) {
|
||||
// Make the view visible now
|
||||
view.setImageBitmap(result);
|
||||
fadeInImage(view);
|
||||
|
||||
// Populate the disk cache if we got an image back.
|
||||
// We do it in a new thread because it can be very expensive, especially
|
||||
// when we do the initial load where lots of disk I/O is happening at once.
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
populateBitmapCache(computer.uuid, appId, result);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
else {
|
||||
// Leave the loading icon as is (probably should change this eventually...)
|
||||
}
|
||||
}
|
||||
});
|
||||
pendingRequests.put(view, f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.limelight.grid;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.limelight.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
||||
protected final Context context;
|
||||
protected final int defaultImageRes;
|
||||
protected final int layoutId;
|
||||
protected final ArrayList<T> itemList = new ArrayList<T>();
|
||||
protected final LayoutInflater inflater;
|
||||
|
||||
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
|
||||
this.context = context;
|
||||
this.layoutId = layoutId;
|
||||
this.defaultImageRes = defaultImageRes;
|
||||
|
||||
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
itemList.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return itemList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int i) {
|
||||
return itemList.get(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int i) {
|
||||
return i;
|
||||
}
|
||||
|
||||
public abstract boolean populateImageView(ImageView imgView, T obj);
|
||||
public abstract boolean populateTextView(TextView txtView, T obj);
|
||||
public abstract boolean populateOverlayView(ImageView overlayView, T obj);
|
||||
|
||||
@Override
|
||||
public View getView(int i, View convertView, ViewGroup viewGroup) {
|
||||
if (convertView == null) {
|
||||
convertView = inflater.inflate(layoutId, viewGroup, false);
|
||||
}
|
||||
|
||||
ImageView imgView = (ImageView) convertView.findViewById(R.id.grid_image);
|
||||
ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay);
|
||||
TextView txtView = (TextView) convertView.findViewById(R.id.grid_text);
|
||||
|
||||
if (imgView != null) {
|
||||
if (!populateImageView(imgView, itemList.get(i))) {
|
||||
imgView.setImageResource(defaultImageRes);
|
||||
}
|
||||
}
|
||||
if (!populateTextView(txtView, itemList.get(i))) {
|
||||
txtView.setText(itemList.get(i).toString());
|
||||
}
|
||||
if (overlayView != null) {
|
||||
if (!populateOverlayView(overlayView, itemList.get(i))) {
|
||||
overlayView.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
else {
|
||||
overlayView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
return convertView;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.limelight.grid;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.limelight.PcView;
|
||||
import com.limelight.R;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
|
||||
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||
|
||||
public PcGridAdapter(Context context, boolean listMode, boolean small) {
|
||||
super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item), R.drawable.computer);
|
||||
}
|
||||
|
||||
public void addComputer(PcView.ComputerObject computer) {
|
||||
itemList.add(computer);
|
||||
sortList();
|
||||
}
|
||||
|
||||
private void sortList() {
|
||||
Collections.sort(itemList, new Comparator<PcView.ComputerObject>() {
|
||||
@Override
|
||||
public int compare(PcView.ComputerObject lhs, PcView.ComputerObject rhs) {
|
||||
return lhs.details.name.compareTo(rhs.details.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean removeComputer(PcView.ComputerObject computer) {
|
||||
return itemList.remove(computer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean populateImageView(ImageView imgView, PcView.ComputerObject obj) {
|
||||
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
|
||||
imgView.setAlpha(1.0f);
|
||||
}
|
||||
else {
|
||||
imgView.setAlpha(0.4f);
|
||||
}
|
||||
|
||||
// Return false to use the default drawable
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
|
||||
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
|
||||
txtView.setAlpha(1.0f);
|
||||
}
|
||||
else {
|
||||
txtView.setAlpha(0.4f);
|
||||
}
|
||||
|
||||
// Return false to use the computer's toString method
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
|
||||
if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||
// Still refreshing this PC so display the overlay
|
||||
overlayView.setImageResource(R.drawable.image_loading);
|
||||
return true;
|
||||
}
|
||||
|
||||
// No overlay
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,44 @@
|
||||
package com.limelight.nvstream.av.video.cpu;
|
||||
|
||||
public class AvcDecoder {
|
||||
static {
|
||||
// FFMPEG dependencies
|
||||
System.loadLibrary("avutil-52");
|
||||
System.loadLibrary("swresample-0");
|
||||
System.loadLibrary("swscale-2");
|
||||
System.loadLibrary("avcodec-55");
|
||||
System.loadLibrary("avformat-55");
|
||||
|
||||
System.loadLibrary("nv_avc_dec");
|
||||
}
|
||||
|
||||
/** Disables the deblocking filter at the cost of image quality */
|
||||
public static final int DISABLE_LOOP_FILTER = 0x1;
|
||||
/** Uses the low latency decode flag (disables multithreading) */
|
||||
public static final int LOW_LATENCY_DECODE = 0x2;
|
||||
/** Threads process each slice, rather than each frame */
|
||||
public static final int SLICE_THREADING = 0x4;
|
||||
/** Uses nonstandard speedup tricks */
|
||||
public static final int FAST_DECODE = 0x8;
|
||||
/** Uses bilinear filtering instead of bicubic */
|
||||
public static final int BILINEAR_FILTERING = 0x10;
|
||||
/** Uses a faster bilinear filtering with lower image quality */
|
||||
public static final int FAST_BILINEAR_FILTERING = 0x20;
|
||||
/** Disables color conversion (output is NV21) */
|
||||
public static final int NO_COLOR_CONVERSION = 0x40;
|
||||
|
||||
public static native int init(int width, int height, int perflvl, int threadcount);
|
||||
public static native void destroy();
|
||||
|
||||
// Rendering API when NO_COLOR_CONVERSION == 0
|
||||
public static native boolean setRenderTarget(Object androidSurface);
|
||||
public static native boolean getRgbFrameInt(int[] rgbFrame, int bufferSize);
|
||||
public static native boolean getRgbFrame(byte[] rgbFrame, int bufferSize);
|
||||
public static native boolean redraw();
|
||||
static {
|
||||
// FFMPEG dependencies
|
||||
System.loadLibrary("avutil-52");
|
||||
System.loadLibrary("swresample-0");
|
||||
System.loadLibrary("swscale-2");
|
||||
System.loadLibrary("avcodec-55");
|
||||
System.loadLibrary("avformat-55");
|
||||
|
||||
// Rendering API when NO_COLOR_CONVERSION == 1
|
||||
public static native boolean getRawFrame(byte[] yuvFrame, int bufferSize);
|
||||
|
||||
public static native int getInputPaddingSize();
|
||||
public static native int decode(byte[] indata, int inoff, int inlen);
|
||||
System.loadLibrary("nv_avc_dec");
|
||||
}
|
||||
|
||||
/** Disables the deblocking filter at the cost of image quality */
|
||||
public static final int DISABLE_LOOP_FILTER = 0x1;
|
||||
/** Uses the low latency decode flag (disables multithreading) */
|
||||
public static final int LOW_LATENCY_DECODE = 0x2;
|
||||
/** Threads process each slice, rather than each frame */
|
||||
public static final int SLICE_THREADING = 0x4;
|
||||
/** Uses nonstandard speedup tricks */
|
||||
public static final int FAST_DECODE = 0x8;
|
||||
/** Uses bilinear filtering instead of bicubic */
|
||||
public static final int BILINEAR_FILTERING = 0x10;
|
||||
/** Uses a faster bilinear filtering with lower image quality */
|
||||
public static final int FAST_BILINEAR_FILTERING = 0x20;
|
||||
/** Disables color conversion (output is NV21) */
|
||||
public static final int NO_COLOR_CONVERSION = 0x40;
|
||||
|
||||
public static native int init(int width, int height, int perflvl, int threadcount);
|
||||
public static native void destroy();
|
||||
|
||||
// Rendering API when NO_COLOR_CONVERSION == 0
|
||||
public static native boolean setRenderTarget(Object androidSurface);
|
||||
public static native boolean getRgbFrameInt(int[] rgbFrame, int bufferSize);
|
||||
public static native boolean getRgbFrame(byte[] rgbFrame, int bufferSize);
|
||||
public static native boolean redraw();
|
||||
|
||||
// Rendering API when NO_COLOR_CONVERSION == 1
|
||||
public static native boolean getRawFrame(byte[] yuvFrame, int bufferSize);
|
||||
|
||||
public static native int getInputPaddingSize();
|
||||
public static native int decode(byte[] indata, int inoff, int inlen);
|
||||
}
|
||||
|
||||
@@ -2,149 +2,180 @@ package com.limelight.preferences;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import com.limelight.computers.ComputerManagerService;
|
||||
import com.limelight.R;
|
||||
import com.limelight.utils.Dialog;
|
||||
import com.limelight.utils.SpinnerDialog;
|
||||
import com.limelight.utils.UiHelper;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.Button;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
public class AddComputerManually extends Activity {
|
||||
private Button addPcButton;
|
||||
private TextView hostText;
|
||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||
private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
|
||||
private Thread addThread;
|
||||
private ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName className, final IBinder binder) {
|
||||
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
|
||||
startAddThread();
|
||||
}
|
||||
private TextView hostText;
|
||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
|
||||
private Thread addThread;
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName className, final IBinder binder) {
|
||||
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
|
||||
startAddThread();
|
||||
}
|
||||
|
||||
public void onServiceDisconnected(ComponentName className) {
|
||||
joinAddThread();
|
||||
managerBinder = null;
|
||||
}
|
||||
};
|
||||
|
||||
private void doAddPc(String host) {
|
||||
String msg;
|
||||
boolean finish = false;
|
||||
try {
|
||||
InetAddress addr = InetAddress.getByName(host);
|
||||
|
||||
if (!managerBinder.addComputerBlocking(addr)){
|
||||
msg = "Unable to connect to the specified computer. Make sure the required ports are allowed through the firewall.";
|
||||
}
|
||||
else {
|
||||
msg = "Successfully added computer";
|
||||
finish = true;
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
msg = "Unable to resolve PC address. Make sure you didn't make a typo in the address.";
|
||||
}
|
||||
|
||||
final boolean toastFinish = finish;
|
||||
final String toastMsg = msg;
|
||||
AddComputerManually.this.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
|
||||
|
||||
if (toastFinish && !isFinishing()) {
|
||||
// Close the activity
|
||||
AddComputerManually.this.finish();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void startAddThread() {
|
||||
addThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
while (!isInterrupted()) {
|
||||
String computer;
|
||||
|
||||
try {
|
||||
computer = computersToAdd.take();
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
doAddPc(computer);
|
||||
}
|
||||
}
|
||||
};
|
||||
addThread.setName("UI - AddComputerManually");
|
||||
addThread.start();
|
||||
}
|
||||
|
||||
private void joinAddThread() {
|
||||
if (addThread != null) {
|
||||
addThread.interrupt();
|
||||
|
||||
try {
|
||||
addThread.join();
|
||||
} catch (InterruptedException ignored) {}
|
||||
|
||||
addThread = null;
|
||||
}
|
||||
}
|
||||
public void onServiceDisconnected(ComponentName className) {
|
||||
joinAddThread();
|
||||
managerBinder = null;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
|
||||
Dialog.closeDialogs();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
if (managerBinder != null) {
|
||||
joinAddThread();
|
||||
unbindService(serviceConnection);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_add_computer_manually);
|
||||
|
||||
this.addPcButton = (Button) findViewById(R.id.addPc);
|
||||
this.hostText = (TextView) findViewById(R.id.hostTextView);
|
||||
|
||||
// Bind to the ComputerManager service
|
||||
bindService(new Intent(AddComputerManually.this,
|
||||
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
|
||||
|
||||
addPcButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (hostText.getText().length() == 0) {
|
||||
Toast.makeText(AddComputerManually.this, "You must enter an IP address", Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.makeText(AddComputerManually.this, "Adding PC...", Toast.LENGTH_SHORT).show();
|
||||
computersToAdd.add(hostText.getText().toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
private void doAddPc(String host) {
|
||||
String msg;
|
||||
boolean finish = false;
|
||||
|
||||
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
|
||||
getResources().getString(R.string.msg_add_pc), false);
|
||||
|
||||
try {
|
||||
InetAddress addr = InetAddress.getByName(host);
|
||||
|
||||
if (!managerBinder.addComputerBlocking(addr)){
|
||||
msg = getResources().getString(R.string.addpc_fail);
|
||||
}
|
||||
else {
|
||||
msg = getResources().getString(R.string.addpc_success);
|
||||
finish = true;
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
msg = getResources().getString(R.string.addpc_unknown_host);
|
||||
}
|
||||
|
||||
dialog.dismiss();
|
||||
|
||||
final boolean toastFinish = finish;
|
||||
final String toastMsg = msg;
|
||||
AddComputerManually.this.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
|
||||
|
||||
if (toastFinish && !isFinishing()) {
|
||||
// Close the activity
|
||||
AddComputerManually.this.finish();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void startAddThread() {
|
||||
addThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
while (!isInterrupted()) {
|
||||
String computer;
|
||||
|
||||
try {
|
||||
computer = computersToAdd.take();
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
doAddPc(computer);
|
||||
}
|
||||
}
|
||||
};
|
||||
addThread.setName("UI - AddComputerManually");
|
||||
addThread.start();
|
||||
}
|
||||
|
||||
private void joinAddThread() {
|
||||
if (addThread != null) {
|
||||
addThread.interrupt();
|
||||
|
||||
try {
|
||||
addThread.join();
|
||||
} catch (InterruptedException ignored) {}
|
||||
|
||||
addThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
|
||||
Dialog.closeDialogs();
|
||||
SpinnerDialog.closeDialogs(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
if (managerBinder != null) {
|
||||
joinAddThread();
|
||||
unbindService(serviceConnection);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
String locale = PreferenceConfiguration.readPreferences(this).language;
|
||||
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||
Configuration config = new Configuration(getResources().getConfiguration());
|
||||
config.locale = new Locale(locale);
|
||||
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_add_computer_manually);
|
||||
|
||||
UiHelper.notifyNewRootView(this);
|
||||
|
||||
this.hostText = (TextView) findViewById(R.id.hostTextView);
|
||||
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
@Override
|
||||
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE ||
|
||||
(keyEvent != null &&
|
||||
keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
|
||||
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
|
||||
if (hostText.getText().length() == 0) {
|
||||
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
|
||||
return true;
|
||||
}
|
||||
|
||||
computersToAdd.add(hostText.getText().toString().trim());
|
||||
}
|
||||
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
|
||||
// This is how the Fire TV dismisses the keyboard
|
||||
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(hostText.getWindowToken(), 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Bind to the ComputerManager service
|
||||
bindService(new Intent(AddComputerManually.this,
|
||||
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.limelight.preferences;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
public class PreferenceConfiguration {
|
||||
@@ -12,11 +13,16 @@ public class PreferenceConfiguration {
|
||||
private static final String SOPS_PREF_STRING = "checkbox_enable_sops";
|
||||
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
|
||||
private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio";
|
||||
private static final String DEADZONE_PREF_STRING = "seekbar_deadzone";
|
||||
private static final String LANGUAGE_PREF_STRING = "list_languages";
|
||||
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
|
||||
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
|
||||
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
|
||||
|
||||
private static final int BITRATE_DEFAULT_720_30 = 5;
|
||||
private static final int BITRATE_DEFAULT_720_60 = 10;
|
||||
private static final int BITRATE_DEFAULT_1080_30 = 10;
|
||||
private static final int BITRATE_DEFAULT_1080_60 = 30;
|
||||
private static final int BITRATE_DEFAULT_1080_60 = 20;
|
||||
|
||||
private static final String DEFAULT_RES_FPS = "720p60";
|
||||
private static final String DEFAULT_DECODER = "auto";
|
||||
@@ -25,6 +31,10 @@ public class PreferenceConfiguration {
|
||||
private static final boolean DEFAULT_SOPS = true;
|
||||
private static final boolean DEFAULT_DISABLE_TOASTS = false;
|
||||
private static final boolean DEFAULT_HOST_AUDIO = false;
|
||||
private static final int DEFAULT_DEADZONE = 15;
|
||||
public static final String DEFAULT_LANGUAGE = "default";
|
||||
private static final boolean DEFAULT_LIST_MODE = false;
|
||||
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
|
||||
|
||||
public static final int FORCE_HARDWARE_DECODER = -1;
|
||||
public static final int AUTOSELECT_DECODER = 0;
|
||||
@@ -33,7 +43,10 @@ public class PreferenceConfiguration {
|
||||
public int width, height, fps;
|
||||
public int bitrate;
|
||||
public int decoder;
|
||||
public int deadzonePercentage;
|
||||
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
|
||||
public String language;
|
||||
public boolean listMode, smallIconMode, multiController;
|
||||
|
||||
public static int getDefaultBitrate(String resFpsString) {
|
||||
if (resFpsString.equals("720p30")) {
|
||||
@@ -54,6 +67,17 @@ public class PreferenceConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean getDefaultSmallMode(Context context) {
|
||||
PackageManager manager = context.getPackageManager();
|
||||
if (manager != null && manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
|
||||
// TVs shouldn't use small mode by default
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use small mode on anything smaller than a 7" tablet
|
||||
return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
|
||||
}
|
||||
|
||||
public static int getDefaultBitrate(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
@@ -130,11 +154,18 @@ public class PreferenceConfiguration {
|
||||
|
||||
config.decoder = getDecoderValue(context);
|
||||
|
||||
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
|
||||
|
||||
config.language = prefs.getString(LANGUAGE_PREF_STRING, DEFAULT_LANGUAGE);
|
||||
|
||||
// Checkbox preferences
|
||||
config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS);
|
||||
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
|
||||
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
|
||||
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
|
||||
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
|
||||
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
|
||||
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -19,11 +19,15 @@ public class SeekBarPreference extends DialogPreference
|
||||
private static final String SCHEMA_URL = "http://schemas.android.com/apk/res/android";
|
||||
|
||||
private SeekBar seekBar;
|
||||
private TextView splashText, valueText;
|
||||
private Context context;
|
||||
private TextView valueText;
|
||||
private final Context context;
|
||||
|
||||
private String dialogMessage, suffix;
|
||||
private int defaultValue, maxValue, currentValue;
|
||||
private final String dialogMessage;
|
||||
private final String suffix;
|
||||
private final int defaultValue;
|
||||
private final int maxValue;
|
||||
private final int minValue;
|
||||
private int currentValue;
|
||||
|
||||
public SeekBarPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
@@ -47,9 +51,10 @@ public class SeekBarPreference extends DialogPreference
|
||||
suffix = context.getString(suffixId);
|
||||
}
|
||||
|
||||
// Get default and max seekbar values
|
||||
defaultValue = PreferenceConfiguration.getDefaultBitrate(context);
|
||||
// Get default, min, and max seekbar values
|
||||
defaultValue = attrs.getAttributeIntValue(SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
|
||||
maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100);
|
||||
minValue = 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -60,7 +65,7 @@ public class SeekBarPreference extends DialogPreference
|
||||
layout.setOrientation(LinearLayout.VERTICAL);
|
||||
layout.setPadding(6, 6, 6, 6);
|
||||
|
||||
splashText = new TextView(context);
|
||||
TextView splashText = new TextView(context);
|
||||
splashText.setPadding(30, 10, 30, 10);
|
||||
if (dialogMessage != null) {
|
||||
splashText.setText(dialogMessage);
|
||||
@@ -71,7 +76,7 @@ public class SeekBarPreference extends DialogPreference
|
||||
valueText.setGravity(Gravity.CENTER_HORIZONTAL);
|
||||
valueText.setTextSize(32);
|
||||
params = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.FILL_PARENT,
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
layout.addView(valueText, params);
|
||||
|
||||
@@ -79,8 +84,13 @@ public class SeekBarPreference extends DialogPreference
|
||||
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int value, boolean b) {
|
||||
if (value < minValue) {
|
||||
seekBar.setProgress(minValue);
|
||||
return;
|
||||
}
|
||||
|
||||
String t = String.valueOf(value);
|
||||
valueText.setText(suffix == null ? t : t.concat(" " + suffix));
|
||||
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -90,7 +100,7 @@ public class SeekBarPreference extends DialogPreference
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||
});
|
||||
|
||||
layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||
layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
if (shouldPersist()) {
|
||||
currentValue = getPersistedInt(defaultValue);
|
||||
@@ -121,13 +131,6 @@ public class SeekBarPreference extends DialogPreference
|
||||
}
|
||||
}
|
||||
|
||||
public void setMax(int max) {
|
||||
this.maxValue = max;
|
||||
}
|
||||
public int getMax() {
|
||||
return this.maxValue;
|
||||
}
|
||||
|
||||
public void setProgress(int progress) {
|
||||
this.currentValue = progress;
|
||||
if (seekBar != null) {
|
||||
@@ -149,10 +152,10 @@ public class SeekBarPreference extends DialogPreference
|
||||
if (shouldPersist()) {
|
||||
currentValue = seekBar.getProgress();
|
||||
persistInt(seekBar.getProgress());
|
||||
callChangeListener(Integer.valueOf(seekBar.getProgress()));
|
||||
callChangeListener(seekBar.getProgress());
|
||||
}
|
||||
|
||||
((AlertDialog) getDialog()).dismiss();
|
||||
getDialog().dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.limelight.preferences;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.preference.CheckBoxPreference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class SmallIconCheckboxPreference extends CheckBoxPreference {
|
||||
public SmallIconCheckboxPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public SmallIconCheckboxPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object onGetDefaultValue(TypedArray a, int index) {
|
||||
return PreferenceConfiguration.getDefaultSmallMode(getContext());
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,48 @@
|
||||
package com.limelight.preferences;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.app.Activity;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import com.limelight.PcView;
|
||||
import com.limelight.R;
|
||||
import com.limelight.utils.UiHelper;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class StreamSettings extends Activity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
String locale = PreferenceConfiguration.readPreferences(this).language;
|
||||
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||
Configuration config = new Configuration(getResources().getConfiguration());
|
||||
config.locale = new Locale(locale);
|
||||
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_stream_settings);
|
||||
getFragmentManager().beginTransaction().replace(
|
||||
R.id.stream_settings, new SettingsFragment()
|
||||
).commit();
|
||||
|
||||
UiHelper.notifyNewRootView(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
finish();
|
||||
|
||||
// Restart the PC view to apply UI changes
|
||||
Intent intent = new Intent(this, PcView.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent, null);
|
||||
}
|
||||
|
||||
public static class SettingsFragment extends PreferenceFragment {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.limelight.ui;
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AbsListView;
|
||||
|
||||
import com.limelight.R;
|
||||
|
||||
public class AdapterFragment extends Fragment {
|
||||
private AdapterFragmentCallbacks callbacks;
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
|
||||
callbacks = (AdapterFragmentCallbacks) activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
return inflater.inflate(callbacks.getAdapterFragmentLayoutId(), container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
callbacks.receiveAbsListView((AbsListView) getView().findViewById(R.id.fragmentView));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.limelight.ui;
|
||||
|
||||
import android.widget.AbsListView;
|
||||
|
||||
public interface AdapterFragmentCallbacks {
|
||||
public int getAdapterFragmentLayoutId();
|
||||
public void receiveAbsListView(AbsListView gridView);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.limelight.ui;
|
||||
|
||||
public interface GameGestures {
|
||||
public void showKeyboard();
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.limelight.utils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Reader;
|
||||
|
||||
public class CacheHelper {
|
||||
private static File openPath(boolean createPath, File root, String... path) {
|
||||
File f = root;
|
||||
for (int i = 0; i < path.length; i++) {
|
||||
String component = path[i];
|
||||
|
||||
if (i == path.length - 1) {
|
||||
// This is the file component so now we create parent directories
|
||||
if (createPath) {
|
||||
f.mkdirs();
|
||||
}
|
||||
}
|
||||
|
||||
f = new File(f, component);
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
public static FileInputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
|
||||
return new FileInputStream(openPath(false, root, path));
|
||||
}
|
||||
|
||||
public static FileOutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
|
||||
return new FileOutputStream(openPath(true, root, path));
|
||||
}
|
||||
|
||||
public static String readInputStreamToString(InputStream in) throws IOException {
|
||||
Reader r = new InputStreamReader(in);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
char[] buf = new char[256];
|
||||
int bytesRead;
|
||||
while ((bytesRead = r.read(buf)) != -1) {
|
||||
sb.append(buf, 0, bytesRead);
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static void writeStringToOutputStream(OutputStream out, String str) throws IOException {
|
||||
out.write(str.getBytes("UTF-8"));
|
||||
}
|
||||
}
|
||||
@@ -7,70 +7,71 @@ import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
|
||||
public class Dialog implements Runnable {
|
||||
private String title, message;
|
||||
private Activity activity;
|
||||
private boolean endAfterDismiss;
|
||||
|
||||
private AlertDialog alert;
|
||||
|
||||
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
|
||||
|
||||
public Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||
{
|
||||
this.activity = activity;
|
||||
this.title = title;
|
||||
this.message = message;
|
||||
this.endAfterDismiss = endAfterDismiss;
|
||||
}
|
||||
|
||||
public static void closeDialogs()
|
||||
{
|
||||
synchronized (rundownDialogs) {
|
||||
for (Dialog d : rundownDialogs) {
|
||||
if (d.alert.isShowing()) {
|
||||
d.alert.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
rundownDialogs.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||
{
|
||||
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// If we're dying, don't bother creating a dialog
|
||||
if (activity.isFinishing())
|
||||
return;
|
||||
|
||||
alert = new AlertDialog.Builder(activity).create();
|
||||
private final String title;
|
||||
private final String message;
|
||||
private final Activity activity;
|
||||
private final boolean endAfterDismiss;
|
||||
|
||||
alert.setTitle(title);
|
||||
alert.setMessage(message);
|
||||
alert.setCancelable(false);
|
||||
alert.setCanceledOnTouchOutside(false);
|
||||
private AlertDialog alert;
|
||||
|
||||
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
|
||||
|
||||
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||
{
|
||||
this.activity = activity;
|
||||
this.title = title;
|
||||
this.message = message;
|
||||
this.endAfterDismiss = endAfterDismiss;
|
||||
}
|
||||
|
||||
public static void closeDialogs()
|
||||
{
|
||||
synchronized (rundownDialogs) {
|
||||
for (Dialog d : rundownDialogs) {
|
||||
if (d.alert.isShowing()) {
|
||||
d.alert.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
rundownDialogs.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||
{
|
||||
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// If we're dying, don't bother creating a dialog
|
||||
if (activity.isFinishing())
|
||||
return;
|
||||
|
||||
alert = new AlertDialog.Builder(activity).create();
|
||||
|
||||
alert.setTitle(title);
|
||||
alert.setMessage(message);
|
||||
alert.setCancelable(false);
|
||||
alert.setCanceledOnTouchOutside(false);
|
||||
|
||||
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
synchronized (rundownDialogs) {
|
||||
rundownDialogs.remove(Dialog.this);
|
||||
alert.dismiss();
|
||||
}
|
||||
|
||||
if (endAfterDismiss) {
|
||||
activity.finish();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
synchronized (rundownDialogs) {
|
||||
rundownDialogs.add(this);
|
||||
alert.show();
|
||||
}
|
||||
}
|
||||
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
synchronized (rundownDialogs) {
|
||||
rundownDialogs.remove(Dialog.this);
|
||||
alert.dismiss();
|
||||
}
|
||||
|
||||
if (endAfterDismiss) {
|
||||
activity.finish();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
synchronized (rundownDialogs) {
|
||||
rundownDialogs.add(this);
|
||||
alert.show();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,111 +9,112 @@ import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnCancelListener;
|
||||
|
||||
public class SpinnerDialog implements Runnable,OnCancelListener {
|
||||
private String title, message;
|
||||
private Activity activity;
|
||||
private ProgressDialog progress;
|
||||
private boolean finish;
|
||||
|
||||
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
|
||||
|
||||
public SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
||||
{
|
||||
this.activity = activity;
|
||||
this.title = title;
|
||||
this.message = message;
|
||||
this.progress = null;
|
||||
this.finish = finish;
|
||||
}
|
||||
|
||||
public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish)
|
||||
{
|
||||
SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish);
|
||||
activity.runOnUiThread(spinner);
|
||||
return spinner;
|
||||
}
|
||||
|
||||
public static void closeDialogs(Activity activity)
|
||||
{
|
||||
synchronized (rundownDialogs) {
|
||||
Iterator<SpinnerDialog> i = rundownDialogs.iterator();
|
||||
while (i.hasNext()) {
|
||||
SpinnerDialog dialog = i.next();
|
||||
if (dialog.activity == activity) {
|
||||
i.remove();
|
||||
if (dialog.progress.isShowing()) {
|
||||
dialog.progress.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void dismiss()
|
||||
{
|
||||
// Running again with progress != null will destroy it
|
||||
activity.runOnUiThread(this);
|
||||
}
|
||||
|
||||
public void setMessage(final String message)
|
||||
{
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
progress.setMessage(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
private final String title;
|
||||
private final String message;
|
||||
private final Activity activity;
|
||||
private ProgressDialog progress;
|
||||
private final boolean finish;
|
||||
|
||||
// If we're dying, don't bother doing anything
|
||||
if (activity.isFinishing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (progress == null)
|
||||
{
|
||||
progress = new ProgressDialog(activity);
|
||||
|
||||
progress.setTitle(title);
|
||||
progress.setMessage(message);
|
||||
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
|
||||
progress.setOnCancelListener(this);
|
||||
|
||||
// If we want to finish the activity when this is killed, make it cancellable
|
||||
if (finish)
|
||||
{
|
||||
progress.setCancelable(true);
|
||||
progress.setCanceledOnTouchOutside(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
progress.setCancelable(false);
|
||||
}
|
||||
|
||||
synchronized (rundownDialogs) {
|
||||
rundownDialogs.add(this);
|
||||
progress.show();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
synchronized (rundownDialogs) {
|
||||
if (rundownDialogs.remove(this) && progress.isShowing()) {
|
||||
progress.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
|
||||
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
synchronized (rundownDialogs) {
|
||||
rundownDialogs.remove(this);
|
||||
}
|
||||
|
||||
// This will only be called if finish was true, so we don't need to check again
|
||||
activity.finish();
|
||||
}
|
||||
private SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
||||
{
|
||||
this.activity = activity;
|
||||
this.title = title;
|
||||
this.message = message;
|
||||
this.progress = null;
|
||||
this.finish = finish;
|
||||
}
|
||||
|
||||
public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish)
|
||||
{
|
||||
SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish);
|
||||
activity.runOnUiThread(spinner);
|
||||
return spinner;
|
||||
}
|
||||
|
||||
public static void closeDialogs(Activity activity)
|
||||
{
|
||||
synchronized (rundownDialogs) {
|
||||
Iterator<SpinnerDialog> i = rundownDialogs.iterator();
|
||||
while (i.hasNext()) {
|
||||
SpinnerDialog dialog = i.next();
|
||||
if (dialog.activity == activity) {
|
||||
i.remove();
|
||||
if (dialog.progress.isShowing()) {
|
||||
dialog.progress.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void dismiss()
|
||||
{
|
||||
// Running again with progress != null will destroy it
|
||||
activity.runOnUiThread(this);
|
||||
}
|
||||
|
||||
public void setMessage(final String message)
|
||||
{
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
progress.setMessage(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
// If we're dying, don't bother doing anything
|
||||
if (activity.isFinishing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (progress == null)
|
||||
{
|
||||
progress = new ProgressDialog(activity);
|
||||
|
||||
progress.setTitle(title);
|
||||
progress.setMessage(message);
|
||||
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
|
||||
progress.setOnCancelListener(this);
|
||||
|
||||
// If we want to finish the activity when this is killed, make it cancellable
|
||||
if (finish)
|
||||
{
|
||||
progress.setCancelable(true);
|
||||
progress.setCanceledOnTouchOutside(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
progress.setCancelable(false);
|
||||
}
|
||||
|
||||
synchronized (rundownDialogs) {
|
||||
rundownDialogs.add(this);
|
||||
progress.show();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
synchronized (rundownDialogs) {
|
||||
if (rundownDialogs.remove(this) && progress.isShowing()) {
|
||||
progress.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
synchronized (rundownDialogs) {
|
||||
rundownDialogs.remove(this);
|
||||
}
|
||||
|
||||
// This will only be called if finish was true, so we don't need to check again
|
||||
activity.finish();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.limelight.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.UiModeManager;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.view.View;
|
||||
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,9 +69,6 @@ int nv_avc_init(int width, int height, int perf_lvl, int thread_count) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Show frames even before a reference frame
|
||||
decoder_ctx->flags2 |= CODEC_FLAG2_SHOW_ALL;
|
||||
|
||||
if (perf_lvl & DISABLE_LOOP_FILTER) {
|
||||
// Skip the loop filter for performance reasons
|
||||
decoder_ctx->skip_loop_filter = AVDISCARD_ALL;
|
||||
@@ -370,17 +367,20 @@ int nv_avc_decode(unsigned char* indata, int inlen) {
|
||||
|
||||
// Only copy the picture at the end of decoding the packet
|
||||
if (got_pic) {
|
||||
// Clone the current decode frame outside of the mutex
|
||||
AVFrame* new_frame = av_frame_clone(dec_frame);
|
||||
AVFrame* old_frame;
|
||||
|
||||
// Swap it in under lock
|
||||
pthread_mutex_lock(&mutex);
|
||||
|
||||
// Only clone this frame if the last frame was taken.
|
||||
// This saves on extra copies for frames that don't get
|
||||
// rendered.
|
||||
if (yuv_frame == NULL) {
|
||||
// Clone a new frame
|
||||
yuv_frame = av_frame_clone(dec_frame);
|
||||
}
|
||||
|
||||
old_frame = yuv_frame;
|
||||
yuv_frame = new_frame;
|
||||
pthread_mutex_unlock(&mutex);
|
||||
|
||||
// Free the old frame outside of the mutex
|
||||
if (old_frame != NULL) {
|
||||
av_frame_free(&old_frame);
|
||||
}
|
||||
}
|
||||
|
||||
return err < 0 ? err : 0;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -2,6 +2,6 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle" >
|
||||
|
||||
<stroke android:width="1dip" android:color="#ffffff"/>
|
||||
|
||||
</shape>
|
||||
<stroke android:width="1dip" android:color="#ffffff"/>
|
||||
|
||||
</shape>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -8,48 +8,67 @@
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context=".PcView" >
|
||||
|
||||
<ListView
|
||||
android:id="@+id/pcListView"
|
||||
android:layout_width="match_parent"
|
||||
<RelativeLayout
|
||||
android:id="@+id/no_pc_found_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_centerHorizontal="true">
|
||||
<ImageView
|
||||
android:id="@+id/pcs_loading"
|
||||
android:layout_width="75dp"
|
||||
android:layout_height="75dp"
|
||||
android:src="@drawable/image_loading"/>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toRightOf="@+id/pcs_loading"
|
||||
android:layout_toEndOf="@+id/pcs_loading"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:gravity="center"
|
||||
android:text="@string/searching_pc"/>
|
||||
</RelativeLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/pcFragmentContainer"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_below="@+id/settingsButton"
|
||||
android:background="@drawable/list_view_unselected"
|
||||
android:fastScrollEnabled="true"
|
||||
android:longClickable="false"
|
||||
android:stackFromBottom="false" >
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_toLeftOf="@+id/manuallyAddPc"
|
||||
android:layout_toStartOf="@+id/manuallyAddPc"
|
||||
android:layout_toRightOf="@+id/settingsButton"
|
||||
android:layout_toEndOf="@+id/settingsButton"/>
|
||||
|
||||
</ListView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/discoveryText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_alignBaseline="@+id/settingsButton"
|
||||
android:text="@string/title_pc_view" />
|
||||
|
||||
<Button
|
||||
<ImageButton
|
||||
android:id="@+id/settingsButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignLeft="@+id/pcListView"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="65dp"
|
||||
android:cropToPadding="false"
|
||||
android:scaleType="fitXY"
|
||||
android:nextFocusDown="@+id/pcGridView"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="15dp"
|
||||
android:text="@string/button_stream_settings" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/manuallyAddPc"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignRight="@+id/pcListView"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="15dp"
|
||||
android:text="@string/button_add_pc_manually" />
|
||||
android:src="@drawable/settings"
|
||||
style="?android:attr/borderlessButtonStyle"/>
|
||||
|
||||
</RelativeLayout>
|
||||
<ImageButton
|
||||
android:id="@+id/manuallyAddPc"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="65dp"
|
||||
android:cropToPadding="false"
|
||||
android:scaleType="fitXY"
|
||||
android:nextFocusDown="@+id/pcGridView"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:src="@drawable/add_computer"
|
||||
style="?android:attr/borderlessButtonStyle"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
@@ -8,46 +8,65 @@
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context=".PcView" >
|
||||
|
||||
<ListView
|
||||
android:id="@+id/pcListView"
|
||||
android:layout_width="match_parent"
|
||||
<RelativeLayout
|
||||
android:id="@+id/no_pc_found_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_centerHorizontal="true">
|
||||
<ImageView
|
||||
android:id="@+id/pcs_loading"
|
||||
android:layout_width="75dp"
|
||||
android:layout_height="75dp"
|
||||
android:src="@drawable/image_loading"/>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toRightOf="@+id/pcs_loading"
|
||||
android:layout_toEndOf="@+id/pcs_loading"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:gravity="center"
|
||||
android:text="@string/searching_pc"/>
|
||||
</RelativeLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/pcFragmentContainer"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:layout_toLeftOf="@+id/manuallyAddPc"
|
||||
android:layout_toStartOf="@+id/manuallyAddPc"
|
||||
android:layout_toRightOf="@+id/settingsButton"
|
||||
android:layout_toEndOf="@+id/settingsButton"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_below="@+id/discoveryText"
|
||||
android:background="@drawable/list_view_unselected"
|
||||
android:fastScrollEnabled="true"
|
||||
android:longClickable="false"
|
||||
android:stackFromBottom="false" >
|
||||
android:layout_alignParentTop="true"/>
|
||||
|
||||
</ListView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/discoveryText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:layout_below="@+id/manuallyAddPc"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:text="@string/title_pc_view" />
|
||||
|
||||
<Button
|
||||
<ImageButton
|
||||
android:id="@+id/settingsButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="65dp"
|
||||
android:cropToPadding="false"
|
||||
android:scaleType="fitXY"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:text="@string/button_stream_settings" />
|
||||
android:src="@drawable/settings"
|
||||
style="?android:attr/borderlessButtonStyle"/>
|
||||
|
||||
<Button
|
||||
<ImageButton
|
||||
android:id="@+id/manuallyAddPc"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/settingsButton"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="@string/button_add_pc_manually" />
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="65dp"
|
||||
android:cropToPadding="false"
|
||||
android:scaleType="fitXY"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:src="@drawable/add_computer"
|
||||
style="?android:attr/borderlessButtonStyle"/>
|
||||
|
||||
</RelativeLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
@@ -5,16 +5,28 @@
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="10dp"
|
||||
tools:context=".Connection" >
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context=".AddComputerManually" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manuallyAddPcText"
|
||||
android:text="@string/title_add_pc"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:layout_alignParentTop="true"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/hostTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/manuallyAddPcText"
|
||||
android:layout_marginTop="25dp"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:ems="10"
|
||||
android:singleLine="true"
|
||||
android:inputType="textNoSuggestions"
|
||||
@@ -22,13 +34,5 @@
|
||||
|
||||
<requestFocus />
|
||||
</EditText>
|
||||
|
||||
<Button
|
||||
android:id="@+id/addPc"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/hostTextView"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="@string/button_add_pc" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
@@ -8,28 +8,27 @@
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context=".AppView" >
|
||||
|
||||
<ListView
|
||||
android:id="@+id/pcListView"
|
||||
android:layout_width="match_parent"
|
||||
<FrameLayout
|
||||
android:id="@+id/appFragmentContainer"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_below="@+id/appListText"
|
||||
android:fastScrollEnabled="true"
|
||||
android:longClickable="false"
|
||||
android:background="@drawable/list_view_unselected"
|
||||
android:stackFromBottom="false">
|
||||
</ListView>
|
||||
android:layout_below="@+id/appListText"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/appListText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:layout_alignParentTop="true"
|
||||
android:gravity="center"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="10dp" />
|
||||
android:paddingBottom="10dp"
|
||||
android:textSize="28sp"/>
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -2,7 +2,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#000"
|
||||
tools:context=".Game" >
|
||||
|
||||
<SurfaceView
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="20dp">
|
||||
<RelativeLayout
|
||||
android:id="@+id/grid_image_layout"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
<ImageView
|
||||
android:id="@+id/grid_image"
|
||||
android:cropToPadding="false"
|
||||
android:scaleType="fitXY"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="175dp">
|
||||
</ImageView>
|
||||
<ImageView
|
||||
android:id="@+id/grid_overlay"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp">
|
||||
</ImageView>
|
||||
</RelativeLayout>
|
||||
<TextView
|
||||
android:id="@+id/grid_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/grid_image_layout"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:textSize="18sp" >
|
||||
</TextView>
|
||||
</RelativeLayout>
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="10dp">
|
||||
<RelativeLayout
|
||||
android:id="@+id/grid_image_layout"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
<ImageView
|
||||
android:id="@+id/grid_image"
|
||||
android:cropToPadding="false"
|
||||
android:scaleType="fitXY"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="117dp">
|
||||
</ImageView>
|
||||
<ImageView
|
||||
android:id="@+id/grid_overlay"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_width="33dp"
|
||||
android:layout_height="33dp">
|
||||
</ImageView>
|
||||
</RelativeLayout>
|
||||
<TextView
|
||||
android:id="@+id/grid_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/grid_image_layout"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:textSize="14sp" >
|
||||
</TextView>
|
||||
</RelativeLayout>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?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">
|
||||
|
||||
<ListView
|
||||
android:id="@+id/fragmentView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/list_view_unselected"
|
||||
android:fastScrollEnabled="true"
|
||||
android:longClickable="false"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:stackFromBottom="false" >
|
||||
|
||||
</ListView>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="20dp">
|
||||
<RelativeLayout
|
||||
android:id="@+id/grid_image_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
<ImageView
|
||||
android:id="@+id/grid_image"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="100dp">
|
||||
</ImageView>
|
||||
<ImageView
|
||||
android:id="@+id/grid_overlay"
|
||||
android:layout_marginTop="15dp"
|
||||
android:layout_marginLeft="65dp"
|
||||
android:layout_marginStart="65dp"
|
||||
android:layout_marginRight="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp">
|
||||
</ImageView>
|
||||
</RelativeLayout>
|
||||
<TextView
|
||||
android:id="@+id/grid_text"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/grid_image_layout"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:gravity="center"
|
||||
android:textSize="18sp" >
|
||||
</TextView>
|
||||
</RelativeLayout>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="15dp">
|
||||
<RelativeLayout
|
||||
android:id="@+id/grid_image_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
<ImageView
|
||||
android:id="@+id/grid_image"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="67dp">
|
||||
</ImageView>
|
||||
<ImageView
|
||||
android:id="@+id/grid_overlay"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginLeft="42dp"
|
||||
android:layout_marginStart="42dp"
|
||||
android:layout_marginRight="13dp"
|
||||
android:layout_marginEnd="13dp"
|
||||
android:layout_width="33dp"
|
||||
android:layout_height="33dp">
|
||||
</ImageView>
|
||||
</RelativeLayout>
|
||||
<TextView
|
||||
android:id="@+id/grid_text"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/grid_image_layout"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:gravity="center"
|
||||
android:textSize="14sp" >
|
||||
</TextView>
|
||||
</RelativeLayout>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?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:gravity="center"/>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?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:gravity="center"/>
|
||||
</LinearLayout>
|
||||
@@ -3,13 +3,13 @@
|
||||
android:layout_width="fill_parent"
|
||||
android:padding="10dp"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/rowTextView"
|
||||
android:id="@+id/grid_text"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textIsSelectable="false"
|
||||
android:textSize="16sp" >
|
||||
</TextView>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="resolution_names">
|
||||
<item>720p - 30 FPS</item>
|
||||
<item>720p - 60 FPS</item>
|
||||
<item>1080p - 30 FPS</item>
|
||||
<item>1080p - 60 FPS</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="decoder_names">
|
||||
<item>Scegli decoder automaticamente</item>
|
||||
<item>Forza decoder software</item>
|
||||
<item>Forza decoder hardware</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- PC view menu entries -->
|
||||
<string name="pcview_menu_app_list">Lista applicazioni</string>
|
||||
<string name="pcview_menu_pair_pc">Accoppia PC</string>
|
||||
<string name="pcview_menu_unpair_pc">Disaccoppia PC</string>
|
||||
<string name="pcview_menu_send_wol">Invia richiesta Wake-On-LAN</string>
|
||||
<string name="pcview_menu_delete_pc">Rimuovi PC</string>
|
||||
|
||||
<!-- Pair messages -->
|
||||
<string name="pairing">Accoppiamento…</string>
|
||||
<string name="pair_pc_offline">PC offline</string>
|
||||
<string name="pair_pc_ingame">PC con applicazione avviata. Devi chiudere l\'applicazione prima dell\'accoppiamento.</string>
|
||||
<string name="pair_pairing_title">Accoppiamento</string>
|
||||
<string name="pair_pairing_msg">Inserisci il seguente PIN sul PC:</string>
|
||||
<string name="pair_incorrect_pin">PIN non corretto</string>
|
||||
<string name="pair_fail">Accoppiamento fallito</string>
|
||||
|
||||
<!-- WOL messages -->
|
||||
<string name="wol_pc_online">PC già avviato</string>
|
||||
<string name="wol_no_mac">Impossibile risvegliare il PC perchè GFE non ha inviato nessun indirizzo MAC</string>
|
||||
<string name="wol_waking_pc">Risveglio PC…</string>
|
||||
<string name="wol_waking_msg">Il PC potrebbe impiegare qualche secondo per risvegliarsi.
|
||||
Se non succede niente, assicurati che l\'opzione Wake-On-LAN sia configurata correttamente.
|
||||
</string>
|
||||
<string name="wol_fail">Invio pacchetto Wake-On-LAN fallito</string>
|
||||
|
||||
<!-- Unpair messages -->
|
||||
<string name="unpairing">Disaccoppiamento…</string>
|
||||
<string name="unpair_success">Disaccoppiato con successo</string>
|
||||
<string name="unpair_fail">Disaccoppiamento fallito</string>
|
||||
<string name="unpair_error">PC non accoppiato</string>
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="error_pc_offline">PC offline</string>
|
||||
<string name="error_manager_not_running">Il servizio ComputerManager non è avviato. Attendi qualche secondo o riavvia l\'applicazione.</string>
|
||||
<string name="error_unknown_host">Risoluzione nome host fallita</string>
|
||||
<string name="error_404">GFE ha ritornato un errore HTTP 404 error. Assicurati che il PC stia usando una GPU supportata.
|
||||
Usare un software di remote-desktop può causare questo errore. Prova a riavviare il PC o a reinstallare GFE.
|
||||
</string>
|
||||
|
||||
<!-- Start application messages -->
|
||||
<string name="conn_establishing_title">Connessione</string>
|
||||
<string name="conn_establishing_msg">Connessione in corso</string>
|
||||
<string name="conn_metered">Attenzione: la rete attiva prevede costi aggiuntivi in base all\'utilizzo!</string>
|
||||
<string name="conn_client_latency">Latenza frame media client-side:</string>
|
||||
<string name="conn_client_latency_hw">latenza decoder hardware:</string>
|
||||
<string name="conn_hardware_latency">Latenza decoder hardware media:</string>
|
||||
<string name="conn_starting">Avvio in corso…</string>
|
||||
<string name="conn_error_title">Errore connessione</string>
|
||||
<string name="conn_error_msg">Avvio fallito</string>
|
||||
<string name="conn_terminated_title">Connessione terminata</string>
|
||||
<string name="conn_terminated_msg">La connessione è stata interrotta</string>
|
||||
|
||||
<!-- General strings -->
|
||||
<string name="ip_hint">Indirizzo IP del PC</string>
|
||||
<string name="searching_pc">Ricerca PC in corso…</string>
|
||||
<string name="yes">Sì</string>
|
||||
<string name="no">No</string>
|
||||
<string name="lost_connection">Connessione con il PC persa</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="title_applist">Applicazioni su</string>
|
||||
<string name="applist_menu_resume">Riprendi Sessione</string>
|
||||
<string name="applist_menu_quit">Chiudi Sessione</string>
|
||||
<string name="applist_menu_quit_and_start">Chiudi sessione corrente e avvia</string>
|
||||
<string name="applist_menu_cancel">Annulla</string>
|
||||
<string name="applist_refresh_title">Lista applicazioni</string>
|
||||
<string name="applist_refresh_msg">Aggiornamento lista in corso…</string>
|
||||
<string name="applist_refresh_error_title">Errore</string>
|
||||
<string name="applist_refresh_error_msg">Ricezione lista applicazioni fallita</string>
|
||||
<string name="applist_quit_app">Chiusura in corso…</string>
|
||||
<string name="applist_quit_success">Sessione chiusa con successo</string>
|
||||
<string name="applist_quit_fail">Chiusura sessione fallita</string>
|
||||
<string name="applist_quit_confirmation">Sei sicuro di voler chiudere l\'applicazione avviata? Tutti i dati non salvati saranno persi.</string>
|
||||
|
||||
<!-- Add computer manually activity -->
|
||||
<string name="title_add_pc">Aggiungi PC Manualmente</string>
|
||||
<string name="msg_add_pc">Connessione al PC in corso…</string>
|
||||
<string name="addpc_fail">Impossibile connettersi al PC. Assicurati che il firewall del PC sia configurato correttamente.</string>
|
||||
<string name="addpc_success">PC aggiunto con successo</string>
|
||||
<string name="addpc_unknown_host">Impossibile risovere l\'indirizzo del PC. Assicurati di aver scritto correttamente l\'indirizzo.</string>
|
||||
<string name="addpc_enter_ip">Devi inserire un indirizzo IP</string>
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="category_basic_settings">Impostazioni Base</string>
|
||||
<string name="title_resolution_list">Risoluzione e FPS</string>
|
||||
<string name="summary_resolution_list">Valori troppo elevati possono causare lag o crash</string>
|
||||
<string name="title_seekbar_bitrate">Bitrate video</string>
|
||||
<string name="summary_seekbar_bitrate">Abbassa il bitrate per ridurre lo stuttering; alza il bitrate per aumenteare la qualità dell\'immagine</string>
|
||||
<string name="suffix_seekbar_bitrate">Mbps</string>
|
||||
<string name="title_checkbox_stretch_video">Forza video in full-screen</string>
|
||||
<string name="title_checkbox_disable_warnings">Disabilita messaggi di warning</string>
|
||||
<string name="summary_checkbox_disable_warnings">Disabilita i messaggi di warning sullo schermo durante lo streaming</string>
|
||||
|
||||
<string name="category_gamepad_settings">Impostazioni Gamepad</string>
|
||||
<string name="title_checkbox_multi_controller">Supporto controller multipli</string>
|
||||
<string name="summary_checkbox_multi_controller">Quando disabilitato, tutti i controllers appaiono come uno solo</string>
|
||||
<string name="title_seekbar_deadzone">Aggiusta deadzone degli stick analogici</string>
|
||||
<string name="suffix_seekbar_deadzone">%</string>
|
||||
|
||||
<string name="category_ui_settings">Impostazioni Interfaccia</string>
|
||||
<string name="title_language_list">Lingua</string>
|
||||
<string name="summary_language_list">Lingua da usare in Limelight</string>
|
||||
<string name="title_checkbox_list_mode">Usa lista invece della griglia</string>
|
||||
<string name="summary_checkbox_list_mode">Visualizza applicazioni e computers in una lista invece di una griglia</string>
|
||||
<string name="title_checkbox_small_icon_mode">Usa icone piccole</string>
|
||||
<string name="summary_checkbox_small_icon_mode">Usa icone piccole nella vista a griglia per avere più oggetti sullo schermo</string>
|
||||
|
||||
<string name="category_host_settings">Impostazioni Host</string>
|
||||
<string name="title_checkbox_enable_sops">Ottimizza le impostazioni dei giochi</string>
|
||||
<string name="summary_checkbox_enable_sops">Permetti a GFE di modificare le impostazioni dei giochi per uno streaming ottimale</string>
|
||||
<string name="title_checkbox_host_audio">Riproduci audio sul PC</string>
|
||||
<string name="summary_checkbox_host_audio">Riproduci l\'audio sul computer e su questo dispositivo</string>
|
||||
|
||||
<string name="category_advanced_settings">Impostazioni Avanzate</string>
|
||||
<string name="title_decoder_list">Cambia decoder</string>
|
||||
<string name="summary_decoder_list">Il decoder software può ridurre la latenza video quando si usano impostazioni streaming basse</string>
|
||||
</resources>
|
||||
@@ -1,8 +0,0 @@
|
||||
<resources>
|
||||
|
||||
<!--
|
||||
Customize dimensions originally defined in res/values/dimens.xml (such as
|
||||
screen margins) for sw600dp devices (e.g. 7" tablets) here.
|
||||
-->
|
||||
|
||||
</resources>
|
||||
@@ -1,9 +0,0 @@
|
||||
<resources>
|
||||
|
||||
<!--
|
||||
Customize dimensions originally defined in res/values/dimens.xml (such as
|
||||
screen margins) for sw720dp devices (e.g. 10" tablets) in landscape here.
|
||||
-->
|
||||
<dimen name="activity_horizontal_margin">128dp</dimen>
|
||||
|
||||
</resources>
|
||||
@@ -3,7 +3,7 @@
|
||||
<!--
|
||||
Base application theme for API 21+. This theme completely replaces
|
||||
AppBaseTheme from BOTH res/values/styles.xml and
|
||||
res/values-v11/styles.xml on API 21+ devices.
|
||||
res/values-v21/styles.xml on API 21+ devices.
|
||||
-->
|
||||
<style name="AppBaseTheme" parent="android:Theme.Material">
|
||||
<!-- API 21 theme customizations can go here. -->
|
||||
|
||||
@@ -6,21 +6,32 @@
|
||||
<item>1080p 30 FPS</item>
|
||||
<item>1080p 60 FPS</item>
|
||||
</string-array>
|
||||
<string-array name="resolution_values">
|
||||
<string-array name="resolution_values" translatable="false">
|
||||
<item>720p30</item>
|
||||
<item>720p60</item>
|
||||
<item>1080p30</item>
|
||||
<item>1080p60</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="language_names" translatable="false">
|
||||
<item>Default</item>
|
||||
<item>English</item>
|
||||
<item>Italiano</item>
|
||||
</string-array>
|
||||
<string-array name="language_values" translatable="false">
|
||||
<item>default</item>
|
||||
<item>en</item>
|
||||
<item>it</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="decoder_names">
|
||||
<item>Force Software Decoding</item>
|
||||
<item>Auto-select Decoder</item>
|
||||
<item>Force Software Decoding</item>
|
||||
<item>Force Hardware Decoding</item>
|
||||
</string-array>
|
||||
<string-array name="decoder_values">
|
||||
<item>software</item>
|
||||
<string-array name="decoder_values" translatable="false">
|
||||
<item>auto</item>
|
||||
<item>software</item>
|
||||
<item>hardware</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<resources>
|
||||
|
||||
<!--
|
||||
Declare custom theme attributes that allow changing which styles are
|
||||
used for button bars depending on the API level.
|
||||
?android:attr/buttonBarStyle is new as of API 11 so this is
|
||||
necessary to support previous API levels.
|
||||
-->
|
||||
<declare-styleable name="ButtonBarContainerTheme">
|
||||
<attr name="buttonBarStyle" format="reference" />
|
||||
<attr name="buttonBarButtonStyle" format="reference" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
@@ -1,16 +1,87 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- PC view menu entries -->
|
||||
<string name="pcview_menu_app_list">View Game List</string>
|
||||
<string name="pcview_menu_pair_pc">Pair with PC</string>
|
||||
<string name="pcview_menu_unpair_pc">Unpair</string>
|
||||
<string name="pcview_menu_send_wol">Send Wake-On-LAN request</string>
|
||||
<string name="pcview_menu_delete_pc">Delete PC</string>
|
||||
|
||||
<!-- Pair messages -->
|
||||
<string name="pairing">Pairing…</string>
|
||||
<string name="pair_pc_offline">Computer is offline</string>
|
||||
<string name="pair_pc_ingame">Computer is currently in a game. You must close the game before pairing.</string>
|
||||
<string name="pair_pairing_title">Pairing</string>
|
||||
<string name="pair_pairing_msg">Please enter the following PIN on the target PC:</string>
|
||||
<string name="pair_incorrect_pin">Incorrect PIN</string>
|
||||
<string name="pair_fail">Pairing failed</string>
|
||||
|
||||
<!-- WOL messages -->
|
||||
<string name="wol_pc_online">Computer is online</string>
|
||||
<string name="wol_no_mac">Unable to wake PC because GFE didn\'t send a MAC address</string>
|
||||
<string name="wol_waking_pc">Waking PC…</string>
|
||||
<string name="wol_waking_msg">It may take a few seconds for your PC to wake up.
|
||||
If it doesn\'t, make sure it\'s configured properly for Wake-On-LAN.
|
||||
</string>
|
||||
<string name="wol_fail">Failed to send Wake-On-LAN packets</string>
|
||||
|
||||
<!-- Unpair messages -->
|
||||
<string name="unpairing">Unpairing…</string>
|
||||
<string name="unpair_success">Unpaired successfully</string>
|
||||
<string name="unpair_fail">Failed to unpair</string>
|
||||
<string name="unpair_error">Device was not paired</string>
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="error_pc_offline">Computer is offline</string>
|
||||
<string name="error_manager_not_running">The ComputerManager service is not running. Please wait a few seconds or restart the app.</string>
|
||||
<string name="error_unknown_host">Failed to resolve host</string>
|
||||
<string name="error_404">GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU.
|
||||
Using remote desktop software can also cause this error. Try rebooting your machine or reinstalling GFE.
|
||||
</string>
|
||||
|
||||
<!-- Start application messages -->
|
||||
<string name="conn_establishing_title">Establishing Connection</string>
|
||||
<string name="conn_establishing_msg">Starting connection</string>
|
||||
<string name="conn_metered">Warning: Your active network connection is metered!</string>
|
||||
<string name="conn_client_latency">Average client-side frame latency:</string>
|
||||
<string name="conn_client_latency_hw">hardware decoder latency:</string>
|
||||
<string name="conn_hardware_latency">Average hardware decoder latency:</string>
|
||||
<string name="conn_starting">Starting</string>
|
||||
<string name="conn_error_title">Connection Error</string>
|
||||
<string name="conn_error_msg">Failed to start</string>
|
||||
<string name="conn_terminated_title">Connection Terminated</string>
|
||||
<string name="conn_terminated_msg">The connection was terminated</string>
|
||||
|
||||
<!-- General strings -->
|
||||
<string name="ip_hint">IP address of GeForce PC</string>
|
||||
|
||||
<!-- PC view activity -->
|
||||
<string name="title_pc_view">PC List</string>
|
||||
<string name="button_stream_settings">Streaming Settings</string>
|
||||
<string name="button_add_pc_manually">Add PC Manually</string>
|
||||
|
||||
<string name="searching_pc">Searching for PCs…</string>
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
<string name="lost_connection">Lost connection to PC</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="title_applist">Apps on</string>
|
||||
<string name="applist_menu_resume">Resume Session</string>
|
||||
<string name="applist_menu_quit">Quit Session</string>
|
||||
<string name="applist_menu_quit_and_start">Quit Current Game and Start</string>
|
||||
<string name="applist_menu_cancel">Cancel</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>
|
||||
<string name="applist_refresh_error_msg">Failed to get app list</string>
|
||||
<string name="applist_quit_app">Quitting</string>
|
||||
<string name="applist_quit_success">Successfully quit</string>
|
||||
<string name="applist_quit_fail">Failed to quit</string>
|
||||
<string name="applist_quit_confirmation">Are you sure you want to quit the running app? All unsaved data will be lost.</string>
|
||||
|
||||
<!-- Add computer manually activity -->
|
||||
<string name="button_add_pc">Manually Add PC</string>
|
||||
<string name="title_add_pc">Add PC Manually</string>
|
||||
<string name="msg_add_pc">Connecting to the PC…</string>
|
||||
<string name="addpc_fail">Unable to connect to the specified computer. Make sure the required ports are allowed through the firewall.</string>
|
||||
<string name="addpc_success">Successfully added computer</string>
|
||||
<string name="addpc_unknown_host">Unable to resolve PC address. Make sure you didn\'t make a typo in the address.</string>
|
||||
<string name="addpc_enter_ip">You must enter an IP address</string>
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="category_basic_settings">Basic Settings</string>
|
||||
@@ -23,12 +94,27 @@
|
||||
<string name="title_checkbox_disable_warnings">Disable warning messages</string>
|
||||
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
|
||||
|
||||
<string name="category_gamepad_settings">Gamepad Settings</string>
|
||||
<string name="title_checkbox_multi_controller">Multiple controller support</string>
|
||||
<string name="summary_checkbox_multi_controller">When unchecked, all controllers appear as one</string>
|
||||
<string name="title_seekbar_deadzone">Adjust analog stick deadzone</string>
|
||||
<string name="suffix_seekbar_deadzone">%</string>
|
||||
|
||||
<string name="category_ui_settings">UI Settings</string>
|
||||
<string name="title_language_list">Language</string>
|
||||
<string name="summary_language_list">Language to use for Limelight</string>
|
||||
<string name="title_checkbox_list_mode">Use lists instead of grids</string>
|
||||
<string name="summary_checkbox_list_mode">Display apps and PCs in lists instead of grids</string>
|
||||
<string name="title_checkbox_small_icon_mode">Use small icons</string>
|
||||
<string name="summary_checkbox_small_icon_mode">Use small icons in grid items to allow more items on screen</string>
|
||||
|
||||
<string name="category_host_settings">Host Settings</string>
|
||||
<string name="title_checkbox_enable_sops">Optimize game settings</string>
|
||||
<string name="summary_checkbox_enable_sops">Allow GFE to modify game settings for optimal streaming</string>
|
||||
<string name="title_checkbox_host_audio">Play audio on PC</string>
|
||||
<string name="summary_checkbox_host_audio">Play audio from the computer and this device. Requires GFE 2.1.2+</string>
|
||||
<string name="summary_checkbox_host_audio">Play audio from the computer and this device</string>
|
||||
|
||||
<string name="category_advanced_settings">Advanced Settings</string>
|
||||
<string name="title_decoder_list">Change decoder</string>
|
||||
<string name="summary_decoder_list">Software decoding may improve video latency at lower streaming settings</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,40 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!--
|
||||
Base application theme, dependent on API level. This theme is replaced
|
||||
by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
|
||||
Base application theme, dependent on API level. This theme is replaced
|
||||
by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
|
||||
|
||||
-->
|
||||
<style name="AppBaseTheme" parent="android:Theme">
|
||||
<!--
|
||||
<!--
|
||||
Theme customizations available in newer API levels can go in
|
||||
res/values-vXX/styles.xml, while customizations related to
|
||||
Theme customizations available in newer API levels can go in
|
||||
res/values-vXX/styles.xml, while customizations related to
|
||||
backward-compatibility can go here.
|
||||
|
||||
-->
|
||||
</style>
|
||||
|
||||
<!-- Application theme. -->
|
||||
<style name="AppTheme" parent="AppBaseTheme">
|
||||
<!-- Application theme. -->
|
||||
<style name="AppTheme" parent="AppBaseTheme">
|
||||
<!-- All customizations that are NOT specific to a particular API-level can go here. -->
|
||||
<item name="android:windowActionBar">false</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
</style>
|
||||
|
||||
</style>
|
||||
|
||||
<style name="FullscreenTheme" parent="android:Theme.NoTitleBar">
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<item name="android:windowBackground">@null</item>
|
||||
<item name="buttonBarStyle">@style/ButtonBar</item>
|
||||
<!-- Stream activity theme -->
|
||||
<style name="StreamTheme" parent="AppBaseTheme">
|
||||
<!-- All customizations that are NOT specific to a particular API-level can go here. -->
|
||||
<item name="android:windowActionBar">false</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
|
||||
</style>
|
||||
|
||||
<style name="ButtonBar">
|
||||
<item name="android:paddingLeft">2dp</item>
|
||||
<item name="android:paddingTop">5dp</item>
|
||||
<item name="android:paddingRight">2dp</item>
|
||||
<!-- Transparent streaming background to avoid extra overdraw -->
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -26,6 +26,19 @@
|
||||
android:summary="@string/summary_checkbox_disable_warnings"
|
||||
android:defaultValue="false" />
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/category_gamepad_settings">
|
||||
<!--com.limelight.preferences.SeekBarPreference
|
||||
android:key="seekbar_deadzone"
|
||||
android:defaultValue="15"
|
||||
android:max="50"
|
||||
android:text="@string/suffix_seekbar_deadzone"
|
||||
android:title="@string/title_seekbar_deadzone"/-->
|
||||
<CheckBoxPreference
|
||||
android:key="checkbox_multi_controller"
|
||||
android:title="@string/title_checkbox_multi_controller"
|
||||
android:summary="@string/summary_checkbox_multi_controller"
|
||||
android:defaultValue="true" />
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/category_host_settings">
|
||||
<CheckBoxPreference
|
||||
android:key="checkbox_enable_sops"
|
||||
@@ -38,13 +51,32 @@
|
||||
android:summary="@string/summary_checkbox_host_audio"
|
||||
android:defaultValue="false" />
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/category_ui_settings">
|
||||
<ListPreference
|
||||
android:key="list_languages"
|
||||
android:title="@string/title_language_list"
|
||||
android:entries="@array/language_names"
|
||||
android:entryValues="@array/language_values"
|
||||
android:summary="@string/summary_language_list"
|
||||
android:defaultValue="default" />
|
||||
<com.limelight.preferences.SmallIconCheckboxPreference
|
||||
android:key="checkbox_small_icon_mode"
|
||||
android:title="@string/title_checkbox_small_icon_mode"
|
||||
android:summary="@string/summary_checkbox_small_icon_mode" />
|
||||
<CheckBoxPreference
|
||||
android:key="checkbox_list_mode"
|
||||
android:title="@string/title_checkbox_list_mode"
|
||||
android:summary="@string/summary_checkbox_list_mode"
|
||||
android:defaultValue="false" />
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/category_advanced_settings">
|
||||
<ListPreference
|
||||
android:key="list_decoders"
|
||||
android:title="@string/title_decoder_list"
|
||||
android:entries="@array/decoder_names"
|
||||
android:entryValues="@array/decoder_values"
|
||||
android:summary="@string/summary_decoder_list"
|
||||
android:defaultValue="auto" />
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
</PreferenceScreen>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.limelight" >
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- Non-root application name -->
|
||||
<application android:label="Limelight" />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.limelight.root" >
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- Root permissions -->
|
||||
<uses-permission android:name="android.permission.ACCESS_SUPERUSER" />
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:0.13.2'
|
||||
classpath 'com.android.tools.build:gradle:1.0.1'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-3
@@ -4,16 +4,22 @@ This file serves to document some of the decoder errata when using MediaCodec ha
|
||||
- Affected decoders: TI OMAP4, Allwinner A20
|
||||
|
||||
2. Some decoders have a huge per-frame latency with the unmodified SPS sent from NVENC. Setting max_dec_frame_buffering fixes this latency issue.
|
||||
- Affected decoders: NVIDIA Tegra 3 and 4
|
||||
- Affected decoders: NVIDIA Tegra 3 and 4, Broadcom VideoCore IV
|
||||
|
||||
3. Some decoders strictly require that you pass BUFFER_FLAG_CODEC_CONFIG and crash upon the IDR frame if you don't
|
||||
- Affected decoders: TI OMAP4
|
||||
|
||||
4. Some decoders require num_ref_frames=1 and max_dec_frame_buffering=1 to avoid crashing on SPS or first I-frame
|
||||
4. Some decoders require num_ref_frames=1 and max_dec_frame_buffering=1 to avoid crashing on SPS on first I-frame
|
||||
- Affected decoders: Qualcomm in GS3 on 4.3+, Exynos 4 at 1080p only
|
||||
|
||||
5. Some decoders will hang if max_dec_frame_buffering is not present
|
||||
- Affected decoders: MediaTek decoder in Fire HD 7 (2014)
|
||||
|
||||
6. Some decoders will hang if max_dec_frame_buffering IS present
|
||||
- Affected decoders: Exynos 5 in Galaxy Note 10.1 (2014)
|
||||
- Affected decoders: Exynos 5 in Galaxy Note 10.1 (2014)
|
||||
|
||||
7. Some decoders will not enter low latency mode if adaptive playback is enabled
|
||||
- Affected decoders: Intel decoder in Nexus Player
|
||||
|
||||
8. Some decoders will not enter low latency mode if the profile isn't baseline in the first SPS.
|
||||
- Affected decoders: Intel decoder in Nexus Player
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
#Wed Apr 10 15:27:10 PDT 2013
|
||||
#Sun Dec 07 22:52:07 PST 2014
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
|
||||
|
||||
Reference in New Issue
Block a user