mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-04-07 00:16:15 +00:00
This commit is contained in:
@@ -1,117 +1,68 @@
|
||||
package com.limelight.grid;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.app.Activity;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.AsyncTask;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.koushikdutta.async.future.FutureCallback;
|
||||
import com.koushikdutta.ion.ImageViewBitmapInfo;
|
||||
import com.koushikdutta.ion.Ion;
|
||||
import com.limelight.AppView;
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.R;
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.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.LimelightCryptoProvider;
|
||||
import com.limelight.utils.CacheHelper;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Principal;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
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;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
private static final int ART_WIDTH_PX = 300;
|
||||
private static final int SMALL_WIDTH_DP = 100;
|
||||
private static final int LARGE_WIDTH_DP = 150;
|
||||
|
||||
private ComputerDetails computer;
|
||||
private String uniqueId;
|
||||
private LimelightCryptoProvider cryptoProvider;
|
||||
private SSLContext sslContext;
|
||||
private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>();
|
||||
private final CachedAppAssetLoader loader;
|
||||
|
||||
public AppGridAdapter(Context context, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException {
|
||||
super(context, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
|
||||
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) {
|
||||
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.uniqueId = uniqueId;
|
||||
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
|
||||
int dp;
|
||||
|
||||
cryptoProvider = PlatformBinding.getCryptoProvider(context);
|
||||
if (small) {
|
||||
dp = SMALL_WIDTH_DP;
|
||||
}
|
||||
else {
|
||||
dp = LARGE_WIDTH_DP;
|
||||
}
|
||||
|
||||
sslContext = SSLContext.getInstance("SSL");
|
||||
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom());
|
||||
double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160.0));
|
||||
if (scalingDivisor < 1.0) {
|
||||
// We don't want to make them bigger before draw-time
|
||||
scalingDivisor = 1.0;
|
||||
}
|
||||
LimeLog.info("Art scaling divisor: " + scalingDivisor);
|
||||
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inSampleSize = (int) scalingDivisor;
|
||||
|
||||
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
||||
new NetworkAssetLoader(context, uniqueId),
|
||||
new MemoryAssetLoader(),
|
||||
new DiskAssetLoader(context.getCacheDir()),
|
||||
BitmapFactory.decodeResource(activity.getResources(),
|
||||
R.drawable.image_loading, options));
|
||||
}
|
||||
|
||||
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) {}
|
||||
}};
|
||||
|
||||
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; }
|
||||
};
|
||||
public void cancelQueuedOperations() {
|
||||
loader.cancelForegroundLoads();
|
||||
loader.cancelBackgroundLoads();
|
||||
loader.freeCacheMemory();
|
||||
}
|
||||
|
||||
private void sortList() {
|
||||
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
||||
@@ -122,62 +73,22 @@ 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) {
|
||||
// Queue a request to fetch this bitmap into cache
|
||||
loader.queueCacheLoad(app.app);
|
||||
|
||||
// Add the app to our sorted list
|
||||
itemList.add(app);
|
||||
sortList();
|
||||
}
|
||||
|
||||
public void abortPendingRequests() {
|
||||
HashMap<ImageView, Future> tempMap;
|
||||
|
||||
synchronized (pendingRequests) {
|
||||
// Copy the pending requests under a lock
|
||||
tempMap = new HashMap<ImageView, Future>(pendingRequests);
|
||||
}
|
||||
|
||||
for (Future f : tempMap.values()) {
|
||||
if (!f.isCancelled() && !f.isDone()) {
|
||||
f.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
synchronized (pendingRequests) {
|
||||
// Remove cancelled requests
|
||||
for (ImageView v : tempMap.keySet()) {
|
||||
pendingRequests.remove(v);
|
||||
}
|
||||
}
|
||||
public void removeApp(AppView.AppObject app) {
|
||||
itemList.remove(app);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Hide the image view while we're loading the image from disk cache
|
||||
imgView.setVisibility(View.INVISIBLE);
|
||||
|
||||
// Check the on-disk cache
|
||||
new ImageCacheRequest(imgView, obj.app.getAppId()).execute();
|
||||
|
||||
public boolean populateImageView(ImageView imgView, AppView.AppObject obj) {
|
||||
// Let the cached asset loader handle it
|
||||
loader.populateImageView(obj.app, imgView);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -201,84 +112,4 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
// No overlay
|
||||
return false;
|
||||
}
|
||||
|
||||
private class ImageCacheRequest extends AsyncTask<Void, Void, Bitmap> {
|
||||
private ImageView view;
|
||||
private 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 e) {}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@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);
|
||||
view.setVisibility(View.VISIBLE);
|
||||
}
|
||||
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);
|
||||
view.setVisibility(View.VISIBLE);
|
||||
|
||||
// Set SSL contexts correctly to allow us to authenticate
|
||||
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts);
|
||||
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext);
|
||||
|
||||
// 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, Bitmap result) {
|
||||
synchronized (pendingRequests) {
|
||||
pendingRequests.remove(view);
|
||||
}
|
||||
|
||||
if (result != null) {
|
||||
// Make the view visible now
|
||||
view.setImageBitmap(result);
|
||||
view.setVisibility(View.VISIBLE);
|
||||
|
||||
// Populate the disk cache if we got an image back
|
||||
populateBitmapCache(computer.uuid, appId, result);
|
||||
}
|
||||
else {
|
||||
// Leave the loading icon as is (probably should change this eventually...)
|
||||
}
|
||||
}
|
||||
});
|
||||
pendingRequests.put(view, f);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ import com.limelight.R;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
||||
protected Context context;
|
||||
protected int defaultImageRes;
|
||||
protected int layoutId;
|
||||
protected ArrayList<T> itemList = new ArrayList<T>();
|
||||
protected LayoutInflater inflater;
|
||||
protected final Context context;
|
||||
protected final int defaultImageRes;
|
||||
protected final int layoutId;
|
||||
protected final ArrayList<T> itemList = new ArrayList<T>();
|
||||
protected final LayoutInflater inflater;
|
||||
|
||||
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
|
||||
this.context = context;
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
package com.limelight.grid.assets;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class CachedAppAssetLoader {
|
||||
private static final int MAX_CONCURRENT_DISK_LOADS = 3;
|
||||
private static final int MAX_CONCURRENT_NETWORK_LOADS = 3;
|
||||
private static final int MAX_CONCURRENT_CACHE_LOADS = 1;
|
||||
|
||||
private static final int MAX_PENDING_CACHE_LOADS = 100;
|
||||
private static final int MAX_PENDING_NETWORK_LOADS = 40;
|
||||
private static final int MAX_PENDING_DISK_LOADS = 40;
|
||||
|
||||
private final ThreadPoolExecutor cacheExecutor = new ThreadPoolExecutor(
|
||||
MAX_CONCURRENT_CACHE_LOADS, MAX_CONCURRENT_CACHE_LOADS,
|
||||
Long.MAX_VALUE, TimeUnit.DAYS,
|
||||
new LinkedBlockingQueue<Runnable>(MAX_PENDING_CACHE_LOADS),
|
||||
new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||
|
||||
private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(
|
||||
MAX_CONCURRENT_DISK_LOADS, MAX_CONCURRENT_DISK_LOADS,
|
||||
Long.MAX_VALUE, TimeUnit.DAYS,
|
||||
new LinkedBlockingQueue<Runnable>(MAX_PENDING_DISK_LOADS),
|
||||
new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||
|
||||
private final ThreadPoolExecutor networkExecutor = new ThreadPoolExecutor(
|
||||
MAX_CONCURRENT_NETWORK_LOADS, MAX_CONCURRENT_NETWORK_LOADS,
|
||||
Long.MAX_VALUE, TimeUnit.DAYS,
|
||||
new LinkedBlockingQueue<Runnable>(MAX_PENDING_NETWORK_LOADS),
|
||||
new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||
|
||||
private final ComputerDetails computer;
|
||||
private final double scalingDivider;
|
||||
private final NetworkAssetLoader networkLoader;
|
||||
private final MemoryAssetLoader memoryLoader;
|
||||
private final DiskAssetLoader diskLoader;
|
||||
private final Bitmap placeholderBitmap;
|
||||
|
||||
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
||||
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
||||
DiskAssetLoader diskLoader, Bitmap placeholderBitmap) {
|
||||
this.computer = computer;
|
||||
this.scalingDivider = scalingDivider;
|
||||
this.networkLoader = networkLoader;
|
||||
this.memoryLoader = memoryLoader;
|
||||
this.diskLoader = diskLoader;
|
||||
this.placeholderBitmap = placeholderBitmap;
|
||||
}
|
||||
|
||||
public void cancelBackgroundLoads() {
|
||||
Runnable r;
|
||||
while ((r = cacheExecutor.getQueue().poll()) != null) {
|
||||
cacheExecutor.remove(r);
|
||||
}
|
||||
}
|
||||
|
||||
public void cancelForegroundLoads() {
|
||||
Runnable r;
|
||||
|
||||
while ((r = foregroundExecutor.getQueue().poll()) != null) {
|
||||
foregroundExecutor.remove(r);
|
||||
}
|
||||
|
||||
while ((r = networkExecutor.getQueue().poll()) != null) {
|
||||
networkExecutor.remove(r);
|
||||
}
|
||||
}
|
||||
|
||||
public void freeCacheMemory() {
|
||||
memoryLoader.clearCache();
|
||||
}
|
||||
|
||||
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
|
||||
// Try 3 times
|
||||
for (int i = 0; i < 3; i++) {
|
||||
// Check again whether we've been cancelled or the image view is gone
|
||||
if (task != null && (task.isCancelled() || task.imageViewRef.get() == null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
InputStream in = networkLoader.getBitmapStream(tuple);
|
||||
if (in != null) {
|
||||
// Write the stream straight to disk
|
||||
diskLoader.populateCacheWithStream(tuple, in);
|
||||
|
||||
// Close the network input stream
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
// If there's a task associated with this load, we should return the bitmap
|
||||
if (task != null) {
|
||||
// If the cached bitmap is valid, return it. Otherwise, we'll try the load again
|
||||
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||
if (bmp != null) {
|
||||
return bmp;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Otherwise it's a background load and we return nothing
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait 1 second with a bit of fuzz
|
||||
try {
|
||||
Thread.sleep((int) (1000 + (Math.random() * 500)));
|
||||
} catch (InterruptedException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
|
||||
private final WeakReference<ImageView> imageViewRef;
|
||||
private final boolean diskOnly;
|
||||
|
||||
private LoaderTuple tuple;
|
||||
|
||||
public LoaderTask(ImageView imageView, boolean diskOnly) {
|
||||
this.imageViewRef = new WeakReference<ImageView>(imageView);
|
||||
this.diskOnly = diskOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitmap doInBackground(LoaderTuple... params) {
|
||||
tuple = params[0];
|
||||
|
||||
// Check whether it has been cancelled or the image view is gone
|
||||
if (isCancelled() || imageViewRef.get() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||
if (bmp == null) {
|
||||
if (!diskOnly) {
|
||||
// Try to load the asset from the network
|
||||
bmp = doNetworkAssetLoad(tuple, this);
|
||||
} else {
|
||||
// Report progress to display the placeholder and spin
|
||||
// off the network-capable task
|
||||
publishProgress();
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the bitmap
|
||||
if (bmp != null) {
|
||||
memoryLoader.populateCache(tuple, bmp);
|
||||
}
|
||||
|
||||
return bmp;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProgressUpdate(Void... nothing) {
|
||||
// Do nothing if cancelled
|
||||
if (isCancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the current loader task for this view isn't us, do nothing
|
||||
final ImageView imageView = imageViewRef.get();
|
||||
if (getLoaderTask(imageView) == this) {
|
||||
// Set off another loader task on the network executor
|
||||
LoaderTask task = new LoaderTask(imageView, false);
|
||||
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
|
||||
imageView.setAlpha(1.0f);
|
||||
imageView.setImageDrawable(asyncDrawable);
|
||||
task.executeOnExecutor(networkExecutor, tuple);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Bitmap bitmap) {
|
||||
// Do nothing if cancelled
|
||||
if (isCancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ImageView imageView = imageViewRef.get();
|
||||
if (getLoaderTask(imageView) == this) {
|
||||
// Set the bitmap
|
||||
if (bitmap != null) {
|
||||
imageView.setImageBitmap(bitmap);
|
||||
}
|
||||
|
||||
// Show the view
|
||||
imageView.setAlpha(1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class AsyncDrawable extends BitmapDrawable {
|
||||
private final WeakReference<LoaderTask> loaderTaskReference;
|
||||
|
||||
public AsyncDrawable(Resources res, Bitmap bitmap,
|
||||
LoaderTask loaderTask) {
|
||||
super(res, bitmap);
|
||||
loaderTaskReference = new WeakReference<LoaderTask>(loaderTask);
|
||||
}
|
||||
|
||||
public LoaderTask getLoaderTask() {
|
||||
return loaderTaskReference.get();
|
||||
}
|
||||
}
|
||||
|
||||
private static LoaderTask getLoaderTask(ImageView imageView) {
|
||||
if (imageView == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Drawable drawable = imageView.getDrawable();
|
||||
|
||||
// If our drawable is in play, get the loader task
|
||||
if (drawable instanceof AsyncDrawable) {
|
||||
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
|
||||
return asyncDrawable.getLoaderTask();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean cancelPendingLoad(LoaderTuple tuple, ImageView imageView) {
|
||||
final LoaderTask loaderTask = getLoaderTask(imageView);
|
||||
|
||||
// Check if any task was pending for this image view
|
||||
if (loaderTask != null && !loaderTask.isCancelled()) {
|
||||
final LoaderTuple taskTuple = loaderTask.tuple;
|
||||
|
||||
// Cancel the task if it's not already loading the same data
|
||||
if (taskTuple == null || !taskTuple.equals(tuple)) {
|
||||
loaderTask.cancel(true);
|
||||
} else {
|
||||
// It's already loading what we want
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow the load to proceed
|
||||
return true;
|
||||
}
|
||||
|
||||
public void queueCacheLoad(NvApp app) {
|
||||
final LoaderTuple tuple = new LoaderTuple(computer, app);
|
||||
|
||||
if (memoryLoader.loadBitmapFromCache(tuple) != null) {
|
||||
// It's in memory which means it must also be on disk
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue a fetch in the cache executor
|
||||
cacheExecutor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Check if the image is cached on disk
|
||||
if (diskLoader.checkCacheExists(tuple)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to load the asset from the network and cache result on disk
|
||||
doNetworkAssetLoad(tuple, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void populateImageView(NvApp app, ImageView view) {
|
||||
LoaderTuple tuple = new LoaderTuple(computer, app);
|
||||
|
||||
// If there's already a task in progress for this view,
|
||||
// cancel it. If the task is already loading the same image,
|
||||
// we return and let that load finish.
|
||||
if (!cancelPendingLoad(tuple, view)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// First, try the memory cache in the current context
|
||||
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
|
||||
if (bmp != null) {
|
||||
// Show the bitmap immediately
|
||||
view.setAlpha(1.0f);
|
||||
view.setImageBitmap(bmp);
|
||||
return;
|
||||
}
|
||||
|
||||
// If it's not in memory, create an async task to load it. This task will be attached
|
||||
// via AsyncDrawable to this view.
|
||||
final LoaderTask task = new LoaderTask(view, true);
|
||||
final AsyncDrawable asyncDrawable = new AsyncDrawable(view.getResources(), placeholderBitmap, task);
|
||||
view.setAlpha(0.0f);
|
||||
view.setImageDrawable(asyncDrawable);
|
||||
|
||||
// Run the task on our foreground executor
|
||||
task.executeOnExecutor(foregroundExecutor, tuple);
|
||||
}
|
||||
|
||||
public class LoaderTuple {
|
||||
public final ComputerDetails computer;
|
||||
public final NvApp app;
|
||||
|
||||
public LoaderTuple(ComputerDetails computer, NvApp app) {
|
||||
this.computer = computer;
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof LoaderTuple)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LoaderTuple other = (LoaderTuple) o;
|
||||
return computer.uuid.equals(other.computer.uuid) && app.getAppId() == other.app.getAppId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "("+computer.uuid+", "+app.getAppId()+")";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
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.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class DiskAssetLoader {
|
||||
// 5 MB
|
||||
private final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
|
||||
|
||||
private final File cacheDir;
|
||||
|
||||
public DiskAssetLoader(File cacheDir) {
|
||||
this.cacheDir = cacheDir;
|
||||
}
|
||||
|
||||
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||
}
|
||||
|
||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
||||
InputStream in = null;
|
||||
Bitmap bmp = null;
|
||||
try {
|
||||
// Make sure the cached asset doesn't exceed the maximum size
|
||||
if (CacheHelper.getFileSize(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png") > MAX_ASSET_SIZE) {
|
||||
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
|
||||
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||
return null;
|
||||
}
|
||||
|
||||
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inSampleSize = sampleSize;
|
||||
options.inPreferredConfig = Bitmap.Config.RGB_565;
|
||||
bmp = BitmapFactory.decodeStream(in, null, options);
|
||||
} catch (IOException ignored) {
|
||||
} 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;
|
||||
boolean success = false;
|
||||
try {
|
||||
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||
CacheHelper.writeInputStreamToOutputStream(input, out, MAX_ASSET_SIZE);
|
||||
success = true;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (out != null) {
|
||||
try {
|
||||
out.close();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
LimeLog.warning("Unable to populate cache with tuple: "+tuple);
|
||||
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 / 16) {
|
||||
@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,49 @@
|
||||
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;
|
||||
|
||||
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 ignored) {}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user