Stub icon scaling and allow background updating of the applist

This commit is contained in:
Cameron Gutman
2015-01-30 18:49:01 -05:00
parent 9ff1386751
commit 4d01e1afe6
11 changed files with 467 additions and 154 deletions
Binary file not shown.
+232 -94
View File
@@ -1,32 +1,45 @@
package com.limelight; package com.limelight;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.UUID;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.binding.crypto.AndroidCryptoProvider;
import com.limelight.computers.ComputerManagerListener;
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.GfeHttpResponseException; 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;
import com.limelight.ui.AdapterFragment; import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks; import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.CacheHelper;
import com.limelight.utils.Dialog; import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog; import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper; import com.limelight.utils.UiHelper;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Service;
import android.content.ComponentName;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@@ -35,26 +48,145 @@ import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AbsListView; import android.widget.AbsListView;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.AdapterContextMenuInfo;
public class AppView extends Activity implements AdapterFragmentCallbacks { public class AppView extends Activity implements AdapterFragmentCallbacks {
private AppGridAdapter appGridAdapter; private AppGridAdapter appGridAdapter;
private InetAddress ipAddress; private String uuidString;
private String uniqueId;
private boolean remote; private ComputerDetails computer;
private boolean firstLoad = true; private ComputerManagerService.ApplistPoller poller;
private SpinnerDialog blockingLoadSpinner;
private String lastRawApplist;
private int consecutiveAppListFailures = 0;
private final static int CONSECUTIVE_FAILURE_LIMIT = 3;
private final static int START_OR_RESUME_ID = 1; private final static int START_OR_RESUME_ID = 1;
private final static int QUIT_ID = 2; private final static int QUIT_ID = 2;
private final static int CANCEL_ID = 3; private final static int CANCEL_ID = 3;
private final static int START_WTIH_QUIT = 4; private final static int START_WTIH_QUIT = 4;
public final static String ADDRESS_EXTRA = "Address"; public final static String NAME_EXTRA = "Name";
public final static String UNIQUEID_EXTRA = "UniqueId"; public final static String UUID_EXTRA = "UUID";
public final static String NAME_EXTRA = "Name";
public final static String REMOTE_EXTRA = "Remote"; private ComputerManagerService.ComputerManagerBinder managerBinder;
private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder);
// Wait in a separate thread to avoid stalling the UI
new Thread() {
@Override
public void run() {
// Wait for the binder to be ready
localBinder.waitForReady();
// Now make the binder visible
managerBinder = localBinder;
// Get the computer object
computer = managerBinder.getComputer(UUID.fromString(uuidString));
// Start updates
startComputerUpdates();
try {
appGridAdapter = new AppGridAdapter(AppView.this, 1.0,
PreferenceConfiguration.readPreferences(AppView.this).listMode,
computer, managerBinder.getUniqueId());
} catch (Exception e) {
e.printStackTrace();
finish();
return;
}
// Load the app grid with cached data (if possible)
populateAppGridWithCache();
getFragmentManager().beginTransaction()
.add(R.id.appFragmentContainer, new AdapterFragment()).commitAllowingStateLoss();
}
}.start();
}
public void onServiceDisconnected(ComponentName className) {
managerBinder = null;
}
};
private InetAddress getAddress() {
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp : computer.remoteIp;
}
private void startComputerUpdates() {
if (managerBinder == null) {
return;
}
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(ComputerDetails details) {
// Don't care about other computers
if (details != computer) {
return;
}
if (details.state != ComputerDetails.State.ONLINE) {
consecutiveAppListFailures++;
if (consecutiveAppListFailures >= CONSECUTIVE_FAILURE_LIMIT) {
// The PC is unreachable now
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
// Display a toast to the user and quit the activity
Toast.makeText(AppView.this, getResources().getText(R.string.lost_connection), Toast.LENGTH_SHORT).show();
finish();
}
});
}
return;
}
// App list is the same or empty; nothing to do
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
return;
}
try {
lastRawApplist = details.rawAppList;
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
} catch (Exception e) {}
}
});
if (poller == null) {
poller = managerBinder.createAppListPoller(computer);
}
poller.start();
}
private void stopComputerUpdates() {
if (poller != null) {
poller.stop();
}
if (managerBinder != null) {
managerBinder.stopPolling();
}
}
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@@ -70,41 +202,35 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
setContentView(R.layout.activity_app_view); setContentView(R.layout.activity_app_view);
UiHelper.notifyNewRootView(this); UiHelper.notifyNewRootView(this);
byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA); uuidString = getIntent().getStringExtra(UUID_EXTRA);
uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA);
remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false);
if (address == null || uniqueId == null) {
finish();
return;
}
String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA); String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA);
TextView label = (TextView) findViewById(R.id.appListText); TextView label = (TextView) findViewById(R.id.appListText);
setTitle(labelText); setTitle(labelText);
label.setText(labelText); label.setText(labelText);
try {
ipAddress = InetAddress.getByAddress(address);
} catch (UnknownHostException e) {
e.printStackTrace();
finish();
return;
}
try { // Bind to the computer manager service
appGridAdapter = new AppGridAdapter(this, bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
PreferenceConfiguration.readPreferences(this).listMode, Service.BIND_AUTO_CREATE);
ipAddress, uniqueId);
} catch (Exception e) {
e.printStackTrace();
finish();
return;
}
getFragmentManager().beginTransaction()
.add(R.id.appFragmentContainer, new AdapterFragment()).commitAllowingStateLoss();
} }
private void populateAppGridWithCache() {
try {
// Try to load from cache
updateUiWithAppList(NvHTTP.getAppListByReader(new InputStreamReader(CacheHelper.openCacheFileForInput(getCacheDir(), "applist", uuidString))));
LimeLog.info("Loaded applist from cache");
} catch (Exception e) {
LimeLog.info("Loading applist from the network");
// We'll need to load from the network
loadAppsBlocking();
}
}
private void loadAppsBlocking() {
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
getResources().getString(R.string.applist_refresh_msg), true);
}
@Override @Override
protected void onDestroy() { protected void onDestroy() {
@@ -112,18 +238,25 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
SpinnerDialog.closeDialogs(this); SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs(); Dialog.closeDialogs();
if (managerBinder != null) {
unbindService(serviceConnection);
}
} }
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
// Display the error message if it was the startComputerUpdates();
// first load, but just kill the activity
// on subsequent errors
updateAppList(firstLoad);
firstLoad = false;
} }
@Override
protected void onPause() {
super.onPause();
stopComputerUpdates();
}
private int getRunningAppId() { private int getRunningAppId() {
int runningAppId = -1; int runningAppId = -1;
@@ -232,62 +365,62 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
return super.onContextItemSelected(item); return super.onContextItemSelected(item);
} }
} }
private void updateAppList(final boolean displayError) { private void updateUiWithAppList(final List<NvApp> appList) {
final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title), AppView.this.runOnUiThread(new Runnable() {
getResources().getString(R.string.applist_refresh_msg), true); @Override
new Thread() { public void run() {
@Override boolean updated = false;
public void run() {
NvHTTP httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this)); for (NvApp app : appList) {
boolean foundExistingApp = false;
try {
final List<NvApp> appList = httpConn.getAppList(); // Try to update an existing app in the list first
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppView.this.runOnUiThread(new Runnable() { AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
@Override if (existingApp.app == null) {
public void run() { continue;
appGridAdapter.clear(); }
for (NvApp app : appList) {
appGridAdapter.addApp(new AppObject(app)); if (existingApp.app.getAppId() == app.getAppId()) {
// Found the app; update its properties
if (existingApp.app.getIsRunning() != app.getIsRunning()) {
existingApp.app.setIsRunningBoolean(app.getIsRunning());
updated = true;
}
if (!existingApp.app.getAppName().equals(app.getAppName())) {
existingApp.app.setAppName(app.getAppName());
updated = true;
} }
appGridAdapter.notifyDataSetChanged(); foundExistingApp = true;
} break;
});
// Success case
return;
} catch (GfeHttpResponseException ignored) {
} catch (IOException ignored) {
} catch (XmlPullParserException ignored) {
} finally {
spinner.dismiss();
}
if (displayError) {
Dialog.displayDialog(AppView.this, getResources().getString(R.string.applist_refresh_error_title),
getResources().getString(R.string.applist_refresh_error_msg), true);
}
else {
// Just finish the activity immediately
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
finish();
} }
}); }
if (!foundExistingApp) {
// This app must be new
appGridAdapter.addApp(new AppObject(app));
updated = true;
}
} }
}
}.start(); if (updated) {
appGridAdapter.notifyDataSetChanged();
}
}
});
} }
private void doStart(NvApp app) { private void doStart(NvApp app) {
Intent intent = new Intent(this, Game.class); Intent intent = new Intent(this, Game.class);
intent.putExtra(Game.EXTRA_HOST, ipAddress.getHostAddress()); intent.putExtra(Game.EXTRA_HOST,
computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
intent.putExtra(Game.EXTRA_APP, app.getAppName()); intent.putExtra(Game.EXTRA_APP, app.getAppName());
intent.putExtra(Game.EXTRA_UNIQUEID, uniqueId); intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
intent.putExtra(Game.EXTRA_STREAMING_REMOTE, remote); intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
computer.reachability != ComputerDetails.Reachability.LOCAL);
startActivity(intent); startActivity(intent);
} }
@@ -299,21 +432,26 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
NvHTTP httpConn; NvHTTP httpConn;
String message; String message;
try { try {
httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this)); httpConn = new NvHTTP(getAddress(),
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 { else {
message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName(); message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName();
} }
updateAppList(true);
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
message = getResources().getString(R.string.error_unknown_host); message = getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
message = getResources().getString(R.string.error_404); message = getResources().getString(R.string.error_404);
} catch (Exception e) { } catch (Exception e) {
message = e.getMessage(); message = e.getMessage();
} } finally {
// Trigger a poll immediately
if (poller != null) {
poller.pollNow();
}
}
final String toastMessage = message; final String toastMessage = message;
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
+2 -11
View File
@@ -156,7 +156,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection, bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE); Service.BIND_AUTO_CREATE);
pcGridAdapter = new PcGridAdapter(this, pcGridAdapter = new PcGridAdapter(this, 1.0,
PreferenceConfiguration.readPreferences(this).listMode); PreferenceConfiguration.readPreferences(this).listMode);
initializeViews(); initializeViews();
@@ -474,16 +474,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
Intent i = new Intent(this, AppView.class); Intent i = new Intent(this, AppView.class);
i.putExtra(AppView.NAME_EXTRA, computer.name); i.putExtra(AppView.NAME_EXTRA, computer.name);
i.putExtra(AppView.UNIQUEID_EXTRA, managerBinder.getUniqueId()); i.putExtra(AppView.UUID_EXTRA, computer.uuid.toString());
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
i.putExtra(AppView.ADDRESS_EXTRA, computer.localIp.getAddress());
i.putExtra(AppView.REMOTE_EXTRA, false);
}
else {
i.putExtra(AppView.ADDRESS_EXTRA, computer.remoteIp.getAddress());
i.putExtra(AppView.REMOTE_EXTRA, true);
}
startActivity(i); startActivity(i);
} }
@@ -1,7 +1,11 @@
package com.limelight.computers; package com.limelight.computers;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import com.limelight.LimeLog; import com.limelight.LimeLog;
@@ -11,6 +15,7 @@ import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.mdns.MdnsComputer; import com.limelight.nvstream.mdns.MdnsComputer;
import com.limelight.nvstream.mdns.MdnsDiscoveryListener; import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
import com.limelight.utils.CacheHelper;
import android.app.Service; import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
@@ -179,10 +184,26 @@ public class ComputerManagerService extends Service {
// Just call the unbind handler to cleanup // Just call the unbind handler to cleanup
ComputerManagerService.this.onUnbind(null); ComputerManagerService.this.onUnbind(null);
} }
public ApplistPoller createAppListPoller(ComputerDetails computer) {
return new ApplistPoller(computer);
}
public String getUniqueId() { public String getUniqueId() {
return idManager.getUniqueId(); return idManager.getUniqueId();
} }
public ComputerDetails getComputer(UUID uuid) {
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
if (uuid.equals(tuple.computer.uuid)) {
return tuple.computer;
}
}
}
return null;
}
} }
@Override @Override
@@ -462,6 +483,98 @@ public class ComputerManagerService extends Service {
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
return binder; return binder;
} }
public class ApplistPoller {
private Thread thread;
private ComputerDetails computer;
private Object pollEvent = new Object();
public ApplistPoller(ComputerDetails computer) {
this.computer = computer;
}
public void pollNow() {
synchronized (pollEvent) {
pollEvent.notify();
}
}
private boolean waitPollingDelay() {
try {
synchronized (pollEvent) {
pollEvent.wait(POLLING_PERIOD_MS);
}
} catch (InterruptedException e) {
return false;
}
return !thread.isInterrupted();
}
public void start() {
thread = new Thread() {
@Override
public void run() {
do {
InetAddress selectedAddr;
// Can't poll if it's not online
if (computer.state != ComputerDetails.State.ONLINE) {
listener.notifyComputerUpdated(computer);
continue;
}
// Can't poll if there's no UUID yet
if (computer.uuid == null) {
continue;
}
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
selectedAddr = computer.localIp;
}
else {
selectedAddr = computer.remoteIp;
}
NvHTTP http = new NvHTTP(selectedAddr, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
try {
// Query the app list from the server
String appList = http.getAppListRaw();
// Open the cache file
LimeLog.info("Updating app list from "+computer.uuid.toString());
FileOutputStream cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
CacheHelper.writeStringToOutputStream(cacheOut, appList);
cacheOut.close();
// Update the computer
computer.rawAppList = appList;
// Notify that the app list has been updated
listener.notifyComputerUpdated(computer);
} catch (IOException e) {
e.printStackTrace();
}
} while (waitPollingDelay());
}
};
thread.start();
}
public void stop() {
if (thread != null) {
thread.interrupt();
try {
thread.join();
} catch (InterruptedException e) {}
thread = null;
}
}
}
} }
class PollingTuple { class PollingTuple {
@@ -13,7 +13,9 @@ import com.limelight.AppView;
import com.limelight.LimeLog; import com.limelight.LimeLog;
import com.limelight.R; import com.limelight.R;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.LimelightCryptoProvider; import com.limelight.nvstream.http.LimelightCryptoProvider;
import com.limelight.utils.CacheHelper;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.File; import java.io.File;
@@ -32,6 +34,7 @@ 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.HashMap;
import java.util.UUID;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
@@ -45,17 +48,16 @@ import java.security.cert.X509Certificate;
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> { public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
private boolean listMode; private ComputerDetails computer;
private InetAddress address;
private String uniqueId; private String uniqueId;
private LimelightCryptoProvider cryptoProvider; private LimelightCryptoProvider cryptoProvider;
private SSLContext sslContext; private SSLContext sslContext;
private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>(); private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>();
public AppGridAdapter(Context context, boolean listMode, InetAddress address, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException { public AppGridAdapter(Context context, double gridScaleFactor, boolean listMode, ComputerDetails computer, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException {
super(context, listMode ? R.layout.simple_row : R.layout.app_grid_item, R.drawable.image_loading); super(context, listMode ? R.layout.simple_row : R.layout.app_grid_item, R.drawable.image_loading, gridScaleFactor);
this.address = address; this.computer = computer;
this.uniqueId = uniqueId; this.uniqueId = uniqueId;
cryptoProvider = PlatformBinding.getCryptoProvider(context); cryptoProvider = PlatformBinding.getCryptoProvider(context);
@@ -117,6 +119,15 @@ 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();
@@ -144,47 +155,24 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
} }
} }
private Bitmap checkBitmapCache(String addrStr, int appId) { private Bitmap checkBitmapCache(int appId) {
File addrFolder = new File(context.getCacheDir(), addrStr); try {
if (addrFolder.isDirectory()) { InputStream in = CacheHelper.openCacheFileForInput(context.getCacheDir(), "boxart", computer.uuid.toString(), appId+".png");
File bitmapFile = new File(addrFolder, appId+".png"); Bitmap bm = BitmapFactory.decodeStream(in);
if (bitmapFile.exists()) { in.close();
InputStream fileIn = null; return bm;
try { } catch (IOException e) {}
fileIn = new BufferedInputStream(new FileInputStream(bitmapFile));
Bitmap bm = BitmapFactory.decodeStream(fileIn);
if (bm == null) {
// The image seems corrupt
bitmapFile.delete();
}
return bm;
} catch (IOException e) {
e.printStackTrace();
bitmapFile.delete();
} finally {
if (fileIn != null) {
try {
fileIn.close();
} catch (IOException ignored) {}
}
}
}
}
return null; return null;
} }
// TODO: Handle pruning of bitmap cache // TODO: Handle pruning of bitmap cache
private void populateBitmapCache(String addrStr, int appId, Bitmap bitmap) { private void populateBitmapCache(UUID uuid, int appId, Bitmap bitmap) {
File addrFolder = new File(context.getCacheDir(), addrStr);
addrFolder.mkdirs();
File bitmapFile = new File(addrFolder, appId+".png");
try { try {
// PNG ignores quality setting // PNG ignores quality setting
bitmap.compress(Bitmap.CompressFormat.PNG, 0, new FileOutputStream(bitmapFile)); FileOutputStream out = CacheHelper.openCacheFileForOutput(context.getCacheDir(), "boxart", uuid.toString(), appId+".png");
} catch (FileNotFoundException e) { bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
out.close();
} catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
@@ -197,10 +185,10 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
Ion.getDefault(imgView.getContext()).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext); Ion.getDefault(imgView.getContext()).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext);
// Check the on-disk cache // Check the on-disk cache
Bitmap cachedBitmap = checkBitmapCache(address.getHostAddress(), obj.app.getAppId()); Bitmap cachedBitmap = checkBitmapCache(obj.app.getAppId());
if (cachedBitmap != null) { if (cachedBitmap != null) {
// Cache hit; we're done // Cache hit; we're done
LimeLog.info("Image cache hit for ("+address.getHostAddress()+", "+obj.app.getAppId()+")"); LimeLog.info("Image cache hit for ("+computer.uuid+", "+obj.app.getAppId()+")");
imgView.setImageBitmap(cachedBitmap); imgView.setImageBitmap(cachedBitmap);
return true; return true;
} }
@@ -210,7 +198,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
Future<ImageViewBitmapInfo> f = Ion.with(imgView) Future<ImageViewBitmapInfo> f = Ion.with(imgView)
.placeholder(defaultImageRes) .placeholder(defaultImageRes)
.error(defaultImageRes) .error(defaultImageRes)
.load("https://" + address.getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" + .load("https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
obj.app.getAppId() + "&AssetType=2&AssetIdx=0") obj.app.getAppId() + "&AssetType=2&AssetIdx=0")
.withBitmapInfo() .withBitmapInfo()
.setCallback( .setCallback(
@@ -225,7 +213,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
if (result != null && if (result != null &&
result.getBitmapInfo() != null && result.getBitmapInfo() != null &&
result.getBitmapInfo().bitmap != null) { result.getBitmapInfo().bitmap != null) {
populateBitmapCache(address.getHostAddress(), obj.app.getAppId(), populateBitmapCache(computer.uuid, obj.app.getAppId(),
result.getBitmapInfo().bitmap); result.getBitmapInfo().bitmap);
} }
} }
@@ -18,11 +18,13 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
protected int layoutId; protected int layoutId;
protected ArrayList<T> itemList = new ArrayList<T>(); protected ArrayList<T> itemList = new ArrayList<T>();
protected LayoutInflater inflater; protected LayoutInflater inflater;
protected double gridSizeFactor;
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) { public GenericGridAdapter(Context context, int layoutId, int defaultImageRes, double gridSizeFactor) {
this.context = context; this.context = context;
this.layoutId = layoutId; this.layoutId = layoutId;
this.defaultImageRes = defaultImageRes; this.defaultImageRes = defaultImageRes;
this.gridSizeFactor = gridSizeFactor;
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
} }
@@ -64,11 +66,26 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
if (!populateImageView(imgView, itemList.get(i))) { if (!populateImageView(imgView, itemList.get(i))) {
imgView.setImageResource(defaultImageRes); imgView.setImageResource(defaultImageRes);
} }
ViewGroup.LayoutParams params = imgView.getLayoutParams();
params.width *= gridSizeFactor;
params.height *= gridSizeFactor;
imgView.setLayoutParams(params);
} }
if (!populateTextView(txtView, itemList.get(i))) { if (!populateTextView(txtView, itemList.get(i))) {
txtView.setText(itemList.get(i).toString()); txtView.setText(itemList.get(i).toString());
ViewGroup.LayoutParams params = txtView.getLayoutParams();
params.width *= gridSizeFactor;
params.height *= gridSizeFactor;
txtView.setLayoutParams(params);
} }
if (overlayView != null) { if (overlayView != null) {
ViewGroup.LayoutParams params = overlayView.getLayoutParams();
params.width *= gridSizeFactor;
params.height *= gridSizeFactor;
overlayView.setLayoutParams(params);
if (!populateOverlayView(overlayView, itemList.get(i))) { if (!populateOverlayView(overlayView, itemList.get(i))) {
overlayView.setVisibility(View.INVISIBLE); overlayView.setVisibility(View.INVISIBLE);
} }
@@ -13,8 +13,8 @@ import java.util.Comparator;
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> { public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
public PcGridAdapter(Context context, boolean listMode) { public PcGridAdapter(Context context, double gridScaleFactor, boolean listMode) {
super(context, listMode ? R.layout.simple_row : R.layout.pc_grid_item, R.drawable.computer); super(context, listMode ? R.layout.simple_row : R.layout.pc_grid_item, R.drawable.computer, gridScaleFactor);
} }
public void addComputer(PcView.ComputerObject computer) { public void addComputer(PcView.ComputerObject computer) {
@@ -15,6 +15,7 @@ public class PreferenceConfiguration {
private static final String DEADZONE_PREF_STRING = "seekbar_deadzone"; private static final String DEADZONE_PREF_STRING = "seekbar_deadzone";
private static final String LANGUAGE_PREF_STRING = "list_languages"; private static final String LANGUAGE_PREF_STRING = "list_languages";
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode"; private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
private static final int BITRATE_DEFAULT_720_30 = 5; private static final int BITRATE_DEFAULT_720_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10; private static final int BITRATE_DEFAULT_720_60 = 10;
@@ -31,6 +32,7 @@ public class PreferenceConfiguration {
private static final int DEFAULT_DEADZONE = 15; private static final int DEFAULT_DEADZONE = 15;
public static final String DEFAULT_LANGUAGE = "default"; public static final String DEFAULT_LANGUAGE = "default";
private static final boolean DEFAULT_LIST_MODE = false; private static final boolean DEFAULT_LIST_MODE = false;
private static final boolean DEFAULT_SMALL_ICON = false;
public static final int FORCE_HARDWARE_DECODER = -1; public static final int FORCE_HARDWARE_DECODER = -1;
public static final int AUTOSELECT_DECODER = 0; public static final int AUTOSELECT_DECODER = 0;
@@ -42,7 +44,7 @@ public class PreferenceConfiguration {
public int deadzonePercentage; public int deadzonePercentage;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings; public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language; public String language;
public boolean listMode; public boolean listMode, smallIconMode;
public static int getDefaultBitrate(String resFpsString) { public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("720p30")) { if (resFpsString.equals("720p30")) {
@@ -149,6 +151,7 @@ public class PreferenceConfiguration {
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH); config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO); config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE); config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, DEFAULT_SMALL_ICON);
return config; return config;
} }
@@ -0,0 +1,55 @@
package com.limelight.utils;
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.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.Scanner;
public class CacheHelper {
private static File openPath(boolean createPath, File root, String... path) {
File f = root;
for (int i = 0; i < path.length; i++) {
String component = path[i];
if (i == path.length - 1) {
// This is the file component so now we create parent directories
if (createPath) {
f.mkdirs();
}
}
f = new File(f, component);
}
return f;
}
public static FileInputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
return new FileInputStream(openPath(false, root, path));
}
public static FileOutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
return new FileOutputStream(openPath(true, root, path));
}
public static String readInputStreamToString(InputStream in) {
Scanner s = new Scanner(in);
StringBuilder sb = new StringBuilder();
while (s.hasNext()) {
sb.append(s.next());
}
return sb.toString();
}
public static void writeStringToOutputStream(OutputStream out, String str) throws IOException {
out.write(str.getBytes("UTF-8"));
}
}
+4 -1
View File
@@ -58,7 +58,8 @@
<string name="searching_pc">Searching for PCs…</string> <string name="searching_pc">Searching for PCs…</string>
<string name="yes">Yes</string> <string name="yes">Yes</string>
<string name="no">No</string> <string name="no">No</string>
<string name="lost_connection">Lost connection to PC</string>
<!-- AppList activity --> <!-- AppList activity -->
<string name="title_applist">Apps on</string> <string name="title_applist">Apps on</string>
<string name="applist_menu_resume">Resume Session</string> <string name="applist_menu_resume">Resume Session</string>
@@ -102,6 +103,8 @@
<string name="summary_language_list">Language to use for Limelight</string> <string name="summary_language_list">Language to use for Limelight</string>
<string name="title_checkbox_list_mode">Use lists instead of grids</string> <string name="title_checkbox_list_mode">Use lists instead of grids</string>
<string name="summary_checkbox_list_mode">Display apps and PCs in lists instead of grids</string> <string name="summary_checkbox_list_mode">Display apps and PCs in lists instead of grids</string>
<string name="title_checkbox_small_icon_mode">Use small icons</string>
<string name="summary_checkbox_small_icon_mode">Use small icons in grid items to allow more items on screen</string>
<string name="category_host_settings">Host Settings</string> <string name="category_host_settings">Host Settings</string>
<string name="title_checkbox_enable_sops">Optimize game settings</string> <string name="title_checkbox_enable_sops">Optimize game settings</string>
+5
View File
@@ -54,6 +54,11 @@
android:entryValues="@array/language_values" android:entryValues="@array/language_values"
android:summary="@string/summary_language_list" android:summary="@string/summary_language_list"
android:defaultValue="default" /> android:defaultValue="default" />
<CheckBoxPreference
android:key="checkbox_small_icon_mode"
android:title="@string/title_checkbox_small_icon_mode"
android:summary="@string/summary_checkbox_small_icon_mode"
android:defaultValue="false" />
<CheckBoxPreference <CheckBoxPreference
android:key="checkbox_list_mode" android:key="checkbox_list_mode"
android:title="@string/title_checkbox_list_mode" android:title="@string/title_checkbox_list_mode"