mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2025-07-20 19:42:45 +00:00
Rewrite the app art caching and fetching (again!) to finally address OOM problems and speed up art loading
This commit is contained in:
parent
194037ff41
commit
80d8c5953e
@ -67,10 +67,6 @@ dependencies {
|
||||
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
|
||||
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
|
||||
|
||||
compile group: 'com.google.android', name: 'support-v4', version:'r7'
|
||||
compile group: 'com.koushikdutta.ion', name: 'ion', version:'2.0.5'
|
||||
compile group: 'com.google.code.gson', name: 'gson', version:'2.3.1'
|
||||
|
||||
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.2.0'
|
||||
compile group: 'com.squareup.okio', name:'okio', version:'1.2.0'
|
||||
|
||||
|
Binary file not shown.
@ -2,10 +2,12 @@ package com.limelight.grid;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.limelight.AppView;
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.R;
|
||||
import com.limelight.grid.assets.CachedAppAssetLoader;
|
||||
import com.limelight.grid.assets.DiskAssetLoader;
|
||||
@ -26,6 +28,10 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
private final Activity activity;
|
||||
|
||||
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 final CachedAppAssetLoader loader;
|
||||
private final ConcurrentHashMap<WeakReference<ImageView>, CachedAppAssetLoader.LoaderTuple> loadingTuples = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Object, CachedAppAssetLoader.LoaderTuple> backgroundLoadingTuples = new ConcurrentHashMap<>();
|
||||
@ -33,8 +39,26 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws KeyManagementException, NoSuchAlgorithmException {
|
||||
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
|
||||
|
||||
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
|
||||
int dp;
|
||||
|
||||
if (small) {
|
||||
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, uniqueId, new NetworkAssetLoader(context),
|
||||
this.loader = new CachedAppAssetLoader(computer, uniqueId, scalingDivisor,
|
||||
new NetworkAssetLoader(context, uniqueId),
|
||||
new MemoryAssetLoader(), new DiskAssetLoader(context.getCacheDir()));
|
||||
}
|
||||
|
||||
@ -102,7 +126,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyLoadComplete(Object object, final Bitmap bitmap) {
|
||||
public void notifyLoadComplete(Object object, Bitmap bitmap) {
|
||||
final WeakReference<ImageView> viewRef = (WeakReference<ImageView>) object;
|
||||
|
||||
loadingTuples.remove(viewRef);
|
||||
@ -117,12 +141,13 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
return;
|
||||
}
|
||||
|
||||
final Bitmap viewBmp = bitmap;
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ImageView view = viewRef.get();
|
||||
if (view != null) {
|
||||
view.setImageBitmap(bitmap);
|
||||
view.setImageBitmap(viewBmp);
|
||||
fadeInImage(view);
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
package com.limelight.grid.assets;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
|
||||
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;
|
||||
@ -12,21 +14,34 @@ import java.util.concurrent.TimeUnit;
|
||||
public class CachedAppAssetLoader {
|
||||
private final ComputerDetails computer;
|
||||
private final String uniqueId;
|
||||
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 NetworkLoader networkLoader;
|
||||
private final CachedLoader memoryLoader;
|
||||
private final CachedLoader diskLoader;
|
||||
private final NetworkAssetLoader networkLoader;
|
||||
private final MemoryAssetLoader memoryLoader;
|
||||
private final DiskAssetLoader diskLoader;
|
||||
|
||||
public CachedAppAssetLoader(ComputerDetails computer, String uniqueId, NetworkLoader networkLoader, CachedLoader memoryLoader, CachedLoader diskLoader) {
|
||||
public CachedAppAssetLoader(ComputerDetails computer, String uniqueId, double scalingDivider,
|
||||
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
||||
DiskAssetLoader diskLoader) {
|
||||
this.computer = computer;
|
||||
this.uniqueId = uniqueId;
|
||||
this.scalingDivider = scalingDivider;
|
||||
|
||||
this.networkLoader = networkLoader;
|
||||
this.memoryLoader = memoryLoader;
|
||||
this.diskLoader = diskLoader;
|
||||
}
|
||||
|
||||
private static Bitmap scaleBitmapAndRecyle(Bitmap bmp, double scalingDivider) {
|
||||
Bitmap newBmp = Bitmap.createScaledBitmap(bmp, (int)(bmp.getWidth() / scalingDivider),
|
||||
(int)(bmp.getHeight() / scalingDivider), true);
|
||||
if (newBmp != bmp) {
|
||||
bmp.recycle();
|
||||
}
|
||||
return newBmp;
|
||||
}
|
||||
|
||||
private Runnable createLoaderRunnable(final LoaderTuple tuple, final Object context, final LoadListener listener) {
|
||||
return new Runnable() {
|
||||
@Override
|
||||
@ -36,7 +51,7 @@ public class CachedAppAssetLoader {
|
||||
return;
|
||||
}
|
||||
|
||||
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple);
|
||||
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||
if (bmp == null) {
|
||||
// Notify the listener that this may take a while
|
||||
listener.notifyLongLoad(context);
|
||||
@ -48,20 +63,24 @@ public class CachedAppAssetLoader {
|
||||
return;
|
||||
}
|
||||
|
||||
bmp = networkLoader.loadBitmap(tuple);
|
||||
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) {}
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (bmp != null) {
|
||||
// Populate the disk cache
|
||||
diskLoader.populateCache(tuple, bmp);
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,7 +114,7 @@ public class CachedAppAssetLoader {
|
||||
}
|
||||
|
||||
private LoaderTuple loadBitmapWithContext(NvApp app, Object context, LoadListener listener, boolean background) {
|
||||
LoaderTuple tuple = new LoaderTuple(computer, uniqueId, app);
|
||||
LoaderTuple tuple = new LoaderTuple(computer, app);
|
||||
|
||||
// First, try the memory cache in the current context
|
||||
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
|
||||
@ -125,15 +144,13 @@ public class CachedAppAssetLoader {
|
||||
|
||||
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) {
|
||||
public LoaderTuple(ComputerDetails computer, NvApp app) {
|
||||
this.computer = computer;
|
||||
this.uniqueId = uniqueId;
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
@ -150,15 +167,6 @@ public class CachedAppAssetLoader {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -11,20 +11,21 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class DiskAssetLoader implements CachedAppAssetLoader.CachedLoader {
|
||||
public class DiskAssetLoader {
|
||||
private final File cacheDir;
|
||||
|
||||
public DiskAssetLoader(File cacheDir) {
|
||||
this.cacheDir = cacheDir;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||
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");
|
||||
bmp = BitmapFactory.decodeStream(in);
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inSampleSize = sampleSize;
|
||||
bmp = BitmapFactory.decodeStream(in, null, options);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
@ -42,13 +43,11 @@ public class DiskAssetLoader implements CachedAppAssetLoader.CachedLoader {
|
||||
return bmp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
|
||||
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
|
||||
OutputStream out = null;
|
||||
try {
|
||||
// PNG ignores quality setting
|
||||
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
|
||||
CacheHelper.writeInputStreamToOutputStream(input, out);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
|
@ -5,9 +5,9 @@ import android.util.LruCache;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
public class MemoryAssetLoader implements CachedAppAssetLoader.CachedLoader {
|
||||
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 / 8) {
|
||||
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 12) {
|
||||
@Override
|
||||
protected int sizeOf(String key, Bitmap bitmap) {
|
||||
// Sizeof returns kilobytes
|
||||
@ -19,7 +19,6 @@ public class MemoryAssetLoader implements CachedAppAssetLoader.CachedLoader {
|
||||
return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||
Bitmap bmp = memoryCache.get(constructKey(tuple));
|
||||
if (bmp != null) {
|
||||
@ -28,7 +27,6 @@ public class MemoryAssetLoader implements CachedAppAssetLoader.CachedLoader {
|
||||
return bmp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
|
||||
memoryCache.put(constructKey(tuple), bitmap);
|
||||
}
|
||||
|
@ -2,109 +2,44 @@ package com.limelight.grid.assets;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
|
||||
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 com.limelight.nvstream.http.NvHTTP;
|
||||
|
||||
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.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 {
|
||||
public class NetworkAssetLoader {
|
||||
private final Context context;
|
||||
private final LimelightCryptoProvider cryptoProvider;
|
||||
private final SSLContext sslContext;
|
||||
private final String uniqueId;
|
||||
|
||||
public NetworkAssetLoader(Context context) throws NoSuchAlgorithmException, KeyManagementException {
|
||||
public NetworkAssetLoader(Context context, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException {
|
||||
this.context = context;
|
||||
|
||||
cryptoProvider = PlatformBinding.getCryptoProvider(context);
|
||||
|
||||
sslContext = SSLContext.getInstance("SSL");
|
||||
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom());
|
||||
this.uniqueId = uniqueId;
|
||||
}
|
||||
|
||||
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) {}
|
||||
}};
|
||||
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||
NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
|
||||
|
||||
private final KeyManager[] ourKeyman = new KeyManager[] {
|
||||
new X509KeyManager() {
|
||||
public String chooseClientAlias(String[] keyTypes,
|
||||
Principal[] issuers, Socket socket) {
|
||||
return "Limelight-RSA";
|
||||
}
|
||||
InputStream in = null;
|
||||
try {
|
||||
in = http.getBoxArt(tuple.app);
|
||||
} catch (IOException e) {}
|
||||
|
||||
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);
|
||||
Ion.getDefault(context).getBitmapCache().clear();
|
||||
|
||||
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) {
|
||||
if (in != null) {
|
||||
LimeLog.info("Network asset load complete: " + tuple);
|
||||
}
|
||||
else {
|
||||
LimeLog.info("Network asset load failed: " + tuple);
|
||||
}
|
||||
|
||||
return bmp;
|
||||
return in;
|
||||
}
|
||||
|
||||
private static InetAddress getCurrentAddress(ComputerDetails computer) {
|
||||
|
@ -38,6 +38,15 @@ public class CacheHelper {
|
||||
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 {
|
||||
Reader r = new InputStreamReader(in);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user