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 @@
-
-
-
-
+
+
+
+
+
+
@@ -21,22 +23,28 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -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…
+ Sì
+ 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" />
+