diff --git a/app/build.gradle b/app/build.gradle index bff822c5..7eb77625 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/libs/limelight-common.jar b/app/libs/limelight-common.jar index 47febbf9..2bffce06 100644 Binary files a/app/libs/limelight-common.jar and b/app/libs/limelight-common.jar differ diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index b4e87227..6a87fba6 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -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 { 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, CachedAppAssetLoader.LoaderTuple> loadingTuples = new ConcurrentHashMap<>(); private final ConcurrentHashMap backgroundLoadingTuples = new ConcurrentHashMap<>(); @@ -33,8 +39,26 @@ public class AppGridAdapter extends GenericGridAdapter { 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 { } @Override - public void notifyLoadComplete(Object object, final Bitmap bitmap) { + public void notifyLoadComplete(Object object, Bitmap bitmap) { final WeakReference viewRef = (WeakReference) object; loadingTuples.remove(viewRef); @@ -117,12 +141,13 @@ public class AppGridAdapter extends GenericGridAdapter { 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); } } diff --git a/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java index 172b05c2..3635040c 100644 --- a/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java @@ -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()); private final ThreadPoolExecutor backgroundExecutor = new ThreadPoolExecutor(2, 2, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue()); - 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); - if (bmp != null) { - break; + 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) {} - } - - if (bmp != null) { - // Populate the disk cache - diskLoader.populateCache(tuple, bmp); + Thread.sleep((int) (1000 + (Math.random() * 500))); + } catch (InterruptedException e) { + break; + } } } @@ -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 diff --git a/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java index 219ca8a7..2da573de 100644 --- a/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java @@ -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 { diff --git a/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java index 4fa26fca..995a4f36 100644 --- a/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java @@ -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 memoryCache = new LruCache(maxMemory / 8) { + private static final LruCache memoryCache = new LruCache(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); } diff --git a/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java index 0150b230..b80f09ee 100644 --- a/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java @@ -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) { diff --git a/app/src/main/java/com/limelight/utils/CacheHelper.java b/app/src/main/java/com/limelight/utils/CacheHelper.java index 4bbebd46..581c26fa 100644 --- a/app/src/main/java/com/limelight/utils/CacheHelper.java +++ b/app/src/main/java/com/limelight/utils/CacheHelper.java @@ -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);