diff --git a/app/libs/limelight-common.jar b/app/libs/limelight-common.jar index c248abce..63660c37 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/AppView.java b/app/src/main/java/com/limelight/AppView.java index 3568c382..21ccbdf0 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -1,32 +1,45 @@ package com.limelight; import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStreamReader; +import java.io.StringReader; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.List; import java.util.Locale; +import java.util.UUID; import org.xmlpull.v1.XmlPullParserException; 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.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; import com.limelight.ui.AdapterFragment; import com.limelight.ui.AdapterFragmentCallbacks; +import com.limelight.utils.CacheHelper; import com.limelight.utils.Dialog; import com.limelight.utils.SpinnerDialog; import com.limelight.utils.UiHelper; import android.app.Activity; import android.app.AlertDialog; +import android.app.Service; +import android.content.ComponentName; import android.content.DialogInterface; import android.content.Intent; +import android.content.ServiceConnection; import android.content.res.Configuration; import android.os.Bundle; +import android.os.IBinder; import android.view.ContextMenu; import android.view.Menu; import android.view.MenuItem; @@ -35,26 +48,145 @@ import android.view.ContextMenu.ContextMenuInfo; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; +import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; import android.widget.AdapterView.AdapterContextMenuInfo; public class AppView extends Activity implements AdapterFragmentCallbacks { private AppGridAdapter appGridAdapter; - private InetAddress ipAddress; - private String uniqueId; - private boolean remote; - private boolean firstLoad = true; - + private String uuidString; + + private ComputerDetails computer; + 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 QUIT_ID = 2; private final static int CANCEL_ID = 3; private final static int START_WTIH_QUIT = 4; - - public final static String ADDRESS_EXTRA = "Address"; - public final static String UNIQUEID_EXTRA = "UniqueId"; - public final static String NAME_EXTRA = "Name"; - public final static String REMOTE_EXTRA = "Remote"; + + public final static String NAME_EXTRA = "Name"; + public final static String UUID_EXTRA = "UUID"; + + 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 protected void onCreate(Bundle savedInstanceState) { @@ -70,41 +202,35 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { setContentView(R.layout.activity_app_view); UiHelper.notifyNewRootView(this); - - byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA); - uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA); - remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false); - if (address == null || uniqueId == null) { - finish(); - return; - } + + uuidString = getIntent().getStringExtra(UUID_EXTRA); String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA); TextView label = (TextView) findViewById(R.id.appListText); setTitle(labelText); label.setText(labelText); - - try { - ipAddress = InetAddress.getByAddress(address); - } catch (UnknownHostException e) { - e.printStackTrace(); - finish(); - return; - } - try { - appGridAdapter = new AppGridAdapter(this, - PreferenceConfiguration.readPreferences(this).listMode, - ipAddress, uniqueId); - } catch (Exception e) { - e.printStackTrace(); - finish(); - return; - } - - getFragmentManager().beginTransaction() - .add(R.id.appFragmentContainer, new AdapterFragment()).commitAllowingStateLoss(); + // Bind to the computer manager service + bindService(new Intent(this, ComputerManagerService.class), serviceConnection, + Service.BIND_AUTO_CREATE); } + + 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 protected void onDestroy() { @@ -112,18 +238,25 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { SpinnerDialog.closeDialogs(this); Dialog.closeDialogs(); + + if (managerBinder != null) { + unbindService(serviceConnection); + } } @Override protected void onResume() { super.onResume(); - // Display the error message if it was the - // first load, but just kill the activity - // on subsequent errors - updateAppList(firstLoad); - firstLoad = false; + startComputerUpdates(); } + + @Override + protected void onPause() { + super.onPause(); + + stopComputerUpdates(); + } private int getRunningAppId() { int runningAppId = -1; @@ -232,62 +365,62 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { return super.onContextItemSelected(item); } } - - private void updateAppList(final boolean displayError) { - final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title), - getResources().getString(R.string.applist_refresh_msg), true); - new Thread() { - @Override - public void run() { - NvHTTP httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this)); - - try { - final List appList = httpConn.getAppList(); - - AppView.this.runOnUiThread(new Runnable() { - @Override - public void run() { - appGridAdapter.clear(); - for (NvApp app : appList) { - appGridAdapter.addApp(new AppObject(app)); + + private void updateUiWithAppList(final List appList) { + AppView.this.runOnUiThread(new Runnable() { + @Override + public void run() { + boolean updated = false; + + for (NvApp app : appList) { + boolean foundExistingApp = false; + + // Try to update an existing app in the list first + for (int i = 0; i < appGridAdapter.getCount(); i++) { + AppObject existingApp = (AppObject) appGridAdapter.getItem(i); + if (existingApp.app == null) { + continue; + } + + 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(); - } - }); - - // 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(); + foundExistingApp = true; + break; } - }); + } + + if (!foundExistingApp) { + // This app must be new + appGridAdapter.addApp(new AppObject(app)); + updated = true; + } } - } - }.start(); + + if (updated) { + appGridAdapter.notifyDataSetChanged(); + } + } + }); } - + private void doStart(NvApp app) { 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_UNIQUEID, uniqueId); - intent.putExtra(Game.EXTRA_STREAMING_REMOTE, remote); + intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId()); + intent.putExtra(Game.EXTRA_STREAMING_REMOTE, + computer.reachability != ComputerDetails.Reachability.LOCAL); startActivity(intent); } @@ -299,21 +432,26 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { NvHTTP httpConn; String message; 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()) { message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName(); } else { message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName(); } - updateAppList(true); } catch (UnknownHostException e) { message = getResources().getString(R.string.error_unknown_host); } catch (FileNotFoundException e) { message = getResources().getString(R.string.error_404); } catch (Exception e) { message = e.getMessage(); - } + } finally { + // Trigger a poll immediately + if (poller != null) { + poller.pollNow(); + } + } final String toastMessage = message; runOnUiThread(new Runnable() { diff --git a/app/src/main/java/com/limelight/PcView.java b/app/src/main/java/com/limelight/PcView.java index e9dd5a6e..ebf64628 100644 --- a/app/src/main/java/com/limelight/PcView.java +++ b/app/src/main/java/com/limelight/PcView.java @@ -156,7 +156,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE); - pcGridAdapter = new PcGridAdapter(this, + pcGridAdapter = new PcGridAdapter(this, 1.0, PreferenceConfiguration.readPreferences(this).listMode); initializeViews(); @@ -474,16 +474,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { Intent i = new Intent(this, AppView.class); i.putExtra(AppView.NAME_EXTRA, computer.name); - i.putExtra(AppView.UNIQUEID_EXTRA, managerBinder.getUniqueId()); - - 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); - } + i.putExtra(AppView.UUID_EXTRA, computer.uuid.toString()); startActivity(i); } diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index afafbe58..9af8808b 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -1,7 +1,11 @@ package com.limelight.computers; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; import java.net.InetAddress; import java.util.LinkedList; +import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import com.limelight.LimeLog; @@ -11,6 +15,7 @@ import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.mdns.MdnsComputer; import com.limelight.nvstream.mdns.MdnsDiscoveryListener; +import com.limelight.utils.CacheHelper; import android.app.Service; import android.content.ComponentName; @@ -179,10 +184,26 @@ public class ComputerManagerService extends Service { // Just call the unbind handler to cleanup ComputerManagerService.this.onUnbind(null); } + + public ApplistPoller createAppListPoller(ComputerDetails computer) { + return new ApplistPoller(computer); + } public String 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 @@ -462,6 +483,98 @@ public class ComputerManagerService extends Service { public IBinder onBind(Intent intent) { 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 { diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java index fb8476a6..481ae2be 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -13,7 +13,9 @@ import com.limelight.AppView; import com.limelight.LimeLog; import com.limelight.R; import com.limelight.binding.PlatformBinding; +import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.LimelightCryptoProvider; +import com.limelight.utils.CacheHelper; import java.io.BufferedInputStream; import java.io.File; @@ -32,6 +34,7 @@ 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; @@ -45,17 +48,16 @@ import java.security.cert.X509Certificate; public class AppGridAdapter extends GenericGridAdapter { - private boolean listMode; - private InetAddress address; + private ComputerDetails computer; private String uniqueId; private LimelightCryptoProvider cryptoProvider; private SSLContext sslContext; private final HashMap pendingRequests = new HashMap(); - public AppGridAdapter(Context context, boolean listMode, InetAddress address, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException { - super(context, listMode ? R.layout.simple_row : R.layout.app_grid_item, R.drawable.image_loading); + 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, gridScaleFactor); - this.address = address; + this.computer = computer; this.uniqueId = uniqueId; cryptoProvider = PlatformBinding.getCryptoProvider(context); @@ -117,6 +119,15 @@ 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(); @@ -144,47 +155,24 @@ public class AppGridAdapter extends GenericGridAdapter { } } - private Bitmap checkBitmapCache(String addrStr, int appId) { - File addrFolder = new File(context.getCacheDir(), addrStr); - if (addrFolder.isDirectory()) { - File bitmapFile = new File(addrFolder, appId+".png"); - if (bitmapFile.exists()) { - InputStream fileIn = null; - try { - 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) {} - } - } - } - } - + private Bitmap checkBitmapCache(int appId) { + try { + InputStream in = CacheHelper.openCacheFileForInput(context.getCacheDir(), "boxart", computer.uuid.toString(), appId+".png"); + Bitmap bm = BitmapFactory.decodeStream(in); + in.close(); + return bm; + } catch (IOException e) {} return null; } // TODO: Handle pruning of bitmap cache - private void populateBitmapCache(String addrStr, int appId, Bitmap bitmap) { - File addrFolder = new File(context.getCacheDir(), addrStr); - addrFolder.mkdirs(); - - File bitmapFile = new File(addrFolder, appId+".png"); + private void populateBitmapCache(UUID uuid, int appId, Bitmap bitmap) { try { // PNG ignores quality setting - bitmap.compress(Bitmap.CompressFormat.PNG, 0, new FileOutputStream(bitmapFile)); - } catch (FileNotFoundException e) { + 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(); } } @@ -197,10 +185,10 @@ public class AppGridAdapter extends GenericGridAdapter { Ion.getDefault(imgView.getContext()).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext); // Check the on-disk cache - Bitmap cachedBitmap = checkBitmapCache(address.getHostAddress(), obj.app.getAppId()); + Bitmap cachedBitmap = checkBitmapCache(obj.app.getAppId()); if (cachedBitmap != null) { // 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); return true; } @@ -210,7 +198,7 @@ public class AppGridAdapter extends GenericGridAdapter { Future f = Ion.with(imgView) .placeholder(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") .withBitmapInfo() .setCallback( @@ -225,7 +213,7 @@ public class AppGridAdapter extends GenericGridAdapter { if (result != null && result.getBitmapInfo() != null && result.getBitmapInfo().bitmap != null) { - populateBitmapCache(address.getHostAddress(), obj.app.getAppId(), + populateBitmapCache(computer.uuid, obj.app.getAppId(), result.getBitmapInfo().bitmap); } } diff --git a/app/src/main/java/com/limelight/grid/GenericGridAdapter.java b/app/src/main/java/com/limelight/grid/GenericGridAdapter.java index f0a17618..b6a416b6 100644 --- a/app/src/main/java/com/limelight/grid/GenericGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/GenericGridAdapter.java @@ -18,11 +18,13 @@ public abstract class GenericGridAdapter extends BaseAdapter { protected int layoutId; protected ArrayList itemList = new ArrayList(); 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.layoutId = layoutId; this.defaultImageRes = defaultImageRes; + this.gridSizeFactor = gridSizeFactor; this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } @@ -64,11 +66,26 @@ public abstract class GenericGridAdapter extends BaseAdapter { if (!populateImageView(imgView, itemList.get(i))) { imgView.setImageResource(defaultImageRes); } + + ViewGroup.LayoutParams params = imgView.getLayoutParams(); + params.width *= gridSizeFactor; + params.height *= gridSizeFactor; + imgView.setLayoutParams(params); } if (!populateTextView(txtView, itemList.get(i))) { txtView.setText(itemList.get(i).toString()); + + ViewGroup.LayoutParams params = txtView.getLayoutParams(); + params.width *= gridSizeFactor; + params.height *= gridSizeFactor; + txtView.setLayoutParams(params); } if (overlayView != null) { + ViewGroup.LayoutParams params = overlayView.getLayoutParams(); + params.width *= gridSizeFactor; + params.height *= gridSizeFactor; + overlayView.setLayoutParams(params); + if (!populateOverlayView(overlayView, itemList.get(i))) { overlayView.setVisibility(View.INVISIBLE); } diff --git a/app/src/main/java/com/limelight/grid/PcGridAdapter.java b/app/src/main/java/com/limelight/grid/PcGridAdapter.java index 8e370de3..b2a63cf1 100644 --- a/app/src/main/java/com/limelight/grid/PcGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/PcGridAdapter.java @@ -13,8 +13,8 @@ import java.util.Comparator; public class PcGridAdapter extends GenericGridAdapter { - public PcGridAdapter(Context context, boolean listMode) { - super(context, listMode ? R.layout.simple_row : R.layout.pc_grid_item, R.drawable.computer); + public PcGridAdapter(Context context, double gridScaleFactor, boolean listMode) { + super(context, listMode ? R.layout.simple_row : R.layout.pc_grid_item, R.drawable.computer, gridScaleFactor); } public void addComputer(PcView.ComputerObject computer) { diff --git a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java index 0d850a0e..bcc8c00c 100644 --- a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java +++ b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java @@ -15,6 +15,7 @@ public class PreferenceConfiguration { private static final String DEADZONE_PREF_STRING = "seekbar_deadzone"; private static final String LANGUAGE_PREF_STRING = "list_languages"; 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_60 = 10; @@ -31,6 +32,7 @@ public class PreferenceConfiguration { private static final int DEFAULT_DEADZONE = 15; public static final String DEFAULT_LANGUAGE = "default"; 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 AUTOSELECT_DECODER = 0; @@ -42,7 +44,7 @@ public class PreferenceConfiguration { public int deadzonePercentage; public boolean stretchVideo, enableSops, playHostAudio, disableWarnings; public String language; - public boolean listMode; + public boolean listMode, smallIconMode; public static int getDefaultBitrate(String resFpsString) { if (resFpsString.equals("720p30")) { @@ -149,6 +151,7 @@ public class PreferenceConfiguration { config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH); config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO); config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE); + config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, DEFAULT_SMALL_ICON); return config; } diff --git a/app/src/main/java/com/limelight/utils/CacheHelper.java b/app/src/main/java/com/limelight/utils/CacheHelper.java new file mode 100644 index 00000000..b22bee93 --- /dev/null +++ b/app/src/main/java/com/limelight/utils/CacheHelper.java @@ -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")); + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2b9e1a68..d5258ef4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -58,7 +58,8 @@ Searching for PCs… Yes No - + Lost connection to PC + Apps on Resume Session @@ -102,6 +103,8 @@ Language to use for Limelight Use lists instead of grids Display apps and PCs in lists instead of grids + Use small icons + Use small icons in grid items to allow more items on screen Host Settings Optimize game settings diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 44f93e74..4e505719 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -54,6 +54,11 @@ android:entryValues="@array/language_values" android:summary="@string/summary_language_list" android:defaultValue="default" /> +