package com.limelight; import java.io.StringReader; import java.util.List; import java.util.Locale; import java.util.UUID; 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.NvApp; import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.PairingManager; 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.ServerHelper; import com.limelight.utils.ShortcutHelper; import com.limelight.utils.SpinnerDialog; import com.limelight.utils.UiHelper; import android.app.Activity; import android.app.Service; import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; import android.content.res.Configuration; import android.os.Bundle; import android.os.IBinder; import android.view.ContextMenu; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ContextMenu.ContextMenuInfo; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.TextView; import android.widget.Toast; import android.widget.AdapterView.AdapterContextMenuInfo; public class AppView extends Activity implements AdapterFragmentCallbacks { private AppGridAdapter appGridAdapter; private String uuidString; private ShortcutHelper shortcutHelper; private ComputerDetails computer; private ComputerManagerService.ApplistPoller poller; private SpinnerDialog blockingLoadSpinner; private String lastRawApplist; private int lastRunningAppId; private boolean suspendGridUpdates; private boolean inForeground; private final static int START_OR_RESUME_ID = 1; private final static int QUIT_ID = 2; private final static int CANCEL_ID = 3; private final static int START_WTIH_QUIT = 4; public final static String NAME_EXTRA = "Name"; public final static String UUID_EXTRA = "UUID"; private ComputerManagerService.ComputerManagerBinder managerBinder; private final 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; } // Load the app grid with cached data (if possible). // This must be done _before_ startComputerUpdates() // so the initial serverinfo response can update the running // icon. populateAppGridWithCache(); // Start updates startComputerUpdates(); runOnUiThread(new Runnable() { @Override public void run() { if (isFinishing() || isChangingConfigurations()) { return; } // Despite my best efforts to catch all conditions that could // cause the activity to be destroyed when we try to commit // I haven't been able to, so we have this try-catch block. try { getFragmentManager().beginTransaction() .replace(R.id.appFragmentContainer, new AdapterFragment()) .commitAllowingStateLoss(); } catch (IllegalStateException e) { e.printStackTrace(); } } }); } }.start(); } public void onServiceDisconnected(ComponentName className) { managerBinder = null; } }; private void startComputerUpdates() { // Don't start polling if we're not bound or in the foreground if (managerBinder == null || !inForeground) { return; } managerBinder.startPolling(new ComputerManagerListener() { @Override public void notifyComputerUpdated(final ComputerDetails details) { // Do nothing if updates are suspended if (suspendGridUpdates) { return; } // Don't care about other computers if (!details.uuid.toString().equalsIgnoreCase(uuidString)) { return; } if (details.state == ComputerDetails.State.OFFLINE) { // 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; } // Close immediately if the PC is no longer paired if (details.state == ComputerDetails.State.ONLINE && details.pairState != PairingManager.PairState.PAIRED) { AppView.this.runOnUiThread(new Runnable() { @Override public void run() { // Disable shortcuts referencing this PC for now shortcutHelper.disableShortcut(details.uuid.toString(), getResources().getString(R.string.scut_not_paired)); // Display a toast to the user and quit the activity Toast.makeText(AppView.this, getResources().getText(R.string.scut_not_paired), Toast.LENGTH_SHORT).show(); finish(); } }); return; } // App list is the same or empty if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) { // Let's check if the running app ID changed if (details.runningGameId != lastRunningAppId) { // Update the currently running game using the app ID lastRunningAppId = details.runningGameId; updateUiWithServerinfo(details); } return; } lastRunningAppId = details.runningGameId; lastRawApplist = details.rawAppList; try { updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList))); updateUiWithServerinfo(details); if (blockingLoadSpinner != null) { blockingLoadSpinner.dismiss(); blockingLoadSpinner = null; } } catch (Exception ignored) {} } }); if (poller == null) { poller = managerBinder.createAppListPoller(computer); } poller.start(); } private void stopComputerUpdates() { if (poller != null) { poller.stop(); } if (managerBinder != null) { managerBinder.stopPolling(); } if (appGridAdapter != null) { appGridAdapter.cancelQueuedOperations(); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); shortcutHelper = new ShortcutHelper(this); UiHelper.setLocale(this); setContentView(R.layout.activity_app_view); UiHelper.notifyNewRootView(this); uuidString = getIntent().getStringExtra(UUID_EXTRA); String computerName = getIntent().getStringExtra(NAME_EXTRA); String labelText = getResources().getString(R.string.title_applist)+" "+computerName; TextView label = (TextView) findViewById(R.id.appListText); setTitle(labelText); label.setText(labelText); // Add a launcher shortcut for this PC (forced, since this is user interaction) shortcutHelper.createAppViewShortcut(uuidString, computerName, uuidString, true); shortcutHelper.reportShortcutUsed(uuidString); // 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<NvApp> 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(); inForeground = true; startComputerUpdates(); } @Override protected void onPause() { super.onPause(); inForeground = false; stopComputerUpdates(); } @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 (lastRunningAppId != 0) { if (lastRunningAppId == 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(); final AppObject app = (AppObject) appGridAdapter.getItem(info.position); switch (item.getItemId()) { case START_WTIH_QUIT: // Display a confirmation dialog first UiHelper.displayQuitConfirmationDialog(this, new Runnable() { @Override public void run() { ServerHelper.doStart(AppView.this, app.app, computer, managerBinder); } }, null); return true; case START_OR_RESUME_ID: // Resume is the same as start for us ServerHelper.doStart(AppView.this, app.app, computer, managerBinder); return true; case QUIT_ID: // Display a confirmation dialog first UiHelper.displayQuitConfirmationDialog(this, new Runnable() { @Override public void run() { suspendGridUpdates = true; ServerHelper.doQuit(AppView.this, ServerHelper.getCurrentAddressFromComputer(computer), app.app, managerBinder, new Runnable() { @Override public void run() { // Trigger a poll immediately suspendGridUpdates = false; if (poller != null) { poller.pollNow(); } } }); } }, null); return true; case CANCEL_ID: return true; default: return super.onContextItemSelected(item); } } private void updateUiWithServerinfo(final ComputerDetails details) { AppView.this.runOnUiThread(new Runnable() { @Override public void run() { boolean updated = false; // Look through our current app list to tag the running app for (int i = 0; i < appGridAdapter.getCount(); i++) { AppObject existingApp = (AppObject) appGridAdapter.getItem(i); // There can only be one or zero apps running. if (existingApp.isRunning && existingApp.app.getAppId() == details.runningGameId) { // This app was running and still is, so we're done now return; } else if (existingApp.app.getAppId() == details.runningGameId) { // This app wasn't running but now is existingApp.isRunning = true; updated = true; } else if (existingApp.isRunning) { // This app was running but now isn't existingApp.isRunning = false; updated = true; } else { // This app wasn't running and still isn't } } if (updated) { appGridAdapter.notifyDataSetChanged(); } } }); } private void updateUiWithAppList(final List<NvApp> appList) { AppView.this.runOnUiThread(new Runnable() { @Override public void run() { boolean updated = false; // First handle app updates and additions 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.getAppId() == app.getAppId()) { // Found the app; update its properties 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; } } // Next handle app removals int i = 0; while (i < appGridAdapter.getCount()) { boolean foundExistingApp = false; AppObject existingApp = (AppObject) appGridAdapter.getItem(i); // Check if this app is in the latest list for (NvApp app : appList) { if (existingApp.app.getAppId() == app.getAppId()) { foundExistingApp = true; break; } } // This app was removed in the latest app list if (!foundExistingApp) { appGridAdapter.removeApp(existingApp); updated = true; // Check this same index again because the item at i+1 is now at i after // the removal continue; } // Move on to the next item i++; } if (updated) { appGridAdapter.notifyDataSetChanged(); } } }); } @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); // Only open the context menu if something is running, otherwise start it if (lastRunningAppId != 0) { openContextMenu(arg1); } else { ServerHelper.doStart(AppView.this, app.app, computer, managerBinder); } } }); registerForContextMenu(listView); listView.requestFocus(); } public class AppObject { public final NvApp app; public boolean isRunning; public AppObject(NvApp app) { if (app == null) { throw new IllegalArgumentException("app must not be null"); } this.app = app; } @Override public String toString() { return app.getAppName(); } } }