mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-10-20 15:27:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4affc3c4ce | ||
|
|
067be54715 | ||
|
|
0dad2dc64b | ||
|
|
867b703644 | ||
|
|
3d398ef6dd | ||
|
|
85d95b2d8e | ||
|
|
d091d9db6b | ||
|
|
e081ab5239 | ||
|
|
7b12fd1ad2 | ||
|
|
80d8c5953e | ||
|
|
194037ff41 | ||
|
|
094d642739 | ||
|
|
010e03252e | ||
|
|
98638186b5 | ||
|
|
c5293ef21f | ||
|
|
366a1c91b8 | ||
|
|
157450e674 | ||
|
|
1b8d2bc81c | ||
|
|
f1787c43e5 | ||
|
|
95ea88e932 | ||
|
|
f2b8461bb9 | ||
|
|
7838a787df | ||
|
|
cc3f2ecb07 | ||
|
|
833b7c3916 | ||
|
|
90209f2ca2 | ||
|
|
2681036c32 | ||
|
|
ee58071ff1 | ||
|
|
e222f2f6c3 | ||
|
|
0b7becb161 | ||
|
|
bf795ab7a5 | ||
|
|
59df38ae8a | ||
|
|
e04ff048b8 | ||
|
|
7d25d07c6d | ||
|
|
7b0ddfae42 | ||
|
|
43fa1a7245 | ||
|
|
057530eed0 | ||
|
|
aee34f6365 |
No files matched your search
+9
-13
@@ -12,9 +12,9 @@
|
|||||||
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
|
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
|
||||||
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
|
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
|
||||||
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
|
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
|
||||||
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" />
|
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugAndroidTest" />
|
||||||
<option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" />
|
<option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" />
|
||||||
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugTestSources" />
|
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugAndroidTestSources" />
|
||||||
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
||||||
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
||||||
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
|
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="NewModuleRootManager" inherit-compiler-output="false">
|
<component name="NewModuleRootManager" inherit-compiler-output="false">
|
||||||
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
|
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
|
||||||
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/test/nonRoot/debug" />
|
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/nonRoot/debug" />
|
||||||
<exclude-output />
|
<exclude-output />
|
||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
|
||||||
@@ -41,12 +41,12 @@
|
|||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/java" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/java" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/jni" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/jni" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/test/nonRoot/debug" isTestSource="true" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/test/nonRoot/debug" isTestSource="true" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/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/source/rs/androidTest/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/rs/androidTest/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$/build/generated/res/generated/androidTest/nonRoot/debug" type="java-test-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-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/resources" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
|
||||||
@@ -106,14 +106,10 @@
|
|||||||
<orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
|
<orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
|
<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="jmdns-fixed" level="project" />
|
||||||
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" 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="tinyrtsp" level="project" />
|
||||||
<orderEntry type="library" exported="" name="limelight-common" 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="okhttp-2.2.0" level="project" />
|
||||||
<orderEntry type="library" exported="" name="jcodec-0.1.9" level="project" />
|
<orderEntry type="library" exported="" name="jcodec-0.1.9" level="project" />
|
||||||
<orderEntry type="library" exported="" name="okio-1.2.0" level="project" />
|
<orderEntry type="library" exported="" name="okio-1.2.0" level="project" />
|
||||||
|
|||||||
+2
-6
@@ -11,8 +11,8 @@ android {
|
|||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 21
|
targetSdkVersion 21
|
||||||
|
|
||||||
versionName "3.1"
|
versionName "3.1.2"
|
||||||
versionCode = 53
|
versionCode = 56
|
||||||
}
|
}
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
@@ -67,10 +67,6 @@ dependencies {
|
|||||||
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
|
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
|
||||||
compile group: 'org.bouncycastle', name: 'bcpkix-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.okhttp', name: 'okhttp', version:'2.2.0'
|
||||||
compile group: 'com.squareup.okio', name:'okio', version:'1.2.0'
|
compile group: 'com.squareup.okio', name:'okio', version:'1.2.0'
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -13,6 +13,7 @@ import com.limelight.computers.ComputerManagerListener;
|
|||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
import com.limelight.grid.AppGridAdapter;
|
import com.limelight.grid.AppGridAdapter;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.GfeHttpResponseException;
|
||||||
import com.limelight.nvstream.http.NvApp;
|
import com.limelight.nvstream.http.NvApp;
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
@@ -54,9 +55,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
private SpinnerDialog blockingLoadSpinner;
|
private SpinnerDialog blockingLoadSpinner;
|
||||||
private String lastRawApplist;
|
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 START_OR_RESUME_ID = 1;
|
||||||
private final static int QUIT_ID = 2;
|
private final static int QUIT_ID = 2;
|
||||||
private final static int CANCEL_ID = 3;
|
private final static int CANCEL_ID = 3;
|
||||||
@@ -127,30 +125,24 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
@Override
|
@Override
|
||||||
public void notifyComputerUpdated(ComputerDetails details) {
|
public void notifyComputerUpdated(ComputerDetails details) {
|
||||||
// Don't care about other computers
|
// Don't care about other computers
|
||||||
if (details != computer) {
|
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (details.state != ComputerDetails.State.ONLINE) {
|
if (details.state == ComputerDetails.State.OFFLINE) {
|
||||||
consecutiveAppListFailures++;
|
// The PC is unreachable now
|
||||||
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
if (consecutiveAppListFailures >= CONSECUTIVE_FAILURE_LIMIT) {
|
@Override
|
||||||
// The PC is unreachable now
|
public void run() {
|
||||||
AppView.this.runOnUiThread(new Runnable() {
|
// Display a toast to the user and quit the activity
|
||||||
@Override
|
Toast.makeText(AppView.this, getResources().getText(R.string.lost_connection), Toast.LENGTH_SHORT).show();
|
||||||
public void run() {
|
finish();
|
||||||
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
consecutiveAppListFailures = 0;
|
|
||||||
|
|
||||||
// App list is the same or empty; nothing to do
|
// App list is the same or empty; nothing to do
|
||||||
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
|
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
|
||||||
return;
|
return;
|
||||||
@@ -182,6 +174,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
if (managerBinder != null) {
|
if (managerBinder != null) {
|
||||||
managerBinder.stopPolling();
|
managerBinder.stopPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (appGridAdapter != null) {
|
||||||
|
appGridAdapter.cancelQueuedOperations();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -264,10 +260,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
int runningAppId = -1;
|
int runningAppId = -1;
|
||||||
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||||
AppObject app = (AppObject) appGridAdapter.getItem(i);
|
AppObject app = (AppObject) appGridAdapter.getItem(i);
|
||||||
if (app.app == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (app.app.getIsRunning()) {
|
if (app.app.getIsRunning()) {
|
||||||
runningAppId = app.app.getAppId();
|
runningAppId = app.app.getAppId();
|
||||||
break;
|
break;
|
||||||
@@ -282,10 +274,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
|
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
|
||||||
if (selectedApp == null || selectedApp.app == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int runningAppId = getRunningAppId();
|
int runningAppId = getRunningAppId();
|
||||||
if (runningAppId != -1) {
|
if (runningAppId != -1) {
|
||||||
if (runningAppId == selectedApp.app.getAppId()) {
|
if (runningAppId == selectedApp.app.getAppId()) {
|
||||||
@@ -374,20 +362,17 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
public void run() {
|
public void run() {
|
||||||
boolean updated = false;
|
boolean updated = false;
|
||||||
|
|
||||||
|
// First handle app updates and additions
|
||||||
for (NvApp app : appList) {
|
for (NvApp app : appList) {
|
||||||
boolean foundExistingApp = false;
|
boolean foundExistingApp = false;
|
||||||
|
|
||||||
// Try to update an existing app in the list first
|
// Try to update an existing app in the list first
|
||||||
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||||
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||||
if (existingApp.app == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingApp.app.getAppId() == app.getAppId()) {
|
if (existingApp.app.getAppId() == app.getAppId()) {
|
||||||
// Found the app; update its properties
|
// Found the app; update its properties
|
||||||
if (existingApp.app.getIsRunning() != app.getIsRunning()) {
|
if (existingApp.app.getIsRunning() != app.getIsRunning()) {
|
||||||
existingApp.app.setIsRunningBoolean(app.getIsRunning());
|
existingApp.app.setIsRunning(app.getIsRunning());
|
||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
if (!existingApp.app.getAppName().equals(app.getAppName())) {
|
if (!existingApp.app.getAppName().equals(app.getAppName())) {
|
||||||
@@ -407,6 +392,34 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Next handle app removals
|
||||||
|
int i = 0;
|
||||||
|
while (i < appGridAdapter.getCount()) {
|
||||||
|
boolean foundExistingApp = false;
|
||||||
|
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||||
|
|
||||||
|
// Check if this app is in the latest list
|
||||||
|
for (NvApp app : appList) {
|
||||||
|
if (existingApp.app.getAppId() == app.getAppId()) {
|
||||||
|
foundExistingApp = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This app was removed in the latest app list
|
||||||
|
if (!foundExistingApp) {
|
||||||
|
appGridAdapter.removeApp(existingApp);
|
||||||
|
updated = true;
|
||||||
|
|
||||||
|
// Check this same index again because the item at i+1 is now at i after
|
||||||
|
// the removal
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move on to the next item
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
if (updated) {
|
if (updated) {
|
||||||
appGridAdapter.notifyDataSetChanged();
|
appGridAdapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
@@ -419,7 +432,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
intent.putExtra(Game.EXTRA_HOST,
|
intent.putExtra(Game.EXTRA_HOST,
|
||||||
computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||||
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
|
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
|
||||||
intent.putExtra(Game.EXTRA_APP, app.getAppName());
|
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
||||||
|
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
||||||
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
||||||
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
||||||
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
||||||
@@ -437,10 +451,18 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
httpConn = new NvHTTP(getAddress(),
|
httpConn = new NvHTTP(getAddress(),
|
||||||
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(AppView.this));
|
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(AppView.this));
|
||||||
if (httpConn.quitApp()) {
|
if (httpConn.quitApp()) {
|
||||||
message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName();
|
message = getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
|
||||||
|
} else {
|
||||||
|
message = getResources().getString(R.string.applist_quit_fail) + " " + app.getAppName();
|
||||||
|
}
|
||||||
|
} catch (GfeHttpResponseException e) {
|
||||||
|
if (e.getErrorCode() == 599) {
|
||||||
|
message = "This session wasn't started by this device," +
|
||||||
|
" so it cannot be quit. End streaming on the original " +
|
||||||
|
"device or the PC itself. (Error code: "+e.getErrorCode()+")";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName();
|
message = e.getMessage();
|
||||||
}
|
}
|
||||||
} catch (UnknownHostException e) {
|
} catch (UnknownHostException e) {
|
||||||
message = getResources().getString(R.string.error_unknown_host);
|
message = getResources().getString(R.string.error_unknown_host);
|
||||||
@@ -481,9 +503,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||||
long id) {
|
long id) {
|
||||||
AppObject app = (AppObject) appGridAdapter.getItem(pos);
|
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
|
// Only open the context menu if something is running, otherwise start it
|
||||||
if (getRunningAppId() != -1) {
|
if (getRunningAppId() != -1) {
|
||||||
@@ -501,6 +520,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
public final NvApp app;
|
public final NvApp app;
|
||||||
|
|
||||||
public AppObject(NvApp app) {
|
public AppObject(NvApp app) {
|
||||||
|
if (app == null) {
|
||||||
|
throw new IllegalArgumentException("app must not be null");
|
||||||
|
}
|
||||||
this.app = app;
|
this.app = app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.limelight.nvstream.NvConnection;
|
|||||||
import com.limelight.nvstream.NvConnectionListener;
|
import com.limelight.nvstream.NvConnectionListener;
|
||||||
import com.limelight.nvstream.StreamConfiguration;
|
import com.limelight.nvstream.StreamConfiguration;
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
import com.limelight.nvstream.input.KeyboardPacket;
|
import com.limelight.nvstream.input.KeyboardPacket;
|
||||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
@@ -89,7 +90,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private int drFlags = 0;
|
private int drFlags = 0;
|
||||||
|
|
||||||
public static final String EXTRA_HOST = "Host";
|
public static final String EXTRA_HOST = "Host";
|
||||||
public static final String EXTRA_APP = "App";
|
public static final String EXTRA_APP_NAME = "AppName";
|
||||||
|
public static final String EXTRA_APP_ID = "AppId";
|
||||||
public static final String EXTRA_UNIQUEID = "UniqueId";
|
public static final String EXTRA_UNIQUEID = "UniqueId";
|
||||||
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
||||||
|
|
||||||
@@ -171,17 +173,23 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
wifiLock.acquire();
|
wifiLock.acquire();
|
||||||
|
|
||||||
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
|
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
|
||||||
String app = Game.this.getIntent().getStringExtra(EXTRA_APP);
|
String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME);
|
||||||
|
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
|
||||||
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
||||||
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
|
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
|
||||||
|
|
||||||
|
if (appId == StreamConfiguration.INVALID_APP_ID) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
decoderRenderer = new ConfigurableDecoderRenderer();
|
decoderRenderer = new ConfigurableDecoderRenderer();
|
||||||
decoderRenderer.initializeWithFlags(drFlags);
|
decoderRenderer.initializeWithFlags(drFlags);
|
||||||
|
|
||||||
StreamConfiguration config = new StreamConfiguration.Builder()
|
StreamConfiguration config = new StreamConfiguration.Builder()
|
||||||
.setResolution(prefConfig.width, prefConfig.height)
|
.setResolution(prefConfig.width, prefConfig.height)
|
||||||
.setRefreshRate(prefConfig.fps)
|
.setRefreshRate(prefConfig.fps)
|
||||||
.setApp(app)
|
.setApp(new NvApp(appName, appId))
|
||||||
.setBitrate(prefConfig.bitrate * 1000)
|
.setBitrate(prefConfig.bitrate * 1000)
|
||||||
.setEnableSops(prefConfig.enableSops)
|
.setEnableSops(prefConfig.enableSops)
|
||||||
.enableAdaptiveResolution((decoderRenderer.getCapabilities() &
|
.enableAdaptiveResolution((decoderRenderer.getCapabilities() &
|
||||||
|
|||||||
@@ -237,8 +237,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
||||||
if (computer == null || computer.details == null ||
|
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||||
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -518,6 +517,12 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
if (details.equals(computer.details)) {
|
if (details.equals(computer.details)) {
|
||||||
pcGridAdapter.removeComputer(computer);
|
pcGridAdapter.removeComputer(computer);
|
||||||
pcGridAdapter.notifyDataSetChanged();
|
pcGridAdapter.notifyDataSetChanged();
|
||||||
|
|
||||||
|
if (pcGridAdapter.getCount() == 0) {
|
||||||
|
// Show the "Discovery in progress" view
|
||||||
|
noPcFoundLayout.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -587,6 +592,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
public ComputerDetails details;
|
public ComputerDetails details;
|
||||||
|
|
||||||
public ComputerObject(ComputerDetails details) {
|
public ComputerObject(ComputerDetails details) {
|
||||||
|
if (details == null) {
|
||||||
|
throw new IllegalArgumentException("details must not be null");
|
||||||
|
}
|
||||||
this.details = details;
|
this.details = details;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -178,7 +178,6 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
long nextFrameTime = System.currentTimeMillis();
|
long nextFrameTime = System.currentTimeMillis();
|
||||||
DecodeUnit du;
|
|
||||||
while (!isInterrupted())
|
while (!isInterrupted())
|
||||||
{
|
{
|
||||||
long diff = nextFrameTime - System.currentTimeMillis();
|
long diff = nextFrameTime - System.currentTimeMillis();
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package com.limelight.computers;
|
package com.limelight.computers;
|
||||||
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Socket;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -31,6 +33,8 @@ import org.xmlpull.v1.XmlPullParserException;
|
|||||||
public class ComputerManagerService extends Service {
|
public class ComputerManagerService extends Service {
|
||||||
private static final int POLLING_PERIOD_MS = 3000;
|
private static final int POLLING_PERIOD_MS = 3000;
|
||||||
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||||
|
private static final int FAST_POLL_TIMEOUT = 500;
|
||||||
|
private static final int OFFLINE_POLL_TRIES = 3;
|
||||||
|
|
||||||
private final ComputerManagerBinder binder = new ComputerManagerBinder();
|
private final ComputerManagerBinder binder = new ComputerManagerBinder();
|
||||||
|
|
||||||
@@ -64,8 +68,7 @@ public class ComputerManagerService extends Service {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Returns true if the details object was modified
|
// Returns true if the details object was modified
|
||||||
private boolean runPoll(ComputerDetails details, boolean newPc)
|
private boolean runPoll(ComputerDetails details, boolean newPc, int offlineCount) throws InterruptedException {
|
||||||
{
|
|
||||||
if (!getLocalDatabaseReference()) {
|
if (!getLocalDatabaseReference()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -73,12 +76,22 @@ public class ComputerManagerService extends Service {
|
|||||||
activePolls.incrementAndGet();
|
activePolls.incrementAndGet();
|
||||||
|
|
||||||
// Poll the machine
|
// Poll the machine
|
||||||
if (!doPollMachine(details)) {
|
try {
|
||||||
details.state = ComputerDetails.State.OFFLINE;
|
if (!pollComputer(details)) {
|
||||||
details.reachability = ComputerDetails.Reachability.OFFLINE;
|
if (!newPc && offlineCount < OFFLINE_POLL_TRIES) {
|
||||||
}
|
// Return without calling the listener
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
activePolls.decrementAndGet();
|
details.state = ComputerDetails.State.OFFLINE;
|
||||||
|
details.reachability = ComputerDetails.Reachability.OFFLINE;
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
releaseLocalDatabaseReference();
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
activePolls.decrementAndGet();
|
||||||
|
}
|
||||||
|
|
||||||
// If it's online, update our persistent state
|
// If it's online, update our persistent state
|
||||||
if (details.state == ComputerDetails.State.ONLINE) {
|
if (details.state == ComputerDetails.State.ONLINE) {
|
||||||
@@ -88,7 +101,7 @@ public class ComputerManagerService extends Service {
|
|||||||
if (dbManager.getComputerByName(details.name) == null) {
|
if (dbManager.getComputerByName(details.name) == null) {
|
||||||
// It's gone
|
// It's gone
|
||||||
releaseLocalDatabaseReference();
|
releaseLocalDatabaseReference();
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,13 +121,21 @@ public class ComputerManagerService extends Service {
|
|||||||
Thread t = new Thread() {
|
Thread t = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
while (!isInterrupted() && pollingActive) {
|
|
||||||
// Check if this poll has modified the details
|
|
||||||
runPoll(details, false);
|
|
||||||
|
|
||||||
// Wait until the next polling interval
|
int offlineCount = 0;
|
||||||
|
while (!isInterrupted() && pollingActive) {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(POLLING_PERIOD_MS);
|
// Check if this poll has modified the details
|
||||||
|
if (!runPoll(details, false, offlineCount)) {
|
||||||
|
LimeLog.warning(details.name + " is offline (try " + offlineCount + ")");
|
||||||
|
offlineCount++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
offlineCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until the next polling interval
|
||||||
|
Thread.sleep(POLLING_PERIOD_MS / ((offlineCount > 0) ? 2 : 1));
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -285,7 +306,11 @@ public class ComputerManagerService extends Service {
|
|||||||
fakeDetails.remoteIp = addr;
|
fakeDetails.remoteIp = addr;
|
||||||
|
|
||||||
// Block while we try to fill the details
|
// Block while we try to fill the details
|
||||||
runPoll(fakeDetails, true);
|
try {
|
||||||
|
runPoll(fakeDetails, true, 0);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// If the machine is reachable, it was successful
|
// If the machine is reachable, it was successful
|
||||||
if (fakeDetails.state == ComputerDetails.State.ONLINE) {
|
if (fakeDetails.state == ComputerDetails.State.ONLINE) {
|
||||||
@@ -361,14 +386,91 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean pollComputer(ComputerDetails details, boolean localFirst) {
|
// Just try to establish a TCP connection to speculatively detect a running
|
||||||
|
// GFE server
|
||||||
|
private boolean fastPollIp(InetAddress addr) {
|
||||||
|
Socket s = new Socket();
|
||||||
|
try {
|
||||||
|
s.connect(new InetSocketAddress(addr, NvHTTP.PORT), FAST_POLL_TIMEOUT);
|
||||||
|
s.close();
|
||||||
|
return true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startFastPollThread(final InetAddress addr, final boolean[] info) {
|
||||||
|
Thread t = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
boolean pollRes = fastPollIp(addr);
|
||||||
|
|
||||||
|
synchronized (info) {
|
||||||
|
info[0] = true; // Done
|
||||||
|
info[1] = pollRes; // Polling result
|
||||||
|
|
||||||
|
info.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
t.setName("Fast Poll - "+addr.getHostAddress());
|
||||||
|
t.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ComputerDetails.Reachability fastPollPc(final InetAddress local, final InetAddress remote) throws InterruptedException {
|
||||||
|
final boolean[] remoteInfo = new boolean[2];
|
||||||
|
final boolean[] localInfo = new boolean[2];
|
||||||
|
|
||||||
|
startFastPollThread(local, localInfo);
|
||||||
|
startFastPollThread(remote, remoteInfo);
|
||||||
|
|
||||||
|
// Check local first
|
||||||
|
synchronized (localInfo) {
|
||||||
|
while (!localInfo[0]) {
|
||||||
|
localInfo.wait(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localInfo[1]) {
|
||||||
|
return ComputerDetails.Reachability.LOCAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now remote
|
||||||
|
synchronized (remoteInfo) {
|
||||||
|
while (!remoteInfo[0]) {
|
||||||
|
remoteInfo.wait(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteInfo[1]) {
|
||||||
|
return ComputerDetails.Reachability.REMOTE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ComputerDetails.Reachability.OFFLINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
|
||||||
ComputerDetails polledDetails;
|
ComputerDetails polledDetails;
|
||||||
|
ComputerDetails.Reachability reachability;
|
||||||
|
|
||||||
// If the local address is routable across the Internet,
|
// If the local address is routable across the Internet,
|
||||||
// always consider this PC remote to be conservative
|
// always consider this PC remote to be conservative
|
||||||
if (details.localIp.equals(details.remoteIp)) {
|
if (details.localIp.equals(details.remoteIp)) {
|
||||||
localFirst = false;
|
reachability = ComputerDetails.Reachability.REMOTE;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// Do a TCP-level connection to the HTTP server to see if it's listening
|
||||||
|
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localIp+", "+details.remoteIp+")");
|
||||||
|
reachability = fastPollPc(details.localIp, details.remoteIp);
|
||||||
|
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no connection could be established to either IP address, there's nothing we can do
|
||||||
|
if (reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
|
||||||
|
|
||||||
if (localFirst) {
|
if (localFirst) {
|
||||||
polledDetails = tryPollIp(details, details.localIp);
|
polledDetails = tryPollIp(details, details.localIp);
|
||||||
@@ -402,24 +504,19 @@ public class ComputerManagerService extends Service {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save the old MAC address
|
||||||
|
String savedMacAddress = details.macAddress;
|
||||||
|
|
||||||
// If we got here, it's reachable
|
// If we got here, it's reachable
|
||||||
details.update(polledDetails);
|
details.update(polledDetails);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean doPollMachine(ComputerDetails details) {
|
// If the new MAC address is empty, restore the old one (workaround for GFE bug)
|
||||||
if (details.reachability == ComputerDetails.Reachability.UNKNOWN ||
|
if (details.macAddress.equals("00:00:00:00:00:00") && savedMacAddress != null) {
|
||||||
details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
LimeLog.info("MAC address was empty; using existing value: "+savedMacAddress);
|
||||||
// Always try local first to avoid potential UDP issues when
|
details.macAddress = savedMacAddress;
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -529,9 +626,19 @@ public class ComputerManagerService extends Service {
|
|||||||
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
|
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
|
||||||
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
|
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
|
||||||
// Open the cache file
|
// Open the cache file
|
||||||
FileOutputStream cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
|
OutputStream cacheOut = null;
|
||||||
CacheHelper.writeStringToOutputStream(cacheOut, appList);
|
try {
|
||||||
cacheOut.close();
|
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
|
||||||
|
CacheHelper.writeStringToOutputStream(cacheOut, appList);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
if (cacheOut != null) {
|
||||||
|
cacheOut.close();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {}
|
||||||
|
}
|
||||||
|
|
||||||
// Update the computer
|
// Update the computer
|
||||||
computer.rawAppList = appList;
|
computer.rawAppList = appList;
|
||||||
|
|||||||
@@ -1,110 +1,83 @@
|
|||||||
package com.limelight.grid;
|
package com.limelight.grid;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.app.Activity;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.koushikdutta.async.future.FutureCallback;
|
|
||||||
import com.koushikdutta.ion.Ion;
|
|
||||||
import com.limelight.AppView;
|
import com.limelight.AppView;
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.grid.assets.CachedAppAssetLoader;
|
||||||
|
import com.limelight.grid.assets.DiskAssetLoader;
|
||||||
|
import com.limelight.grid.assets.MemoryAssetLoader;
|
||||||
|
import com.limelight.grid.assets.NetworkAssetLoader;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
|
||||||
import com.limelight.utils.CacheHelper;
|
|
||||||
|
|
||||||
import java.io.FileOutputStream;
|
import java.lang.ref.WeakReference;
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.Socket;
|
|
||||||
import java.security.KeyManagementException;
|
import java.security.KeyManagementException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.Principal;
|
import java.util.Collection;
|
||||||
import java.security.PrivateKey;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.Iterator;
|
||||||
import java.util.UUID;
|
import java.util.Map;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||||
|
private final Activity activity;
|
||||||
|
|
||||||
private final ComputerDetails computer;
|
private static final int ART_WIDTH_PX = 300;
|
||||||
private final String uniqueId;
|
private static final int SMALL_WIDTH_DP = 100;
|
||||||
private final LimelightCryptoProvider cryptoProvider;
|
private static final int LARGE_WIDTH_DP = 150;
|
||||||
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 {
|
private final CachedAppAssetLoader loader;
|
||||||
super(context, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
|
private final ConcurrentHashMap<WeakReference<ImageView>, CachedAppAssetLoader.LoaderTuple> loadingTuples = new ConcurrentHashMap<>();
|
||||||
|
private final ConcurrentHashMap<Object, CachedAppAssetLoader.LoaderTuple> backgroundLoadingTuples = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
this.computer = computer;
|
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws KeyManagementException, NoSuchAlgorithmException {
|
||||||
this.uniqueId = uniqueId;
|
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
|
||||||
|
|
||||||
cryptoProvider = PlatformBinding.getCryptoProvider(context);
|
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
|
||||||
|
int dp;
|
||||||
|
|
||||||
sslContext = SSLContext.getInstance("SSL");
|
if (small) {
|
||||||
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom());
|
dp = SMALL_WIDTH_DP;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dp = LARGE_WIDTH_DP;
|
||||||
|
}
|
||||||
|
|
||||||
|
double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160));
|
||||||
|
if (scalingDivisor < 1.0) {
|
||||||
|
// We don't want to make them bigger before draw-time
|
||||||
|
scalingDivisor = 1.0;
|
||||||
|
}
|
||||||
|
LimeLog.info("Art scaling divisor: " + scalingDivisor);
|
||||||
|
|
||||||
|
this.activity = activity;
|
||||||
|
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
||||||
|
new NetworkAssetLoader(context, uniqueId),
|
||||||
|
new MemoryAssetLoader(), new DiskAssetLoader(context.getCacheDir()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private final TrustManager[] trustAllCerts = new TrustManager[] {
|
private static void cancelTuples(ConcurrentHashMap<?, CachedAppAssetLoader.LoaderTuple> map) {
|
||||||
new X509TrustManager() {
|
Collection<CachedAppAssetLoader.LoaderTuple> tuples = map.values();
|
||||||
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[] {
|
for (CachedAppAssetLoader.LoaderTuple tuple : tuples) {
|
||||||
new X509KeyManager() {
|
tuple.cancel();
|
||||||
public String chooseClientAlias(String[] keyTypes,
|
}
|
||||||
Principal[] issuers, Socket socket) {
|
|
||||||
return "Limelight-RSA";
|
|
||||||
}
|
|
||||||
|
|
||||||
public String chooseServerAlias(String keyType, Principal[] issuers,
|
map.clear();
|
||||||
Socket socket) {
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public X509Certificate[] getCertificateChain(String alias) {
|
public void cancelQueuedOperations() {
|
||||||
return new X509Certificate[] {cryptoProvider.getClientCertificate()};
|
cancelTuples(loadingTuples);
|
||||||
}
|
cancelTuples(backgroundLoadingTuples);
|
||||||
|
|
||||||
public String[] getClientAliases(String keyType, Principal[] issuers) {
|
loader.freeCacheMemory();
|
||||||
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() {
|
private void sortList() {
|
||||||
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
||||||
@@ -115,62 +88,117 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private InetAddress getCurrentAddress() {
|
|
||||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
|
||||||
return computer.localIp;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return computer.remoteIp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addApp(AppView.AppObject app) {
|
public void addApp(AppView.AppObject app) {
|
||||||
|
// Queue a request to fetch this bitmap in the background
|
||||||
|
Object tupleKey = new Object();
|
||||||
|
CachedAppAssetLoader.LoaderTuple tuple =
|
||||||
|
loader.loadBitmapWithContextInBackground(app.app, tupleKey, backgroundLoadListener);
|
||||||
|
if (tuple != null) {
|
||||||
|
backgroundLoadingTuples.put(tupleKey, tuple);
|
||||||
|
}
|
||||||
|
|
||||||
itemList.add(app);
|
itemList.add(app);
|
||||||
sortList();
|
sortList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void abortPendingRequests() {
|
public void removeApp(AppView.AppObject app) {
|
||||||
HashMap<ImageView, Future> tempMap;
|
itemList.remove(app);
|
||||||
|
}
|
||||||
|
|
||||||
synchronized (pendingRequests) {
|
private final CachedAppAssetLoader.LoadListener imageViewLoadListener = new CachedAppAssetLoader.LoadListener() {
|
||||||
// Copy the pending requests under a lock
|
@Override
|
||||||
tempMap = new HashMap<ImageView, Future>(pendingRequests);
|
public void notifyLongLoad(Object object) {
|
||||||
}
|
final WeakReference<ImageView> viewRef = (WeakReference<ImageView>) object;
|
||||||
|
|
||||||
for (Future f : tempMap.values()) {
|
// If the view isn't there anymore, don't bother scheduling on the UI thread
|
||||||
if (!f.isCancelled() && !f.isDone()) {
|
if (viewRef.get() == null) {
|
||||||
f.cancel(true);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
ImageView view = viewRef.get();
|
||||||
|
if (view != null) {
|
||||||
|
view.setImageResource(R.drawable.image_loading);
|
||||||
|
fadeInImage(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized (pendingRequests) {
|
@Override
|
||||||
// Remove cancelled requests
|
public void notifyLoadComplete(Object object, final Bitmap bitmap) {
|
||||||
for (ImageView v : tempMap.keySet()) {
|
final WeakReference<ImageView> viewRef = (WeakReference<ImageView>) object;
|
||||||
pendingRequests.remove(v);
|
|
||||||
|
loadingTuples.remove(viewRef);
|
||||||
|
|
||||||
|
// Just leave the loading icon in place
|
||||||
|
if (bitmap == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the view isn't there anymore, don't bother scheduling on the UI thread
|
||||||
|
if (viewRef.get() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
ImageView view = viewRef.get();
|
||||||
|
if (view != null) {
|
||||||
|
view.setImageBitmap(bitmap);
|
||||||
|
fadeInImage(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final CachedAppAssetLoader.LoadListener backgroundLoadListener = new CachedAppAssetLoader.LoadListener() {
|
||||||
|
@Override
|
||||||
|
public void notifyLongLoad(Object object) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyLoadComplete(Object object, final Bitmap bitmap) {
|
||||||
|
backgroundLoadingTuples.remove(object);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private void reapLoaderTuples(ImageView view) {
|
||||||
|
// Poor HashMap doesn't deserve this...
|
||||||
|
Iterator<Map.Entry<WeakReference<ImageView>, CachedAppAssetLoader.LoaderTuple>> i = loadingTuples.entrySet().iterator();
|
||||||
|
while (i.hasNext()) {
|
||||||
|
Map.Entry<WeakReference<ImageView>, CachedAppAssetLoader.LoaderTuple> entry = i.next();
|
||||||
|
ImageView imageView = entry.getKey().get();
|
||||||
|
|
||||||
|
// Remove tuples that refer to this view or no view
|
||||||
|
if (imageView == null || imageView == view) {
|
||||||
|
// FIXME: There's a small chance that this can race if we've already gone down
|
||||||
|
// the path to notification but haven't been notified yet
|
||||||
|
entry.getValue().cancel();
|
||||||
|
|
||||||
|
// Remove it from the tuple list
|
||||||
|
i.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) {
|
||||||
|
// Cancel pending loads on this image view
|
||||||
|
reapLoaderTuples(imgView);
|
||||||
|
|
||||||
// Clear existing contents of the image view
|
// Clear existing contents of the image view
|
||||||
imgView.setAlpha(0.0f);
|
imgView.setAlpha(0.0f);
|
||||||
|
|
||||||
// Check the on-disk cache
|
// Start loading the bitmap
|
||||||
new ImageCacheRequest(imgView, obj.app.getAppId()).execute();
|
WeakReference<ImageView> viewRef = new WeakReference<>(imgView);
|
||||||
|
CachedAppAssetLoader.LoaderTuple tuple = loader.loadBitmapWithContext(obj.app, viewRef, imageViewLoadListener);
|
||||||
|
if (tuple != null) {
|
||||||
|
// The load was issued asynchronously
|
||||||
|
loadingTuples.put(viewRef, tuple);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,95 +223,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ImageCacheRequest extends AsyncTask<Void, Void, Bitmap> {
|
private static void fadeInImage(ImageView view) {
|
||||||
private final ImageView view;
|
view.animate().alpha(1.0f).setDuration(100).start();
|
||||||
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,162 @@
|
|||||||
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class CachedAppAssetLoader {
|
||||||
|
private final ComputerDetails computer;
|
||||||
|
private final double scalingDivider;
|
||||||
|
private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(8, 8, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>());
|
||||||
|
private final ThreadPoolExecutor backgroundExecutor = new ThreadPoolExecutor(2, 2, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>());
|
||||||
|
private final NetworkAssetLoader networkLoader;
|
||||||
|
private final MemoryAssetLoader memoryLoader;
|
||||||
|
private final DiskAssetLoader diskLoader;
|
||||||
|
|
||||||
|
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
||||||
|
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
||||||
|
DiskAssetLoader diskLoader) {
|
||||||
|
this.computer = computer;
|
||||||
|
this.scalingDivider = scalingDivider;
|
||||||
|
|
||||||
|
this.networkLoader = networkLoader;
|
||||||
|
this.memoryLoader = memoryLoader;
|
||||||
|
this.diskLoader = diskLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void freeCacheMemory() {
|
||||||
|
memoryLoader.clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Runnable createLoaderRunnable(final LoaderTuple tuple, final Object context, final LoadListener listener) {
|
||||||
|
return new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Abort if we've been cancelled
|
||||||
|
if (tuple.cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||||
|
if (bmp == null) {
|
||||||
|
// Notify the listener that this may take a while
|
||||||
|
listener.notifyLongLoad(context);
|
||||||
|
|
||||||
|
// Try 5 times maximum
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
// Check again whether we've been cancelled
|
||||||
|
if (tuple.cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStream in = networkLoader.getBitmapStream(tuple);
|
||||||
|
if (in != null) {
|
||||||
|
// Write the stream straight to disk
|
||||||
|
diskLoader.populateCacheWithStream(tuple, in);
|
||||||
|
|
||||||
|
// Read it back scaled
|
||||||
|
bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||||
|
if (bmp != null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait 1 second with a bit of fuzz
|
||||||
|
try {
|
||||||
|
Thread.sleep((int) (1000 + (Math.random() * 500)));
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bmp != null) {
|
||||||
|
// Populate the memory cache
|
||||||
|
memoryLoader.populateCache(tuple, bmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check one last time whether we've been cancelled
|
||||||
|
synchronized (tuple) {
|
||||||
|
if (tuple.cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tuple.notified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the load complete callback (possible with a null bitmap)
|
||||||
|
listener.notifyLoadComplete(context, bmp);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoaderTuple loadBitmapWithContext(NvApp app, Object context, LoadListener listener) {
|
||||||
|
return loadBitmapWithContext(app, context, listener, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoaderTuple loadBitmapWithContextInBackground(NvApp app, Object context, LoadListener listener) {
|
||||||
|
return loadBitmapWithContext(app, context, listener, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoaderTuple loadBitmapWithContext(NvApp app, Object context, LoadListener listener, boolean background) {
|
||||||
|
LoaderTuple tuple = new LoaderTuple(computer, app);
|
||||||
|
|
||||||
|
// First, try the memory cache in the current context
|
||||||
|
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
|
||||||
|
if (bmp != null) {
|
||||||
|
// The caller never sees our tuple in this case
|
||||||
|
listener.notifyLoadComplete(context, bmp);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's not in memory, throw this in our executor
|
||||||
|
if (background) {
|
||||||
|
backgroundExecutor.execute(createLoaderRunnable(tuple, context, listener));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
foregroundExecutor.execute(createLoaderRunnable(tuple, context, listener));
|
||||||
|
}
|
||||||
|
return tuple;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LoaderTuple {
|
||||||
|
public final ComputerDetails computer;
|
||||||
|
public final NvApp app;
|
||||||
|
|
||||||
|
public boolean notified;
|
||||||
|
public boolean cancelled;
|
||||||
|
|
||||||
|
public LoaderTuple(ComputerDetails computer, NvApp app) {
|
||||||
|
this.computer = computer;
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean cancel() {
|
||||||
|
synchronized (this) {
|
||||||
|
cancelled = true;
|
||||||
|
return !notified;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "("+computer.uuid+", "+app.getAppId()+")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface LoadListener {
|
||||||
|
// Notifies that the load didn't hit any cache and is about to be dispatched
|
||||||
|
// over the network
|
||||||
|
public void notifyLongLoad(Object context);
|
||||||
|
|
||||||
|
// Bitmap may be null if the load failed
|
||||||
|
public void notifyLoadComplete(Object context, Bitmap bitmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.utils.CacheHelper;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public class DiskAssetLoader {
|
||||||
|
private final File cacheDir;
|
||||||
|
|
||||||
|
public DiskAssetLoader(File cacheDir) {
|
||||||
|
this.cacheDir = cacheDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
||||||
|
InputStream in = null;
|
||||||
|
Bitmap bmp = null;
|
||||||
|
try {
|
||||||
|
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
|
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||||
|
options.inSampleSize = sampleSize;
|
||||||
|
bmp = BitmapFactory.decodeStream(in, null, options);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
if (in != null) {
|
||||||
|
try {
|
||||||
|
in.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bmp != null) {
|
||||||
|
LimeLog.info("Disk cache hit for tuple: "+tuple);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
|
||||||
|
OutputStream out = null;
|
||||||
|
try {
|
||||||
|
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
|
CacheHelper.writeInputStreamToOutputStream(input, out);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
if (out != null) {
|
||||||
|
try {
|
||||||
|
out.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.util.LruCache;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
|
public class MemoryAssetLoader {
|
||||||
|
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
|
||||||
|
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 12) {
|
||||||
|
@Override
|
||||||
|
protected int sizeOf(String key, Bitmap bitmap) {
|
||||||
|
// Sizeof returns kilobytes
|
||||||
|
return bitmap.getByteCount() / 1024;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
|
return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
|
Bitmap bmp = memoryCache.get(constructKey(tuple));
|
||||||
|
if (bmp != null) {
|
||||||
|
LimeLog.info("Memory cache hit for tuple: "+tuple);
|
||||||
|
}
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
|
||||||
|
memoryCache.put(constructKey(tuple), bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearCache() {
|
||||||
|
memoryCache.evictAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
public class NetworkAssetLoader {
|
||||||
|
private final Context context;
|
||||||
|
private final String uniqueId;
|
||||||
|
|
||||||
|
public NetworkAssetLoader(Context context, String uniqueId) {
|
||||||
|
this.context = context;
|
||||||
|
this.uniqueId = uniqueId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
|
NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
|
||||||
|
|
||||||
|
InputStream in = null;
|
||||||
|
try {
|
||||||
|
in = http.getBoxArt(tuple.app);
|
||||||
|
} catch (IOException e) {}
|
||||||
|
|
||||||
|
if (in != null) {
|
||||||
|
LimeLog.info("Network asset load complete: " + tuple);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.info("Network asset load failed: " + tuple);
|
||||||
|
}
|
||||||
|
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InetAddress getCurrentAddress(ComputerDetails computer) {
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
|
return computer.localIp;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return computer.remoteIp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.limelight.utils;
|
package com.limelight.utils;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.BufferedOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
@@ -28,12 +30,21 @@ public class CacheHelper {
|
|||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static FileInputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
|
public static InputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
|
||||||
return new FileInputStream(openPath(false, root, path));
|
return new BufferedInputStream(new FileInputStream(openPath(false, root, path)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static FileOutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
|
public static OutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
|
||||||
return new FileOutputStream(openPath(true, root, path));
|
return new BufferedOutputStream(new FileOutputStream(openPath(true, root, path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeInputStreamToOutputStream(InputStream in, OutputStream out) throws IOException {
|
||||||
|
byte[] buf = new byte[4096];
|
||||||
|
int bytesRead;
|
||||||
|
|
||||||
|
while ((bytesRead = in.read(buf)) != -1) {
|
||||||
|
out.write(buf, 0, bytesRead);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String readInputStreamToString(InputStream in) throws IOException {
|
public static String readInputStreamToString(InputStream in) throws IOException {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@@ -10,4 +10,4 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="center" />
|
android:layout_gravity="center" />
|
||||||
|
|
||||||
</FrameLayout>
|
</merge>
|
||||||
+1
-1
@@ -4,7 +4,7 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:1.0.1'
|
classpath 'com.android.tools.build:gradle:1.1.0'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user