diff --git a/app/app.iml b/app/app.iml index 4a3b7889..f7cfabed 100644 --- a/app/app.iml +++ b/app/app.iml @@ -8,10 +8,12 @@ - - + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + @@ -51,13 +59,13 @@ - - - - - - - + + + + + + + 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 ec46a78b..4a8cb3b1 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.FragmentManager; +import android.app.AlertDialog; +import android.app.Service; +import android.content.ComponentName; +import android.content.DialogInterface; import android.content.Intent; -import android.content.SharedPreferences; +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,33 +48,155 @@ import android.view.ContextMenu.ContextMenuInfo; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; -import android.widget.GridView; -import android.widget.ListView; +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 final static int RESUME_ID = 1; + private AppGridAdapter appGridAdapter; + 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; - - public final static String ADDRESS_EXTRA = "Address"; - public final static String UNIQUEID_EXTRA = "UniqueId"; + private final static int START_WTIH_QUIT = 4; + public final static String NAME_EXTRA = "Name"; - public final static String REMOTE_EXTRA = "Remote"; - + 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)); + + try { + appGridAdapter = new AppGridAdapter(AppView.this, + PreferenceConfiguration.readPreferences(AppView.this).listMode, + PreferenceConfiguration.readPreferences(AppView.this).smallIconMode, + computer, managerBinder.getUniqueId()); + } catch (Exception e) { + e.printStackTrace(); + finish(); + return; + } + + // Start updates + startComputerUpdates(); + + // Load the app grid with cached data (if possible) + populateAppGridWithCache(); + + getFragmentManager().beginTransaction() + .replace(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; + } + + consecutiveAppListFailures = 0; + + // 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) { super.onCreate(savedInstanceState); - String locale = PreferenceConfiguration.readPreferences(this).language; + String locale = PreferenceConfiguration.readPreferences(this).language; if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) { Configuration config = new Configuration(getResources().getConfiguration()); config.locale = new Locale(locale); @@ -70,185 +205,235 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { setContentView(R.layout.activity_app_view); - UiHelper.notifyNewRootView(this); + UiHelper.notifyNewRootView(this); + + uuidString = getIntent().getStringExtra(UUID_EXTRA); - byte[] address = getIntent().getByteArrayExtra(ADDRESS_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); 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 + lastRawApplist = CacheHelper.readInputStreamToString(CacheHelper.openCacheFileForInput(getCacheDir(), "applist", uuidString)); + List applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist)); + updateUiWithAppList(applist); + LimeLog.info("Loaded applist from cache"); + } catch (Exception e) { + if (lastRawApplist != null) { + LimeLog.warning("Saved applist corrupted: "+lastRawApplist); + e.printStackTrace(); + } + 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() { super.onDestroy(); - + 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; - for (int i = 0; i < appGridAdapter.getCount(); i++) { - AppObject app = (AppObject) appGridAdapter.getItem(i); - if (app.app == null) { - continue; - } - - if (app.app.getIsRunning()) { - runningAppId = app.app.getAppId(); - break; - } - } - return runningAppId; + int runningAppId = -1; + for (int i = 0; i < appGridAdapter.getCount(); i++) { + AppObject app = (AppObject) appGridAdapter.getItem(i); + if (app.app == null) { + continue; + } + + if (app.app.getIsRunning()) { + runningAppId = app.app.getAppId(); + break; + } + } + return runningAppId; } - + @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); - - AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; - AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position); - if (selectedApp == null || selectedApp.app == null) { - return; - } - - int runningAppId = getRunningAppId(); - if (runningAppId != -1) { - if (runningAppId == selectedApp.app.getAppId()) { - menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume)); - menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit)); - } - else { - menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_quit_and_start)); - menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel)); - } - } - } - + + AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; + AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position); + if (selectedApp == null || selectedApp.app == null) { + return; + } + + int runningAppId = getRunningAppId(); + if (runningAppId != -1) { + if (runningAppId == selectedApp.app.getAppId()) { + menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume)); + menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit)); + } + else { + menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start)); + menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel)); + } + } + } + @Override public void onContextMenuClosed(Menu menu) { } - @Override - public boolean onContextItemSelected(MenuItem item) { - AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); - AppObject app = (AppObject) appGridAdapter.getItem(info.position); - switch (item.getItemId()) { - case RESUME_ID: - // Resume is the same as start for us - doStart(app.app); - return true; + private void displayQuitConfirmationDialog(final Runnable onYes, final Runnable onNo) { + DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which){ + case DialogInterface.BUTTON_POSITIVE: + if (onYes != null) { + onYes.run(); + } + break; - case QUIT_ID: - doQuit(app.app); - return true; + case DialogInterface.BUTTON_NEGATIVE: + if (onNo != null) { + onNo.run(); + } + break; + } + } + }; - case CANCEL_ID: - return true; + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(getResources().getString(R.string.applist_quit_confirmation)) + .setPositiveButton(getResources().getString(R.string.yes), dialogClickListener) + .setNegativeButton(getResources().getString(R.string.no), dialogClickListener) + .show(); + } - default: - 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 boolean onContextItemSelected(MenuItem item) { + AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); + final AppObject app = (AppObject) appGridAdapter.getItem(info.position); + switch (item.getItemId()) { + case START_WTIH_QUIT: + // Display a confirmation dialog first + displayQuitConfirmationDialog(new Runnable() { + @Override + public void run() { + doStart(app.app); + } + }, null); + return true; + + case START_OR_RESUME_ID: + // Resume is the same as start for us + doStart(app.app); + return true; + + case QUIT_ID: + // Display a confirmation dialog first + displayQuitConfirmationDialog(new Runnable() { + @Override + public void run() { + doQuit(app.app); + } + }, null); + return true; + + case CANCEL_ID: + return true; + + default: + return super.onContextItemSelected(item); + } + } + + private void updateUiWithAppList(final List appList) { + AppView.this.runOnUiThread(new Runnable() { @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)); - } + boolean updated = false; - appGridAdapter.notifyDataSetChanged(); + 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; } - }); - - // Success case - return; - } catch (GfeHttpResponseException ignored) { - } catch (IOException ignored) { - } catch (XmlPullParserException ignored) { - } finally { - spinner.dismiss(); + + 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; + } + + foundExistingApp = true; + break; + } + } + + if (!foundExistingApp) { + // This app must be new + appGridAdapter.addApp(new AppObject(app)); + updated = true; + } } - 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 (updated) { + appGridAdapter.notifyDataSetChanged(); + } } - }.start(); - } - + }); + } + 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); } - + private void doQuit(final NvApp app) { Toast.makeText(AppView.this, getResources().getString(R.string.applist_quit_app)+" "+app.getAppName()+"...", Toast.LENGTH_SHORT).show(); new Thread(new Runnable() { @@ -257,22 +442,27 @@ 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() { @Override @@ -284,45 +474,46 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { }).start(); } - @Override - public int getAdapterFragmentLayoutId() { - return PreferenceConfiguration.readPreferences(this).listMode ? - R.layout.list_view : R.layout.app_grid_view; - } + @Override + public int getAdapterFragmentLayoutId() { + return PreferenceConfiguration.readPreferences(this).listMode ? + R.layout.list_view : (PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ? + R.layout.app_grid_view_small : R.layout.app_grid_view); + } - @Override - public void receiveAbsListView(AbsListView listView) { - listView.setAdapter(appGridAdapter); - listView.setOnItemClickListener(new OnItemClickListener() { - @Override - public void onItemClick(AdapterView arg0, View arg1, int pos, - long id) { - AppObject app = (AppObject) appGridAdapter.getItem(pos); - if (app == null || app.app == null) { - return; - } + @Override + public void receiveAbsListView(AbsListView listView) { + listView.setAdapter(appGridAdapter); + listView.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView arg0, View arg1, int pos, + long id) { + AppObject app = (AppObject) appGridAdapter.getItem(pos); + if (app == null || app.app == null) { + return; + } - // Only open the context menu if something is running, otherwise start it - if (getRunningAppId() != -1) { - openContextMenu(arg1); - } else { - doStart(app.app); - } - } - }); - registerForContextMenu(listView); - } + // Only open the context menu if something is running, otherwise start it + if (getRunningAppId() != -1) { + openContextMenu(arg1); + } else { + doStart(app.app); + } + } + }); + registerForContextMenu(listView); + } - public class AppObject { + public class AppObject { public NvApp app; - + public AppObject(NvApp app) { this.app = app; } - + @Override public String toString() { return app.getAppName(); } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index 90c58042..fbb1318a 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -24,7 +24,6 @@ import com.limelight.utils.SpinnerDialog; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; -import android.content.SharedPreferences; import android.content.res.Configuration; import android.graphics.Point; import android.media.AudioManager; @@ -33,7 +32,6 @@ import android.net.wifi.WifiManager; import android.os.Bundle; import android.os.Handler; import android.os.SystemClock; -import android.preference.PreferenceManager; import android.view.Display; import android.view.InputDevice; import android.view.KeyEvent; diff --git a/app/src/main/java/com/limelight/PcView.java b/app/src/main/java/com/limelight/PcView.java index 8f60d8e6..76b44642 100644 --- a/app/src/main/java/com/limelight/PcView.java +++ b/app/src/main/java/com/limelight/PcView.java @@ -30,7 +30,6 @@ import android.app.Service; import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; -import android.content.SharedPreferences; import android.content.res.Configuration; import android.os.Bundle; import android.os.IBinder; @@ -44,15 +43,12 @@ import android.view.View.OnClickListener; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; -import android.widget.GridView; import android.widget.ImageButton; -import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.Toast; import android.widget.AdapterView.AdapterContextMenuInfo; public class PcView extends Activity implements AdapterFragmentCallbacks { - private AdapterFragment adapterFragment; private RelativeLayout noPcFoundLayout; private PcGridAdapter pcGridAdapter; private ComputerManagerService.ComputerManagerBinder managerBinder; @@ -125,14 +121,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { } }); - FragmentTransaction transaction = getFragmentManager().beginTransaction(); - if (adapterFragment != null) { - // Remove the old fragment - transaction.remove(adapterFragment); - } - adapterFragment = new AdapterFragment(); - transaction.add(R.id.pcFragmentContainer, adapterFragment); - transaction.commitAllowingStateLoss(); + getFragmentManager().beginTransaction() + .replace(R.id.pcFragmentContainer, new AdapterFragment()) + .commitAllowingStateLoss(); noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout); if (pcGridAdapter.getCount() == 0) { @@ -160,7 +151,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { Service.BIND_AUTO_CREATE); pcGridAdapter = new PcGridAdapter(this, - PreferenceConfiguration.readPreferences(this).listMode); + PreferenceConfiguration.readPreferences(this).listMode, + PreferenceConfiguration.readPreferences(this).smallIconMode); initializeViews(); } @@ -477,16 +469,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); } @@ -570,7 +553,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { @Override public int getAdapterFragmentLayoutId() { return PreferenceConfiguration.readPreferences(this).listMode ? - R.layout.list_view : R.layout.pc_grid_view; + R.layout.list_view : (PreferenceConfiguration.readPreferences(this).smallIconMode ? + R.layout.pc_grid_view_small : R.layout.pc_grid_view); } @Override diff --git a/app/src/main/java/com/limelight/binding/input/virtual_controller/AnalogStick.java b/app/src/main/java/com/limelight/binding/input/virtual_controller/AnalogStick.java index 7383e474..13bbafc3 100644 --- a/app/src/main/java/com/limelight/binding/input/virtual_controller/AnalogStick.java +++ b/app/src/main/java/com/limelight/binding/input/virtual_controller/AnalogStick.java @@ -6,7 +6,6 @@ import android.graphics.Color; import android.graphics.Paint; import android.view.MotionEvent; -import java.lang.reflect.Array; import java.util.ArrayList; import java.util.List; @@ -15,25 +14,27 @@ import java.util.List; */ public class AnalogStick extends VirtualControllerElement { - protected static boolean _PRINT_DEBUG_INFORMATION = true; + float radius_complete = 0; + float radius_minimum = 0; + float radius_dead_zone = 0; + float radius_analog_stick = 0; - float radius_complete = 0; - float radius_dead_zone = 0; - float radius_analog_stick = 0; + float position_pressed_x = 0; + float position_pressed_y = 0; - float position_pressed_x = 0; - float position_pressed_y = 0; + float position_moved_x = 0; + float position_moved_y = 0; - float position_stick_x = 0; - float position_stick_y = 0; + float position_stick_x = 0; + float position_stick_y = 0; + + Paint paint = new Paint(); - boolean viewPressed = false; - boolean analogStickActive = false; _STICK_STATE stick_state = _STICK_STATE.NO_MOVEMENT; _CLICK_STATE click_state = _CLICK_STATE.SINGLE; - List listeners = new ArrayList(); + List listeners = new ArrayList<>(); OnTouchListener onTouchListener = null; private long timeoutDoubleClick = 250; private long timeLastClick = 0; @@ -80,9 +81,10 @@ public class AnalogStick extends VirtualControllerElement @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { - radius_complete = getPercent(getCorrectWidth() / 2, 40); - radius_dead_zone = getPercent(getCorrectWidth() / 2, 20); - radius_analog_stick = getPercent(getCorrectWidth() / 2, 20); + radius_complete = getPercent(getCorrectWidth() / 2, 90); + radius_minimum = getPercent(getCorrectWidth() / 2, 30); + radius_dead_zone = getPercent(getCorrectWidth() / 2, 10); + radius_analog_stick = getPercent(getCorrectWidth() / 2, 20); super.onSizeChanged(w, h, oldw, oldh); } @@ -93,12 +95,11 @@ public class AnalogStick extends VirtualControllerElement // set transparent background canvas.drawColor(Color.TRANSPARENT); - Paint paint = new Paint(); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(getPercent(getCorrectWidth() / 2, 2)); // draw outer circle - if (!viewPressed || click_state == _CLICK_STATE.SINGLE) + if (!isPressed() || click_state == _CLICK_STATE.SINGLE) { paint.setColor(normalColor); } @@ -107,47 +108,36 @@ public class AnalogStick extends VirtualControllerElement paint.setColor(pressedColor); } - canvas.drawRect(0, 0, - getWidth(), getHeight(), - paint); + canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_complete, paint); paint.setColor(normalColor); - - // draw dead zone - if (analogStickActive) - { - canvas.drawCircle(position_pressed_x, position_pressed_y, radius_dead_zone, paint); - } - else - { - canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_dead_zone, paint); - } + // draw minimum + canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_minimum, paint); // draw stick depending on state (no movement, moved, active(out of dead zone)) - if (analogStickActive) + switch (stick_state) { - switch (stick_state) + case NO_MOVEMENT: { - case NO_MOVEMENT: - { - paint.setColor(normalColor); - canvas.drawCircle(position_stick_x, position_stick_y, radius_analog_stick, paint); + paint.setColor(normalColor); + canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_analog_stick, paint); - break; - } - case MOVED: - { - paint.setColor(pressedColor); - canvas.drawCircle(position_stick_x, position_stick_y, radius_analog_stick, paint); - - break; - } + break; + } + case MOVED_IN_DEAD_ZONE: + { + paint.setColor(normalColor); + canvas.drawCircle(position_stick_x, position_stick_y, radius_analog_stick, paint); + + break; + } + case MOVED_ACTIVE: + { + paint.setColor(pressedColor); + canvas.drawCircle(position_stick_x, position_stick_y, radius_analog_stick, paint); + + break; } - } - else - { - paint.setColor(normalColor); - canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_analog_stick, paint); } super.onDraw(canvas); @@ -256,116 +246,79 @@ public class AnalogStick extends VirtualControllerElement } } - private void updatePosition(float x, float y) + private void updatePosition() { - float way_x; - float way_y; + // get real way for each axis + float way_center_x = -(getWidth() / 2 - position_moved_x); + float way_center_y = -(getHeight() / 2 - position_moved_y); - if (x > position_pressed_x) - { - way_x = x - position_pressed_x; + // get radius and angel of movement from center + double movement_radius = getMovementRadius(way_center_x, way_center_y); + double movement_angle = getAngle(way_center_x, way_center_y); - if (way_x > radius_complete) - { - way_x = radius_complete; - } - } - else - { - way_x = -(position_pressed_x - x); + // get dead zone way for each axis + float way_pressed_x = position_pressed_x - position_moved_x; + float way_pressed_y = position_pressed_y - position_moved_y; - if (way_x < -radius_complete) - { - way_x = -radius_complete; - } - } + // get radius and angel from pressed position + double movement_dead_zone_radius = getMovementRadius(way_pressed_x, way_pressed_y); - if (y > position_pressed_y) - { - way_y = y - position_pressed_y; - - if (way_y > radius_complete) - { - way_y = radius_complete; - } - } - else - { - way_y = -(position_pressed_y - y); - - if (way_y < -radius_complete) - { - way_y = -radius_complete; - } - } - - float movement_x = 0; - float movement_y = 0; - - double movement_radius = getMovementRadius(way_x, way_y); - //double movement_angle = getAngle(way_x, way_y); - - /* // chop radius if out of outer circle if (movement_radius > (radius_complete - radius_analog_stick)) { movement_radius = radius_complete - radius_analog_stick; } + // calculate new positions float correlated_y = (float) (Math.sin(Math.PI / 2 - movement_angle) * (movement_radius)); float correlated_x = (float) (Math.cos(Math.PI / 2 - movement_angle) * (movement_radius)); - float complete = (radius_complete - radius_analog_stick); + float complete = (radius_complete - radius_analog_stick - radius_minimum); - movement_x = -(1 / complete) * correlated_x; - movement_y = (1 / complete) * correlated_y; + float movement_x; + float movement_y; - */ + movement_x = -(1 / complete) * (correlated_x - (correlated_x > 0 ? radius_minimum : -radius_minimum)); + movement_y = (1 / complete) * (correlated_y - (correlated_y > 0 ? radius_minimum : -radius_minimum)); - movement_x = (1 / radius_complete) * way_x; - movement_y = -(1 / radius_complete) * way_y; + position_stick_x = getWidth() / 2 - correlated_x; + position_stick_y = getHeight() / 2 - correlated_y; - position_stick_x = position_pressed_x + way_x; - position_stick_y = position_pressed_y + way_y; + // check if analog stick is outside of dead zone and minimum + if (movement_radius > radius_minimum && movement_dead_zone_radius > radius_dead_zone) + { + // set active + stick_state = _STICK_STATE.MOVED_ACTIVE; + } - // check if analog stick is outside of dead zone - if (movement_radius > radius_dead_zone) + if (stick_state == _STICK_STATE.MOVED_ACTIVE) { moveActionCallback(movement_x, movement_y); - - stick_state = _STICK_STATE.MOVED; - } - else - { - stick_state = _STICK_STATE.NO_MOVEMENT; } } @Override public boolean onTouchEvent(MotionEvent event) { - if (onTouchListener != null) - { - return onTouchListener.onTouch(this, event); - } - // get masked (not specific to a pointer) action int action = event.getActionMasked(); _CLICK_STATE lastClickState = click_state; - boolean wasActive = analogStickActive; + + position_moved_x = event.getX(); + position_moved_y = event.getY(); switch (action) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: { - position_pressed_x = event.getX(); - position_pressed_y = event.getY(); + setPressed(true); + position_pressed_x = position_moved_x; + position_pressed_y = position_moved_y; + stick_state = _STICK_STATE.MOVED_IN_DEAD_ZONE; - analogStickActive = true; - viewPressed = true; // check for double click if (lastClickState == _CLICK_STATE.SINGLE && timeLastClick + timeoutDoubleClick > System.currentTimeMillis()) { @@ -384,15 +337,11 @@ public class AnalogStick extends VirtualControllerElement break; } - case MotionEvent.ACTION_MOVE: - { - break; - } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: { - analogStickActive = false; - viewPressed = false; + setPressed(false); + stick_state = _STICK_STATE.NO_MOVEMENT; revokeActionCallback(); @@ -400,13 +349,12 @@ public class AnalogStick extends VirtualControllerElement } } - // no longer pressed reset movement - if (analogStickActive) + if (isPressed()) { // when is pressed calculate new positions (will trigger movement if necessary) - updatePosition(event.getX(), event.getY()); + updatePosition(); } - else if (wasActive) - { + else + { // not longer pressed reset analog stick moveActionCallback(0, 0); } @@ -419,7 +367,8 @@ public class AnalogStick extends VirtualControllerElement private enum _STICK_STATE { NO_MOVEMENT, - MOVED + MOVED_IN_DEAD_ZONE, + MOVED_ACTIVE } diff --git a/app/src/main/java/com/limelight/binding/input/virtual_controller/DigitalButton.java b/app/src/main/java/com/limelight/binding/input/virtual_controller/DigitalButton.java index 7c012c9c..dc8d36ee 100644 --- a/app/src/main/java/com/limelight/binding/input/virtual_controller/DigitalButton.java +++ b/app/src/main/java/com/limelight/binding/input/virtual_controller/DigitalButton.java @@ -17,19 +17,97 @@ import java.util.TimerTask; */ public class DigitalButton extends VirtualControllerElement { - List listeners = new ArrayList(); + static List allButtonsList = new ArrayList<>(); + + List listeners = new ArrayList<>(); OnTouchListener onTouchListener = null; - boolean clicked; private String text = ""; private int icon = -1; private long timerLongClickTimeout = 3000; private Timer timerLongClick = null; private TimerLongClickTimerTask longClickTimerTask = null; - public DigitalButton(Context context) + private int layer; + private DigitalButton movingButton = null; + + boolean inRange(float x, float y) + { + return (this.getX() < x && this.getX() + this.getWidth() > x) && + (this.getY() < y && this.getY() + this.getHeight() > y); + } + + public boolean checkMovement(float x, float y, DigitalButton movingButton) + { + // check if the movement happened in the same layer + if (movingButton.layer != this.layer) + { + return false; + } + + // save current pressed state + boolean wasPressed = isPressed(); + + // check if the movement directly happened on the button + if ((this.movingButton == null || movingButton == this.movingButton) + && this.inRange(x, y)) + { + // set button pressed state depending on moving button pressed state + if (this.isPressed() != movingButton.isPressed()) + { + this.setPressed(movingButton.isPressed()); + } + } + // check if the movement is outside of the range and the movement button + // is saved moving button + else if (movingButton == this.movingButton) + { + this.setPressed(false); + } + + // check if a change occurred + if (wasPressed != isPressed()) + { + + if (isPressed()) + { // is pressed set moving button and emit click event + this.movingButton = movingButton; + + onClickCallback(); + } + + else + { // no longer pressed reset moving button and emit release event + this.movingButton = null; + + onReleaseCallback(); + } + + invalidate(); + + return true; + } + + return false; + } + + private void checkMovementForAllButtons(float x, float y) + { + for (DigitalButton button : allButtonsList) + { + if (button != this) + { + button.checkMovement(x, y, this); + } + } + } + + public DigitalButton(int layer, Context context) { super(context); - clicked = false; + + this.layer = layer; + + allButtonsList.add(this); } public void addDigitalButtonListener(DigitalButtonListener listener) @@ -66,10 +144,10 @@ public class DigitalButton extends VirtualControllerElement paint.setTextAlign(Paint.Align.CENTER); paint.setStrokeWidth(3); - paint.setColor(clicked ? pressedColor : normalColor); + paint.setColor(isPressed() ? pressedColor : normalColor); paint.setStyle(Paint.Style.STROKE); canvas.drawRect( - 1, 1, + 1, 1, getWidth() - 1, getHeight() - 1, paint ); @@ -142,6 +220,9 @@ public class DigitalButton extends VirtualControllerElement } */ // get masked (not specific to a pointer) action + + float x = getX() + event.getX(); + float y = getY() + event.getY(); int action = event.getActionMasked(); switch (action) @@ -149,20 +230,29 @@ public class DigitalButton extends VirtualControllerElement case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: { - clicked = true; + movingButton = null; + setPressed(true); onClickCallback(); invalidate(); return true; } + case MotionEvent.ACTION_MOVE: + { + checkMovementForAllButtons(x, y); + + return true; + } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: { - clicked = false; + setPressed(false); onReleaseCallback(); + checkMovementForAllButtons(x, y); + invalidate(); return true; diff --git a/app/src/main/java/com/limelight/binding/input/virtual_controller/VirtualController.java b/app/src/main/java/com/limelight/binding/input/virtual_controller/VirtualController.java index 87c9a2a7..023cca83 100644 --- a/app/src/main/java/com/limelight/binding/input/virtual_controller/VirtualController.java +++ b/app/src/main/java/com/limelight/binding/input/virtual_controller/VirtualController.java @@ -10,6 +10,9 @@ import com.limelight.R; import com.limelight.nvstream.NvConnection; import com.limelight.nvstream.input.ControllerPacket; +import java.util.ArrayList; +import java.util.List; + /** * Created by Karim Mreisi on 30.11.2014. */ @@ -135,7 +138,7 @@ public class VirtualController buttonA = createDigitalButton("A", ControllerPacket.A_FLAG, context); buttonB = createDigitalButton("B", ControllerPacket.B_FLAG, context); - buttonLT = new DigitalButton(context); + buttonLT = new DigitalButton(2, context); buttonLT.setText("LT"); buttonLT.addDigitalButtonListener(new DigitalButton.DigitalButtonListener() { @@ -162,7 +165,7 @@ public class VirtualController } }); - buttonRT = new DigitalButton(context); + buttonRT = new DigitalButton(2, context); buttonRT.setText("RT"); buttonRT.addDigitalButtonListener(new DigitalButton.DigitalButtonListener() { @@ -267,7 +270,7 @@ public class VirtualController buttonSelect = createDigitalButton("SELECT", ControllerPacket.SPECIAL_BUTTON_FLAG, context); - buttonConfigure = new DigitalButton(context); + buttonConfigure = new DigitalButton(1, context); buttonConfigure.setIcon(R.drawable.settings); buttonConfigure.addDigitalButtonListener(new DigitalButton.DigitalButtonListener() { @@ -412,7 +415,7 @@ public class VirtualController private DigitalButton createDigitalButton(String text, final int key, Context context) { - DigitalButton button = new DigitalButton(context); + DigitalButton button = new DigitalButton(1, context); button.setText(text); button.addDigitalButtonListener(new DigitalButton.DigitalButtonListener() { diff --git a/app/src/main/java/com/limelight/binding/input/virtual_controller/VirtualControllerElement.java b/app/src/main/java/com/limelight/binding/input/virtual_controller/VirtualControllerElement.java index 72e30c32..f0476069 100644 --- a/app/src/main/java/com/limelight/binding/input/virtual_controller/VirtualControllerElement.java +++ b/app/src/main/java/com/limelight/binding/input/virtual_controller/VirtualControllerElement.java @@ -21,7 +21,7 @@ public abstract class VirtualControllerElement extends View { if (_PRINT_DEBUG_INFORMATION) { - System.out.println("DigitalButton: " + text); + System.out.println(text); } } diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index afafbe58..3112399b 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -1,16 +1,24 @@ package com.limelight.computers; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.StringReader; import java.net.InetAddress; import java.util.LinkedList; +import java.util.List; +import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import com.limelight.LimeLog; import com.limelight.binding.PlatformBinding; import com.limelight.discovery.DiscoveryService; import com.limelight.nvstream.http.ComputerDetails; +import com.limelight.nvstream.http.NvApp; 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; @@ -19,6 +27,8 @@ import android.content.ServiceConnection; import android.os.Binder; import android.os.IBinder; +import org.xmlpull.v1.XmlPullParserException; + public class ComputerManagerService extends Service { private static final int POLLING_PERIOD_MS = 3000; private static final int MDNS_QUERY_PERIOD_MS = 1000; @@ -179,10 +189,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 +488,107 @@ 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 != null && !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) { + if (listener != null) { + 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(); + List list = NvHTTP.getAppListByReader(new StringReader(appList)); + if (appList != null && !appList.isEmpty() && !list.isEmpty()) { + // Open the cache file + 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 + // and ensure that the thread is still active + if (listener != null && thread != null) { + listener.notifyComputerUpdated(computer); + } + } + else { + LimeLog.warning("Empty app list received from "+computer.uuid); + } + } catch (IOException e) { + e.printStackTrace(); + } catch (XmlPullParserException e) { + e.printStackTrace(); + } + } while (waitPollingDelay()); + } + }; + thread.start(); + } + + public void stop() { + if (thread != null) { + thread.interrupt(); + + // Don't join here because we might be blocked on network I/O + + 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 199eaf03..100e49e3 100644 --- a/app/src/main/java/com/limelight/grid/AppGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -3,20 +3,21 @@ package com.limelight.grid; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.view.View; import android.widget.ImageView; import android.widget.TextView; import com.koushikdutta.async.future.FutureCallback; import com.koushikdutta.ion.ImageViewBitmapInfo; import com.koushikdutta.ion.Ion; -import com.koushikdutta.ion.bitmap.BitmapInfo; 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,7 +33,11 @@ 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.Map; +import java.util.UUID; import java.util.concurrent.Future; import javax.net.ssl.HostnameVerifier; @@ -46,17 +51,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, 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); - this.address = address; + this.computer = computer; this.uniqueId = uniqueId; cryptoProvider = PlatformBinding.getCryptoProvider(context); @@ -109,8 +113,27 @@ public class AppGridAdapter extends GenericGridAdapter { public boolean verify(String hostname, SSLSession session) { return true; } }; + private void sortList() { + Collections.sort(itemList, new Comparator() { + @Override + public int compare(AppView.AppObject lhs, AppView.AppObject rhs) { + return lhs.app.getAppName().compareTo(rhs.app.getAppName()); + } + }); + } + + 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(); } public void abortPendingRequests() { @@ -135,94 +158,25 @@ 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) {} - } - } - } - } - - 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(); } } @Override public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) { - - // Set SSL contexts correctly to allow us to authenticate - Ion.getDefault(imgView.getContext()).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts); - Ion.getDefault(imgView.getContext()).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext); + // Hide the image view while we're loading the image from disk cache + imgView.setVisibility(View.INVISIBLE); // Check the on-disk cache - Bitmap cachedBitmap = checkBitmapCache(address.getHostAddress(), obj.app.getAppId()); - if (cachedBitmap != null) { - // Cache hit; we're done - LimeLog.info("Image cache hit for ("+address.getHostAddress()+", "+obj.app.getAppId()+")"); - imgView.setImageBitmap(cachedBitmap); - return true; - } - - // Kick off the deferred image load - synchronized (pendingRequests) { - Future f = Ion.with(imgView) - .placeholder(defaultImageRes) - .error(defaultImageRes) - .load("https://" + address.getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" + - obj.app.getAppId() + "&AssetType=2&AssetIdx=0") - .withBitmapInfo() - .setCallback( - new FutureCallback() { - @Override - public void onCompleted(Exception e, ImageViewBitmapInfo result) { - synchronized (pendingRequests) { - pendingRequests.remove(imgView); - } - - // Populate the cache if we got an image back - if (result != null && - result.getBitmapInfo() != null && - result.getBitmapInfo().bitmap != null) { - populateBitmapCache(address.getHostAddress(), obj.app.getAppId(), - result.getBitmapInfo().bitmap); - } - } - }); - pendingRequests.put(imgView, f); - } + new ImageCacheRequest(imgView, obj.app.getAppId()).execute(); return true; } @@ -247,4 +201,84 @@ public class AppGridAdapter extends GenericGridAdapter { // No overlay return false; } + + private class ImageCacheRequest extends AsyncTask { + private ImageView view; + private 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 e) {} + } + } + return null; + } + + @Override + protected void onPostExecute(Bitmap result) { + if (result != null) { + // Disk cache was read successfully + LimeLog.info("Image disk cache hit for ("+computer.uuid+", "+appId+")"); + view.setImageBitmap(result); + view.setVisibility(View.VISIBLE); + } + 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); + view.setVisibility(View.VISIBLE); + + // Set SSL contexts correctly to allow us to authenticate + Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts); + Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext); + + // Kick off the deferred image load + synchronized (pendingRequests) { + 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, Bitmap result) { + synchronized (pendingRequests) { + pendingRequests.remove(view); + } + + if (result != null) { + // Make the view visible now + view.setImageBitmap(result); + view.setVisibility(View.VISIBLE); + + // Populate the disk cache if we got an image back + populateBitmapCache(computer.uuid, appId, result); + } + else { + // Leave the loading icon as is (probably should change this eventually...) + } + } + }); + pendingRequests.put(view, f); + } + } + } + }; } diff --git a/app/src/main/java/com/limelight/grid/PcGridAdapter.java b/app/src/main/java/com/limelight/grid/PcGridAdapter.java index 8e370de3..c6b22e41 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, boolean listMode, boolean small) { + super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item), R.drawable.computer); } public void addComputer(PcView.ComputerObject computer) { diff --git a/app/src/main/java/com/limelight/preferences/AddComputerManually.java b/app/src/main/java/com/limelight/preferences/AddComputerManually.java index c2eaa19a..723b2330 100644 --- a/app/src/main/java/com/limelight/preferences/AddComputerManually.java +++ b/app/src/main/java/com/limelight/preferences/AddComputerManually.java @@ -19,7 +19,6 @@ import android.content.ServiceConnection; import android.content.res.Configuration; import android.os.Bundle; import android.os.IBinder; -import android.preference.Preference; import android.view.KeyEvent; import android.view.inputmethod.EditorInfo; import android.widget.TextView; @@ -160,7 +159,7 @@ public class AddComputerManually extends Activity { return true; } - computersToAdd.add(hostText.getText().toString()); + computersToAdd.add(hostText.getText().toString().trim()); } return false; diff --git a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java index 73aee4a3..df0b3c42 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 String VIRTUAL_CONTROLLER_ENABLE = "virtual_controller_checkbox_enable"; private static final Boolean VIRTUAL_CONTROLLER_ENABLE_DEFAULT = true; @@ -45,7 +46,7 @@ public class PreferenceConfiguration { public int deadzonePercentage; public boolean stretchVideo, enableSops, playHostAudio, disableWarnings; public String language; - public boolean listMode; + public boolean listMode, smallIconMode; public boolean virtualController_enable; @@ -68,6 +69,11 @@ public class PreferenceConfiguration { } } + public static boolean getDefaultSmallMode(Context context) { + // Use small mode on anything smaller than a 7" tablet + return context.getResources().getConfiguration().smallestScreenWidthDp < 600; + } + public static int getDefaultBitrate(Context context) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); @@ -154,6 +160,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, getDefaultSmallMode(context)); config.virtualController_enable = prefs.getBoolean(VIRTUAL_CONTROLLER_ENABLE, VIRTUAL_CONTROLLER_ENABLE_DEFAULT); diff --git a/app/src/main/java/com/limelight/preferences/SmallIconCheckboxPreference.java b/app/src/main/java/com/limelight/preferences/SmallIconCheckboxPreference.java new file mode 100644 index 00000000..c216b749 --- /dev/null +++ b/app/src/main/java/com/limelight/preferences/SmallIconCheckboxPreference.java @@ -0,0 +1,21 @@ +package com.limelight.preferences; + +import android.content.Context; +import android.content.res.TypedArray; +import android.preference.CheckBoxPreference; +import android.util.AttributeSet; + +public class SmallIconCheckboxPreference extends CheckBoxPreference { + public SmallIconCheckboxPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public SmallIconCheckboxPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return PreferenceConfiguration.getDefaultSmallMode(getContext()); + } +} 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..d7cee523 --- /dev/null +++ b/app/src/main/java/com/limelight/utils/CacheHelper.java @@ -0,0 +1,57 @@ +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) throws IOException { + Reader r = new InputStreamReader(in); + + StringBuilder sb = new StringBuilder(); + char[] buf = new char[256]; + int bytesRead; + while ((bytesRead = r.read(buf)) != -1) { + sb.append(buf, 0, bytesRead); + } + + 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/layout/activity_app_view.xml b/app/src/main/res/layout/activity_app_view.xml index c54753a7..9decf13d 100644 --- a/app/src/main/res/layout/activity_app_view.xml +++ b/app/src/main/res/layout/activity_app_view.xml @@ -26,6 +26,7 @@ android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_alignParentTop="true" + android:gravity="center" android:paddingTop="0dp" android:paddingBottom="10dp" android:textSize="28sp"/> diff --git a/app/src/main/res/layout/app_grid_item_small.xml b/app/src/main/res/layout/app_grid_item_small.xml new file mode 100644 index 00000000..05457e66 --- /dev/null +++ b/app/src/main/res/layout/app_grid_item_small.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/app_grid_view.xml b/app/src/main/res/layout/app_grid_view.xml index 6926a933..980ef19a 100644 --- a/app/src/main/res/layout/app_grid_view.xml +++ b/app/src/main/res/layout/app_grid_view.xml @@ -5,7 +5,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/pc_grid_item_small.xml b/app/src/main/res/layout/pc_grid_item_small.xml new file mode 100644 index 00000000..490aff50 --- /dev/null +++ b/app/src/main/res/layout/pc_grid_item_small.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/pc_grid_view.xml b/app/src/main/res/layout/pc_grid_view.xml index 98a0c9cd..312d840c 100644 --- a/app/src/main/res/layout/pc_grid_view.xml +++ b/app/src/main/res/layout/pc_grid_view.xml @@ -5,7 +5,7 @@ diff --git a/app/src/main/res/layout/pc_grid_view_small.xml b/app/src/main/res/layout/pc_grid_view_small.xml new file mode 100644 index 00000000..22486764 --- /dev/null +++ b/app/src/main/res/layout/pc_grid_view_small.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 705bd4f4..269435e5 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -56,6 +56,9 @@ Indirizzo IP del PC Ricerca PC in corso… + + No + Connessione con il PC persa Applicazioni su @@ -70,6 +73,7 @@ Chiusura in corso… Sessione chiusa con successo Chiusura sessione fallita + Sei sicuro di voler chiudere l\'applicazione avviata? Tutti i dati non salvati saranno persi. Aggiungi PC Manualmente @@ -94,11 +98,13 @@ Aggiusta deadzone degli stick analogici % - UI Settings + Impostazioni Interfaccia Lingua Lingua da usare in Limelight - Use lists instead of grids - Display apps and PCs in lists instead of grids + Usa lista invece della griglia + Visualizza applicazioni e computers in una lista invece di una griglia + Usa icone piccole + Usa icone piccole nella vista a griglia per avere più oggetti sullo schermo Impostazioni Host Ottimizza le impostazioni dei giochi diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b9a58ee2..d5258ef4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -56,7 +56,10 @@ IP address of GeForce PC Searching for PCs… - + Yes + No + Lost connection to PC + Apps on Resume Session @@ -70,7 +73,8 @@ Quitting Successfully quit Failed to quit - + Are you sure you want to quit the running app? All unsaved data will be lost. + Add PC Manually Connecting to the PC… @@ -99,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 91563473..f88efa48 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -54,6 +54,10 @@ android:entryValues="@array/language_values" android:summary="@string/summary_language_list" android:defaultValue="default" /> +