mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2025-07-20 11:33:06 +00:00
Fix huge performance issues when dealing with large app lists
This commit is contained in:
parent
e222f2f6c3
commit
ee58071ff1
@ -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;
|
||||||
@ -446,10 +447,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);
|
||||||
|
@ -1,112 +1,38 @@
|
|||||||
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.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.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.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.concurrent.ConcurrentHashMap;
|
||||||
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> {
|
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||||
|
private final Activity activity;
|
||||||
|
|
||||||
private final ComputerDetails computer;
|
private final CachedAppAssetLoader loader;
|
||||||
private final String uniqueId;
|
private final ConcurrentHashMap<ImageView, CachedAppAssetLoader.LoaderTuple> loadingTuples = new ConcurrentHashMap<>();
|
||||||
private final LimelightCryptoProvider cryptoProvider;
|
|
||||||
private final SSLContext sslContext;
|
|
||||||
private final HashMap<ImageView, Future> pendingIonRequests = new HashMap<ImageView, Future>();
|
|
||||||
private final HashMap<ImageView, ImageCacheRequest> pendingCacheRequests = new HashMap<ImageView, ImageCacheRequest>();
|
|
||||||
|
|
||||||
public AppGridAdapter(Context context, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException {
|
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws KeyManagementException, NoSuchAlgorithmException {
|
||||||
super(context, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
|
super(activity, 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.activity = activity;
|
||||||
this.uniqueId = uniqueId;
|
this.loader = new CachedAppAssetLoader(computer, uniqueId, new NetworkAssetLoader(context),
|
||||||
|
new MemoryAssetLoader(), new DiskAssetLoader(context.getCacheDir()));
|
||||||
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() {
|
private void sortList() {
|
||||||
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
||||||
@Override
|
@Override
|
||||||
@ -116,15 +42,6 @@ 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) {
|
||||||
itemList.add(app);
|
itemList.add(app);
|
||||||
sortList();
|
sortList();
|
||||||
@ -134,46 +51,59 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
itemList.remove(app);
|
itemList.remove(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Handle pruning of bitmap cache
|
private final CachedAppAssetLoader.LoadListener loadListener = new CachedAppAssetLoader.LoadListener() {
|
||||||
private void populateBitmapCache(UUID uuid, int appId, Bitmap bitmap) {
|
@Override
|
||||||
try {
|
public void notifyLongLoad(Object object) {
|
||||||
// PNG ignores quality setting
|
final ImageView view = (ImageView) object;
|
||||||
FileOutputStream out = CacheHelper.openCacheFileForOutput(context.getCacheDir(), "boxart", uuid.toString(), appId+".png");
|
|
||||||
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
|
activity.runOnUiThread(new Runnable() {
|
||||||
out.close();
|
@Override
|
||||||
} catch (IOException e) {
|
public void run() {
|
||||||
e.printStackTrace();
|
view.setImageResource(R.drawable.image_loading);
|
||||||
}
|
fadeInImage(view);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyLoadComplete(Object object, final Bitmap bitmap) {
|
||||||
|
final ImageView view = (ImageView) object;
|
||||||
|
|
||||||
|
loadingTuples.remove(view);
|
||||||
|
|
||||||
|
// Just leave the loading icon in place
|
||||||
|
if (bitmap == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
view.setImageBitmap(bitmap);
|
||||||
|
fadeInImage(view);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) {
|
public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) {
|
||||||
// Cancel any pending cache requests for this view
|
// Cancel pending loads on this image view
|
||||||
synchronized (pendingCacheRequests) {
|
CachedAppAssetLoader.LoaderTuple tuple = loadingTuples.remove(imgView);
|
||||||
ImageCacheRequest req = pendingCacheRequests.remove(imgView);
|
if (tuple != null) {
|
||||||
if (req != null) {
|
// FIXME: There's a small chance that this can race if we've already gone down
|
||||||
req.cancel(false);
|
// the path to notification but haven't been notified yet
|
||||||
}
|
tuple.cancel();
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel any pending Ion requests for this view
|
|
||||||
synchronized (pendingIonRequests) {
|
|
||||||
Future f = pendingIonRequests.remove(imgView);
|
|
||||||
if (f != null && !f.isCancelled() && !f.isDone()) {
|
|
||||||
f.cancel(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
ImageCacheRequest req = new ImageCacheRequest(imgView, obj.app.getAppId());
|
tuple = loader.loadBitmapWithContext(obj.app, imgView, loadListener);
|
||||||
synchronized (pendingCacheRequests) {
|
if (tuple != null) {
|
||||||
pendingCacheRequests.put(imgView, req);
|
// The load was issued asynchronously
|
||||||
|
loadingTuples.put(imgView, tuple);
|
||||||
}
|
}
|
||||||
req.execute();
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,105 +128,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) {
|
|
||||||
// Check if the cache request is still live
|
|
||||||
synchronized (pendingCacheRequests) {
|
|
||||||
if (pendingCacheRequests.remove(view) == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (pendingIonRequests) {
|
|
||||||
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 (pendingIonRequests) {
|
|
||||||
// Don't set this image if the request was cancelled
|
|
||||||
if (pendingIonRequests.remove(view) == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
pendingIonRequests.put(view, f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,156 @@
|
|||||||
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class CachedAppAssetLoader {
|
||||||
|
private final ComputerDetails computer;
|
||||||
|
private final String uniqueId;
|
||||||
|
private final ThreadPoolExecutor executor = new ThreadPoolExecutor(8, 8, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>());
|
||||||
|
private final NetworkLoader networkLoader;
|
||||||
|
private final CachedLoader memoryLoader;
|
||||||
|
private final CachedLoader diskLoader;
|
||||||
|
|
||||||
|
public CachedAppAssetLoader(ComputerDetails computer, String uniqueId, NetworkLoader networkLoader, CachedLoader memoryLoader, CachedLoader diskLoader) {
|
||||||
|
this.computer = computer;
|
||||||
|
this.uniqueId = uniqueId;
|
||||||
|
|
||||||
|
this.networkLoader = networkLoader;
|
||||||
|
this.memoryLoader = memoryLoader;
|
||||||
|
this.diskLoader = diskLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
bmp = networkLoader.loadBitmap(tuple);
|
||||||
|
if (bmp != null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait 1 second with a bit of fuzz
|
||||||
|
try {
|
||||||
|
Thread.sleep((int) (1000 + (Math.random()*500)));
|
||||||
|
} catch (InterruptedException e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bmp != null) {
|
||||||
|
// Populate the disk cache
|
||||||
|
diskLoader.populateCache(tuple, bmp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
LoaderTuple tuple = new LoaderTuple(computer, uniqueId, app);
|
||||||
|
|
||||||
|
// First, try the memory cache in the current context
|
||||||
|
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
|
||||||
|
if (bmp != null) {
|
||||||
|
synchronized (tuple) {
|
||||||
|
if (tuple.cancelled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tuple.notified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listener.notifyLoadComplete(context, bmp);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's not in memory, throw this in our executor
|
||||||
|
executor.execute(createLoaderRunnable(tuple, context, listener));
|
||||||
|
return tuple;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LoaderTuple {
|
||||||
|
public final ComputerDetails computer;
|
||||||
|
public final String uniqueId;
|
||||||
|
public final NvApp app;
|
||||||
|
|
||||||
|
public boolean notified;
|
||||||
|
public boolean cancelled;
|
||||||
|
|
||||||
|
public LoaderTuple(ComputerDetails computer, String uniqueId, NvApp app) {
|
||||||
|
this.computer = computer;
|
||||||
|
this.uniqueId = uniqueId;
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean cancel() {
|
||||||
|
synchronized (this) {
|
||||||
|
cancelled = true;
|
||||||
|
return !notified;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "("+computer.uuid+", "+app.getAppId()+")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface NetworkLoader {
|
||||||
|
public Bitmap loadBitmap(LoaderTuple tuple);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface CachedLoader {
|
||||||
|
public Bitmap loadBitmapFromCache(LoaderTuple tuple);
|
||||||
|
public void populateCache(LoaderTuple tuple, Bitmap bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,56 @@
|
|||||||
|
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.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public class DiskAssetLoader implements CachedAppAssetLoader.CachedLoader {
|
||||||
|
private final File cacheDir;
|
||||||
|
|
||||||
|
public DiskAssetLoader(File cacheDir) {
|
||||||
|
this.cacheDir = cacheDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
|
InputStream in = null;
|
||||||
|
Bitmap bmp = null;
|
||||||
|
try {
|
||||||
|
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
|
bmp = BitmapFactory.decodeStream(in);
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
|
||||||
|
try {
|
||||||
|
// PNG ignores quality setting
|
||||||
|
FileOutputStream out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
|
||||||
|
out.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.util.LruCache;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
|
public class MemoryAssetLoader implements CachedAppAssetLoader.CachedLoader {
|
||||||
|
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
|
||||||
|
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 4) {
|
||||||
|
@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();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
|
||||||
|
memoryCache.put(constructKey(tuple), bitmap);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,120 @@
|
|||||||
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
|
||||||
|
import com.koushikdutta.ion.Ion;
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||||
|
|
||||||
|
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.security.cert.X509Certificate;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
public class NetworkAssetLoader implements CachedAppAssetLoader.NetworkLoader {
|
||||||
|
private final Context context;
|
||||||
|
private final LimelightCryptoProvider cryptoProvider;
|
||||||
|
private final SSLContext sslContext;
|
||||||
|
|
||||||
|
public NetworkAssetLoader(Context context) throws NoSuchAlgorithmException, KeyManagementException {
|
||||||
|
this.context = context;
|
||||||
|
|
||||||
|
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
|
||||||
|
private final HostnameVerifier hv = new HostnameVerifier() {
|
||||||
|
public boolean verify(String hostname, SSLSession session) { return true; }
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Bitmap loadBitmap(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
Bitmap bmp = Ion.with(context)
|
||||||
|
.load("https://" + getCurrentAddress(tuple.computer).getHostAddress() + ":47984/appasset?uniqueid=" +
|
||||||
|
tuple.uniqueId + "&appid=" + tuple.app.getAppId() + "&AssetType=2&AssetIdx=0")
|
||||||
|
.asBitmap()
|
||||||
|
.tryGet();
|
||||||
|
if (bmp != null) {
|
||||||
|
LimeLog.info("Network asset load complete: " + tuple);
|
||||||
|
|
||||||
|
// Scale the bitmap to half size
|
||||||
|
bmp = Bitmap.createScaledBitmap(bmp, bmp.getWidth() / 2, bmp.getHeight() / 2, true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.info("Network asset load failed: " + tuple);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InetAddress getCurrentAddress(ComputerDetails computer) {
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
|
return computer.localIp;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return computer.remoteIp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user