From ee58071ff176a07c97a3b33931555d57a5149ca7 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Wed, 25 Feb 2015 21:07:35 -0500 Subject: [PATCH] Fix huge performance issues when dealing with large app lists --- app/src/main/java/com/limelight/AppView.java | 13 +- .../com/limelight/grid/AppGridAdapter.java | 290 ++++-------------- .../grid/assets/CachedAppAssetLoader.java | 156 ++++++++++ .../grid/assets/DiskAssetLoader.java | 56 ++++ .../grid/assets/MemoryAssetLoader.java | 35 +++ .../grid/assets/NetworkAssetLoader.java | 120 ++++++++ 6 files changed, 439 insertions(+), 231 deletions(-) create mode 100644 app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java create mode 100644 app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java create mode 100644 app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java create mode 100644 app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index f160181c..89e6891c 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -13,6 +13,7 @@ import com.limelight.computers.ComputerManagerListener; import com.limelight.computers.ComputerManagerService; import com.limelight.grid.AppGridAdapter; import com.limelight.nvstream.http.ComputerDetails; +import com.limelight.nvstream.http.GfeHttpResponseException; import com.limelight.nvstream.http.NvApp; import com.limelight.nvstream.http.NvHTTP; import com.limelight.preferences.PreferenceConfiguration; @@ -446,10 +447,18 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { httpConn = new NvHTTP(getAddress(), managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(AppView.this)); 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 { - message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName(); + message = e.getMessage(); } } catch (UnknownHostException e) { message = getResources().getString(R.string.error_unknown_host); diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index 0ff2f89e..bf6b9144 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -1,112 +1,38 @@ package com.limelight.grid; -import android.content.Context; +import android.app.Activity; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.os.AsyncTask; import android.widget.ImageView; import android.widget.TextView; -import com.koushikdutta.async.future.FutureCallback; -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.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.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; +import java.util.concurrent.ConcurrentHashMap; public class AppGridAdapter extends GenericGridAdapter { + private final Activity activity; - private final ComputerDetails computer; - private final String uniqueId; - private final LimelightCryptoProvider cryptoProvider; - private final SSLContext sslContext; - private final HashMap pendingIonRequests = new HashMap(); - private final HashMap pendingCacheRequests = new HashMap(); + private final CachedAppAssetLoader loader; + private final ConcurrentHashMap loadingTuples = new ConcurrentHashMap<>(); - 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) 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); - this.computer = computer; - this.uniqueId = uniqueId; - - cryptoProvider = PlatformBinding.getCryptoProvider(context); - - sslContext = SSLContext.getInstance("SSL"); - sslContext.init(ourKeyman, trustAllCerts, new SecureRandom()); + this.activity = activity; + this.loader = new CachedAppAssetLoader(computer, uniqueId, new NetworkAssetLoader(context), + new MemoryAssetLoader(), new DiskAssetLoader(context.getCacheDir())); } - 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() { Collections.sort(itemList, new Comparator() { @Override @@ -116,15 +42,6 @@ public class AppGridAdapter extends GenericGridAdapter { }); } - private InetAddress getCurrentAddress() { - if (computer.reachability == ComputerDetails.Reachability.LOCAL) { - return computer.localIp; - } - else { - return computer.remoteIp; - } - } - public void addApp(AppView.AppObject app) { itemList.add(app); sortList(); @@ -134,46 +51,59 @@ public class AppGridAdapter extends GenericGridAdapter { 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(); - } - } + private final CachedAppAssetLoader.LoadListener loadListener = new CachedAppAssetLoader.LoadListener() { + @Override + public void notifyLongLoad(Object object) { + final ImageView view = (ImageView) object; + + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + 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) { - // Cancel any pending cache requests for this view - synchronized (pendingCacheRequests) { - ImageCacheRequest req = pendingCacheRequests.remove(imgView); - if (req != null) { - req.cancel(false); - } - } - - // 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); - } + // Cancel pending loads on this image view + CachedAppAssetLoader.LoaderTuple tuple = loadingTuples.remove(imgView); + if (tuple != null) { + // FIXME: There's a small chance that this can race if we've already gone down + // the path to notification but haven't been notified yet + tuple.cancel(); } // Clear existing contents of the image view imgView.setAlpha(0.0f); - // Check the on-disk cache - ImageCacheRequest req = new ImageCacheRequest(imgView, obj.app.getAppId()); - synchronized (pendingCacheRequests) { - pendingCacheRequests.put(imgView, req); + // Start loading the bitmap + tuple = loader.loadBitmapWithContext(obj.app, imgView, loadListener); + if (tuple != null) { + // The load was issued asynchronously + loadingTuples.put(imgView, tuple); } - req.execute(); - return true; } @@ -198,105 +128,7 @@ public class AppGridAdapter extends GenericGridAdapter { return false; } - private class ImageCacheRequest extends AsyncTask { - private final ImageView view; - 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 f = Ion.with(context) - .load("https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" + - appId + "&AssetType=2&AssetIdx=0") - .asBitmap() - .setCallback(new FutureCallback() { - @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); - } - } - } + private static void fadeInImage(ImageView view) { + view.animate().alpha(1.0f).setDuration(100).start(); } } diff --git a/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java new file mode 100644 index 00000000..898ce908 --- /dev/null +++ b/app/src/main/java/com/limelight/grid/assets/CachedAppAssetLoader.java @@ -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()); + 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); + } +} diff --git a/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java new file mode 100644 index 00000000..6845395b --- /dev/null +++ b/app/src/main/java/com/limelight/grid/assets/DiskAssetLoader.java @@ -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(); + } + } +} diff --git a/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java new file mode 100644 index 00000000..c343e939 --- /dev/null +++ b/app/src/main/java/com/limelight/grid/assets/MemoryAssetLoader.java @@ -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 memoryCache = new LruCache(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); + } +} diff --git a/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java new file mode 100644 index 00000000..117f2b66 --- /dev/null +++ b/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java @@ -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; + } + } +}