diff --git a/app/app.iml b/app/app.iml index 436535aa..b9d6ee2a 100644 --- a/app/app.iml +++ b/app/app.iml @@ -101,12 +101,18 @@ + + + + + + diff --git a/app/build.gradle b/app/build.gradle index 55348db7..c1d02b1b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { minSdkVersion 16 targetSdkVersion 21 - versionName "2.9" - versionCode = 38 + versionName "3.0-beta1.01" + versionCode = 41 } productFlavors { @@ -65,6 +65,10 @@ dependencies { compile group: 'org.jcodec', name: 'jcodec', version: '0.1.6-3' compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51' compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51' + compile group: 'com.google.android', name: 'support-v4', version:'r6' + compile group: 'com.koushikdutta.ion', name: 'ion', version:'1.3.7' + compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.1.0-RC1' + compile group: 'com.squareup.okio', name:'okio', version:'1.0.1' compile files('libs/jmdns-fixed.jar') compile files('libs/limelight-common.jar') compile files('libs/tinyrtsp.jar') diff --git a/app/libs/limelight-common.jar b/app/libs/limelight-common.jar index de1d908b..d6f99b3f 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 aeff34de..c44b19a7 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -4,15 +4,17 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; import java.util.List; import org.xmlpull.v1.XmlPullParserException; import com.limelight.binding.PlatformBinding; +import com.limelight.grid.AppGridAdapter; import com.limelight.nvstream.http.GfeHttpResponseException; import com.limelight.nvstream.http.NvApp; import com.limelight.nvstream.http.NvHTTP; -import com.limelight.R; import com.limelight.utils.Dialog; import com.limelight.utils.SpinnerDialog; import com.limelight.utils.UiHelper; @@ -27,15 +29,14 @@ import android.view.View; import android.view.ContextMenu.ContextMenuInfo; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; -import android.widget.ArrayAdapter; -import android.widget.ListView; +import android.widget.GridView; import android.widget.TextView; import android.widget.Toast; import android.widget.AdapterView.AdapterContextMenuInfo; public class AppView extends Activity { - private ListView appList; - private ArrayAdapter appListAdapter; + private GridView appGrid; + private AppGridAdapter appGridAdapter; private InetAddress ipAddress; private String uniqueId; private boolean remote; @@ -60,6 +61,7 @@ public class AppView extends Activity { uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA); remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false); if (address == null || uniqueId == null) { + finish(); return; } @@ -71,20 +73,26 @@ public class AppView extends Activity { try { ipAddress = InetAddress.getByAddress(address); } catch (UnknownHostException e) { - return; + e.printStackTrace(); + finish(); + return; } // Setup the list view - appList = (ListView)findViewById(R.id.pcListView); - appListAdapter = new ArrayAdapter(this, R.layout.simplerow, R.id.rowTextView); - appListAdapter.setNotifyOnChange(false); - appList.setAdapter(appListAdapter); - appList.setItemsCanFocus(true); - appList.setOnItemClickListener(new OnItemClickListener() { + appGrid = (GridView)findViewById(R.id.appGridView); + try { + appGridAdapter = new AppGridAdapter(this, ipAddress, uniqueId); + } catch (Exception e) { + e.printStackTrace(); + finish(); + return; + } + appGrid.setAdapter(appGridAdapter); + appGrid.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView arg0, View arg1, int pos, long id) { - AppObject app = appListAdapter.getItem(pos); + AppObject app = (AppObject) appGridAdapter.getItem(pos); if (app == null || app.app == null) { return; } @@ -98,7 +106,7 @@ public class AppView extends Activity { } } }); - registerForContextMenu(appList); + registerForContextMenu(appGrid); } @Override @@ -118,8 +126,8 @@ public class AppView extends Activity { private int getRunningAppId() { int runningAppId = -1; - for (int i = 0; i < appListAdapter.getCount(); i++) { - AppObject app = appListAdapter.getItem(i); + for (int i = 0; i < appGridAdapter.getCount(); i++) { + AppObject app = (AppObject) appGridAdapter.getItem(i); if (app.app == null) { continue; } @@ -135,25 +143,25 @@ public class AppView extends Activity { @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); - - AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; - AppObject selectedApp = appListAdapter.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, 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)); + } + } + } @Override public void onContextMenuClosed(Menu menu) { @@ -162,42 +170,28 @@ public class AppView extends Activity { @Override public boolean onContextItemSelected(MenuItem item) { AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); - AppObject app = appListAdapter.getItem(info.position); - switch (item.getItemId()) - { - case RESUME_ID: - // Resume is the same as start for us - doStart(app.app); - return true; - - case QUIT_ID: - doQuit(app.app); - return true; - - case CANCEL_ID: - return true; - - default: - return super.onContextItemSelected(item); - } - } + 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 static String generateString(NvApp app) { - StringBuilder str = new StringBuilder(); - str.append(app.getAppName()); - if (app.getIsRunning()) { - str.append(" - "+getResources().getString(R.string.applist_app_running)); - } - return str.toString(); - } - - private void addListPlaceholder() { - appListAdapter.add(new AppObject(getResources().getString(R.string.applist_no_apps), null)); + case QUIT_ID: + doQuit(app.app); + return true; + + case CANCEL_ID: + return true; + + default: + return super.onContextItemSelected(item); + } } private void updateAppList() { final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title), - getResources().getString(R.string.applist_refresh_msg), true); + getResources().getString(R.string.applist_refresh_title), true); new Thread() { @Override public void run() { @@ -209,17 +203,12 @@ public class AppView extends Activity { AppView.this.runOnUiThread(new Runnable() { @Override public void run() { - appListAdapter.clear(); - if (appList.isEmpty()) { - addListPlaceholder(); - } - else { - for (NvApp app : appList) { - appListAdapter.add(new AppObject(generateString(app), app)); - } - } - - appListAdapter.notifyDataSetChanged(); + appGridAdapter.clear(); + for (NvApp app : appList) { + appGridAdapter.addApp(new AppObject(app)); + } + + appGridAdapter.notifyDataSetChanged(); } }); @@ -282,17 +271,15 @@ public class AppView extends Activity { } public class AppObject { - public String text; public NvApp app; - public AppObject(String text, NvApp app) { - this.text = text; + public AppObject(NvApp app) { this.app = app; } @Override public String toString() { - return text; + return app.getAppName(); } } } diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index 3bb036c2..abb2bf12 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -678,8 +678,8 @@ public class Game extends Activity implements SurfaceHolder.Callback, e.printStackTrace(); stopConnection(); - Dialog.displayDialog(this, getResources().getString(R.string.conn_fail_title), - getResources().getString(R.string.conn_fail_msg), true); + Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title), + getResources().getString(R.string.conn_terminated_msg), true); } } diff --git a/app/src/main/java/com/limelight/PcView.java b/app/src/main/java/com/limelight/PcView.java index b453596c..697104ab 100644 --- a/app/src/main/java/com/limelight/PcView.java +++ b/app/src/main/java/com/limelight/PcView.java @@ -9,6 +9,7 @@ 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.PcGridAdapter; import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.PairingManager; @@ -36,16 +37,18 @@ import android.view.ContextMenu.ContextMenuInfo; import android.view.View.OnClickListener; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; -import android.widget.ArrayAdapter; import android.widget.Button; -import android.widget.ListView; +import android.widget.GridView; +import android.widget.ImageButton; +import android.widget.RelativeLayout; import android.widget.Toast; import android.widget.AdapterView.AdapterContextMenuInfo; public class PcView extends Activity { - private Button settingsButton, addComputerButton; - private ListView pcList; - private ArrayAdapter pcListAdapter; + private ImageButton settingsButton, addComputerButton; + private RelativeLayout noPcFoundLayout; + private GridView pcGrid; + private PcGridAdapter pcGridAdapter; private ComputerManagerService.ComputerManagerBinder managerBinder; private boolean freezeUpdates, runningPolling; private ServiceConnection serviceConnection = new ServiceConnection() { @@ -99,21 +102,19 @@ public class PcView extends Activity { PreferenceManager.setDefaultValues(this, R.xml.preferences, false); // Setup the list view - settingsButton = (Button)findViewById(R.id.settingsButton); - addComputerButton = (Button)findViewById(R.id.manuallyAddPc); + settingsButton = (ImageButton)findViewById(R.id.settingsButton); + addComputerButton = (ImageButton)findViewById(R.id.manuallyAddPc); - pcList = (ListView)findViewById(R.id.pcListView); - pcList.setAdapter(pcListAdapter); - pcList.setItemsCanFocus(true); - pcList.setOnItemClickListener(new OnItemClickListener() { + pcGrid = (GridView)findViewById(R.id.pcGridView); + pcGrid.setAdapter(pcGridAdapter); + pcGrid.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView arg0, View arg1, int pos, long id) { - ComputerObject computer = pcListAdapter.getItem(pos); - if (computer.details == null) { - // Placeholder item; no context menu for it - return; - } + ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos); + if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) { + // Do nothing + } else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) { // Open the context menu if a PC is offline openContextMenu(arg1); @@ -127,7 +128,7 @@ public class PcView extends Activity { } } }); - registerForContextMenu(pcList); + registerForContextMenu(pcGrid); settingsButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { @@ -142,13 +143,15 @@ public class PcView extends Activity { } }); - if (pcListAdapter.isEmpty()) { - addListPlaceholder(); - } - else { - pcListAdapter.notifyDataSetChanged(); - } - } + noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout); + if (pcGridAdapter.getCount() == 0) { + noPcFoundLayout.setVisibility(View.VISIBLE); + } + else { + noPcFoundLayout.setVisibility(View.INVISIBLE); + } + pcGridAdapter.notifyDataSetChanged(); + } @Override protected void onCreate(Bundle savedInstanceState) { @@ -157,9 +160,8 @@ public class PcView extends Activity { // Bind to the computer manager service bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE); - - pcListAdapter = new ArrayAdapter(this, R.layout.simplerow, R.id.rowTextView); - pcListAdapter.setNotifyOnChange(false); + + pcGridAdapter = new PcGridAdapter(this); initializeViews(); } @@ -178,7 +180,7 @@ public class PcView extends Activity { PcView.this.runOnUiThread(new Runnable() { @Override public void run() { - updateListView(details); + updateComputer(details); } }); } @@ -242,30 +244,29 @@ public class PcView extends Activity { // Call superclass super.onCreateContextMenu(menu, v, menuInfo); - - AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; - ComputerObject computer = pcListAdapter.getItem(info.position); - if (computer == null || computer.details == null) { - startComputerUpdates(); - return; - } - - // Inflate the context menu - if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) { - menu.add(Menu.NONE, WOL_ID, 1, getResources().getString(R.string.pcview_menu_send_wol)); - menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc)); - } - else if (computer.details.pairState != PairState.PAIRED) { - menu.add(Menu.NONE, PAIR_ID, 1, getResources().getString(R.string.pcview_menu_pair_pc)); - if (computer.details.reachability == ComputerDetails.Reachability.REMOTE) { - menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc)); - } - } - else { - menu.add(Menu.NONE, APP_LIST_ID, 1, getResources().getString(R.string.pcview_menu_app_list)); - menu.add(Menu.NONE, UNPAIR_ID, 2, getResources().getString(R.string.pcview_menu_unpair_pc)); - } - } + + AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; + ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position); + if (computer == null || computer.details == null || + computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) { + startComputerUpdates(); + return; + } + + // Inflate the context menu + if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) { + menu.add(Menu.NONE, WOL_ID, 1, getResources().getString(R.string.pcview_menu_send_wol)); + menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc)); + } + else if (computer.details.pairState != PairState.PAIRED) { + menu.add(Menu.NONE, PAIR_ID, 1, getResources().getString(R.string.pcview_menu_pair_pc)); + menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc)); + } + else { + menu.add(Menu.NONE, APP_LIST_ID, 1, getResources().getString(R.string.pcview_menu_app_list)); + menu.add(Menu.NONE, UNPAIR_ID, 2, getResources().getString(R.string.pcview_menu_unpair_pc)); + } + } @Override public void onContextMenuClosed(Menu menu) { @@ -338,6 +339,7 @@ public class PcView extends Activity { } catch (FileNotFoundException e) { message = getResources().getString(R.string.error_404); } catch (Exception e) { + e.printStackTrace(); message = e.getMessage(); } @@ -476,104 +478,58 @@ public class PcView extends Activity { startActivity(i); } - @Override - public boolean onContextItemSelected(MenuItem item) { - AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); - ComputerObject computer = pcListAdapter.getItem(info.position); - switch (item.getItemId()) - { - case PAIR_ID: - doPair(computer.details); - return true; - - case UNPAIR_ID: - doUnpair(computer.details); - return true; - - case WOL_ID: - doWakeOnLan(computer.details); - return true; - - case DELETE_ID: - if (managerBinder == null) { - Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); - return true; - } - managerBinder.removeComputer(computer.details.name); - removeListView(computer.details); - return true; - - case APP_LIST_ID: - doAppList(computer.details); - return true; - - default: - return super.onContextItemSelected(item); - } - } - - private static String generateString(ComputerDetails details) { - StringBuilder str = new StringBuilder(); - str.append(details.name).append(" - "); - if (details.state == ComputerDetails.State.ONLINE) { - str.append(getResources().getString(R.string.status_online)+" "); - if (details.reachability == ComputerDetails.Reachability.LOCAL) { - str.append("("+getResources().getString(R.string.status_local)+") - "); - } - else { - str.append("("+getResources().getString(R.string.status_remote)+") - "); - } - if (details.pairState == PairState.PAIRED) { - if (details.runningGameId == 0) { - str.append(getResources().getString(R.string.status_available)); - } - else { - str.append(getResources().getString(R.string.status_ingame)); - } - } - else { - str.append(getResources().getString(R.string.status_not_paired)); - } - } - else { - str.append(getResources().getString(R.string.status_offline)); - } - return str.toString(); - } - - private void addListPlaceholder() { - pcListAdapter.add(new ComputerObject(getResources().getString(R.string.discovery_running), null)); - } - - private void removeListView(ComputerDetails details) { - for (int i = 0; i < pcListAdapter.getCount(); i++) { - ComputerObject computer = pcListAdapter.getItem(i); + @Override + public boolean onContextItemSelected(MenuItem item) { + AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); + ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position); + switch (item.getItemId()) + { + case PAIR_ID: + doPair(computer.details); + return true; + + case UNPAIR_ID: + doUnpair(computer.details); + return true; + + case WOL_ID: + doWakeOnLan(computer.details); + return true; + + case DELETE_ID: + if (managerBinder == null) { + Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); + return true; + } + managerBinder.removeComputer(computer.details.name); + removeComputer(computer.details); + return true; + + case APP_LIST_ID: + doAppList(computer.details); + return true; + + default: + return super.onContextItemSelected(item); + } + } + + private void removeComputer(ComputerDetails details) { + for (int i = 0; i < pcGridAdapter.getCount(); i++) { + ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i); if (details.equals(computer.details)) { - pcListAdapter.remove(computer); + pcGridAdapter.removeComputer(computer); break; } } - - if (pcListAdapter.getCount() == 0) { - // Add the placeholder if we're down to 0 computers - addListPlaceholder(); - } - } - - private void updateListView(ComputerDetails details) { - String computerString = generateString(details); + } + + private void updateComputer(ComputerDetails details) { ComputerObject existingEntry = null; - boolean placeholderPresent = false; - for (int i = 0; i < pcListAdapter.getCount(); i++) { - ComputerObject computer = pcListAdapter.getItem(i); - - // If there's a placeholder, there's nothing else - if (computer.details == null) { - placeholderPresent = true; - break; - } + for (int i = 0; i < pcGridAdapter.getCount(); i++) { + ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i); // Check if this is the same computer if (details.equals(computer.details)) { @@ -584,35 +540,30 @@ public class PcView extends Activity { if (existingEntry != null) { // Replace the information in the existing entry - existingEntry.text = computerString; existingEntry.details = details; } else { - // If the placeholder is the only object, remove it - if (placeholderPresent) { - pcListAdapter.remove(pcListAdapter.getItem(0)); - } - // Add a new entry - pcListAdapter.add(new ComputerObject(computerString, details)); + pcGridAdapter.addComputer(new ComputerObject(details)); + + // Remove the "Discovery in progress" view + noPcFoundLayout.setVisibility(View.INVISIBLE); } - - // Notify the view that the data has changed - pcListAdapter.notifyDataSetChanged(); + + // Notify the view that the data has changed + pcGridAdapter.notifyDataSetChanged(); } public class ComputerObject { - public String text; public ComputerDetails details; - public ComputerObject(String text, ComputerDetails details) { - this.text = text; + public ComputerObject(ComputerDetails details) { this.details = details; } @Override public String toString() { - return text; + return details.name; } } } diff --git a/app/src/main/java/com/limelight/computers/ComputerDatabaseManager.java b/app/src/main/java/com/limelight/computers/ComputerDatabaseManager.java index 78630166..701b1222 100644 --- a/app/src/main/java/com/limelight/computers/ComputerDatabaseManager.java +++ b/app/src/main/java/com/limelight/computers/ComputerDatabaseManager.java @@ -99,15 +99,17 @@ public class ComputerDatabaseManager { details.macAddress = c.getString(4); // This signifies we don't have dynamic state (like pair state) - details.state = ComputerDetails.State.UNKNOWN; + details.state = ComputerDetails.State.UNKNOWN; + details.reachability = ComputerDetails.Reachability.UNKNOWN; // If a field is corrupt or missing, skip the database entry if (details.uuid == null || details.localIp == null || details.remoteIp == null || details.macAddress == null) { continue; } - - computerList.add(details); + + + computerList.add(details); } c.close(); @@ -151,6 +153,9 @@ public class ComputerDatabaseManager { details.macAddress = c.getString(4); c.close(); + + details.state = ComputerDetails.State.UNKNOWN; + details.reachability = ComputerDetails.Reachability.UNKNOWN; // If a field is corrupt or missing, delete the database entry if (details.uuid == null || details.localIp == null || details.remoteIp == null || diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index fc08a440..fde9dd32 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -151,6 +151,9 @@ public class ComputerManagerService extends Service { for (ComputerDetails computer : computerList) { // This polling thread might already be there if (!pollingThreads.containsKey(computer)) { + // Report this computer initially + listener.notifyComputerUpdated(computer); + Thread t = createPollingThread(computer); pollingThreads.put(computer, t); t.start(); diff --git a/app/src/main/java/com/limelight/grid/AppGridAdapter.java b/app/src/main/java/com/limelight/grid/AppGridAdapter.java new file mode 100644 index 00000000..4a3a280c --- /dev/null +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -0,0 +1,167 @@ +package com.limelight.grid; + +import android.content.Context; +import android.util.Log; +import android.widget.ImageView; +import android.widget.TextView; + +import com.koushikdutta.async.future.FutureCallback; +import com.koushikdutta.ion.Ion; +import com.limelight.AppView; +import com.limelight.R; +import com.limelight.binding.PlatformBinding; +import com.limelight.nvstream.http.LimelightCryptoProvider; + +import java.net.InetAddress; +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.concurrent.Future; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; +import java.security.cert.X509Certificate; + +public class AppGridAdapter extends GenericGridAdapter { + + private InetAddress address; + private String uniqueId; + private LimelightCryptoProvider cryptoProvider; + private SSLContext sslContext; + private HashMap pendingRequests = new HashMap(); + + public AppGridAdapter(Context context, InetAddress address, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException { + super(context, R.layout.app_grid_item, R.drawable.image_loading); + + this.address = address; + this.uniqueId = uniqueId; + + cryptoProvider = PlatformBinding.getCryptoProvider(context); + + sslContext = SSLContext.getInstance("SSL"); + sslContext.init(ourKeyman, trustAllCerts, new SecureRandom()); + } + + TrustManager[] trustAllCerts = new TrustManager[] { + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + public void checkClientTrusted(X509Certificate[] certs, String authType) {} + public void checkServerTrusted(X509Certificate[] certs, String authType) {} + }}; + + KeyManager[] ourKeyman = new KeyManager[] { + new X509KeyManager() { + public String chooseClientAlias(String[] keyTypes, + Principal[] issuers, Socket socket) { + return "Limelight-RSA"; + } + + public String chooseServerAlias(String keyType, Principal[] issuers, + Socket socket) { + return null; + } + + public X509Certificate[] getCertificateChain(String alias) { + return new X509Certificate[] {cryptoProvider.getClientCertificate()}; + } + + public String[] getClientAliases(String keyType, Principal[] issuers) { + return null; + } + + public PrivateKey getPrivateKey(String alias) { + return cryptoProvider.getClientPrivateKey(); + } + + public String[] getServerAliases(String keyType, Principal[] issuers) { + return null; + } + } + }; + + // Ignore differences between given hostname and certificate hostname + HostnameVerifier hv = new HostnameVerifier() { + public boolean verify(String hostname, SSLSession session) { return true; } + }; + + public void addApp(AppView.AppObject app) { + itemList.add(app); + } + + public void abortPendingRequests() { + HashMap tempMap; + + synchronized (pendingRequests) { + // Copy the pending requests under a lock + tempMap = new HashMap(pendingRequests); + } + + for (Future f : tempMap.values()) { + f.cancel(true); + } + + synchronized (pendingRequests) { + // Remove cancelled requests + for (ImageView v : tempMap.keySet()) { + pendingRequests.remove(v); + } + } + } + + @Override + public boolean populateImageView(final ImageView imgView, 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); + + // Set 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") + .setCallback(new FutureCallback() { + @Override + public void onCompleted(Exception e, ImageView result) { + synchronized (pendingRequests) { + pendingRequests.remove(imgView); + } + } + }); + pendingRequests.put(imgView, f); + } + + return true; + } + + @Override + public boolean populateTextView(TextView txtView, AppView.AppObject obj) { + // Return false to use the app's toString method + return false; + } + + @Override + public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) { + if (obj.app.getIsRunning()) { + // Show the play button overlay + overlayView.setImageResource(R.drawable.play); + return true; + } + + // No overlay + return false; + } +} diff --git a/app/src/main/java/com/limelight/grid/GenericGridAdapter.java b/app/src/main/java/com/limelight/grid/GenericGridAdapter.java new file mode 100644 index 00000000..adaaa368 --- /dev/null +++ b/app/src/main/java/com/limelight/grid/GenericGridAdapter.java @@ -0,0 +1,79 @@ +package com.limelight.grid; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import com.limelight.PcView; +import com.limelight.R; + +import java.util.ArrayList; + +public abstract class GenericGridAdapter extends BaseAdapter { + protected Context context; + protected int defaultImageRes; + protected int layoutId; + protected ArrayList itemList = new ArrayList(); + protected LayoutInflater inflater; + + public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) { + this.context = context; + this.layoutId = layoutId; + this.defaultImageRes = defaultImageRes; + + this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + public void clear() { + itemList.clear(); + } + + @Override + public int getCount() { + return itemList.size(); + } + + @Override + public Object getItem(int i) { + return itemList.get(i); + } + + @Override + public long getItemId(int i) { + return i; + } + + public abstract boolean populateImageView(ImageView imgView, T obj); + public abstract boolean populateTextView(TextView txtView, T obj); + public abstract boolean populateOverlayView(ImageView overlayView, T obj); + + @Override + public View getView(int i, View convertView, ViewGroup viewGroup) { + if (convertView == null) { + convertView = inflater.inflate(layoutId, viewGroup, false); + } + + ImageView imgView = (ImageView) convertView.findViewById(R.id.grid_image); + ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay); + TextView txtView = (TextView) convertView.findViewById(R.id.grid_text); + + if (!populateImageView(imgView, itemList.get(i))) { + imgView.setImageResource(defaultImageRes); + } + if (!populateTextView(txtView, itemList.get(i))) { + txtView.setText(itemList.get(i).toString()); + } + if (!populateOverlayView(overlayView, itemList.get(i))) { + overlayView.setVisibility(View.INVISIBLE); + } + else { + overlayView.setVisibility(View.VISIBLE); + } + + return convertView; + } +} diff --git a/app/src/main/java/com/limelight/grid/PcGridAdapter.java b/app/src/main/java/com/limelight/grid/PcGridAdapter.java new file mode 100644 index 00000000..a191381c --- /dev/null +++ b/app/src/main/java/com/limelight/grid/PcGridAdapter.java @@ -0,0 +1,62 @@ +package com.limelight.grid; + +import android.content.Context; +import android.widget.ImageView; +import android.widget.TextView; + +import com.limelight.PcView; +import com.limelight.R; +import com.limelight.nvstream.http.ComputerDetails; + +public class PcGridAdapter extends GenericGridAdapter { + + public PcGridAdapter(Context context) { + super(context, R.layout.pc_grid_item, R.drawable.computer); + } + + public void addComputer(PcView.ComputerObject computer) { + itemList.add(computer); + } + + public boolean removeComputer(PcView.ComputerObject computer) { + return itemList.remove(computer); + } + + @Override + public boolean populateImageView(ImageView imgView, PcView.ComputerObject obj) { + if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) { + imgView.setAlpha(1.0f); + } + else { + imgView.setAlpha(0.4f); + } + + // Return false to use the default drawable + return false; + } + + @Override + public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) { + if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) { + txtView.setAlpha(1.0f); + } + else { + txtView.setAlpha(0.4f); + } + + // Return false to use the computer's toString method + return false; + } + + @Override + public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) { + if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) { + // Still refreshing this PC so display the overlay + overlayView.setImageResource(R.drawable.image_loading); + return true; + } + + // No overlay + return false; + } +} diff --git a/app/src/main/java/com/limelight/preferences/AddComputerManually.java b/app/src/main/java/com/limelight/preferences/AddComputerManually.java index 0cb46136..ebb3e717 100644 --- a/app/src/main/java/com/limelight/preferences/AddComputerManually.java +++ b/app/src/main/java/com/limelight/preferences/AddComputerManually.java @@ -7,6 +7,7 @@ import java.util.concurrent.LinkedBlockingQueue; import com.limelight.computers.ComputerManagerService; import com.limelight.R; import com.limelight.utils.Dialog; +import com.limelight.utils.SpinnerDialog; import com.limelight.utils.UiHelper; import android.app.Activity; @@ -16,14 +17,17 @@ import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; import android.os.IBinder; +import android.view.KeyEvent; import android.view.View; import android.view.View.OnClickListener; +import android.view.inputmethod.EditorInfo; import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; public class AddComputerManually extends Activity { - private Button addPcButton; private TextView hostText; private ComputerManagerService.ComputerManagerBinder managerBinder; private LinkedBlockingQueue computersToAdd = new LinkedBlockingQueue(); @@ -43,6 +47,9 @@ public class AddComputerManually extends Activity { private void doAddPc(String host) { String msg; boolean finish = false; + + SpinnerDialog dialog = SpinnerDialog.displayDialog(this, "Add PC Manually", "Connecting to the specified PC...", false); + try { InetAddress addr = InetAddress.getByName(host); @@ -56,8 +63,10 @@ public class AddComputerManually extends Activity { } catch (UnknownHostException e) { msg = getResources().getString(R.string.addpc_unknown_host); } - - final boolean toastFinish = finish; + + dialog.dismiss(); + + final boolean toastFinish = finish; final String toastMsg = msg; AddComputerManually.this.runOnUiThread(new Runnable() { @Override @@ -110,6 +119,7 @@ public class AddComputerManually extends Activity { super.onStop(); Dialog.closeDialogs(); + SpinnerDialog.closeDialogs(this); } @Override @@ -130,24 +140,28 @@ public class AddComputerManually extends Activity { UiHelper.notifyNewRootView(this); - this.addPcButton = (Button) findViewById(R.id.addPc); this.hostText = (TextView) findViewById(R.id.hostTextView); + hostText.setImeOptions(EditorInfo.IME_ACTION_DONE); + hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) { + if (actionId == EditorInfo.IME_ACTION_DONE || + keyEvent.getAction() == KeyEvent.ACTION_DOWN && + keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER) { + if (hostText.getText().length() == 0) { + Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show(); + return true; + } + + computersToAdd.add(hostText.getText().toString()); + } + + return false; + } + }); // Bind to the ComputerManager service bindService(new Intent(AddComputerManually.this, - ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE); - - addPcButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - if (hostText.getText().length() == 0) { - Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show(); - return; - } - - Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_adding_pc), Toast.LENGTH_SHORT).show(); - computersToAdd.add(hostText.getText().toString()); - } - }); + ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE); } } diff --git a/app/src/main/res/drawable/add_computer.png b/app/src/main/res/drawable/add_computer.png new file mode 100644 index 00000000..e228d32d Binary files /dev/null and b/app/src/main/res/drawable/add_computer.png differ diff --git a/app/src/main/res/drawable/computer.png b/app/src/main/res/drawable/computer.png new file mode 100644 index 00000000..1b6abe7c Binary files /dev/null and b/app/src/main/res/drawable/computer.png differ diff --git a/app/src/main/res/drawable/image_loading.png b/app/src/main/res/drawable/image_loading.png new file mode 100644 index 00000000..e03a5d77 Binary files /dev/null and b/app/src/main/res/drawable/image_loading.png differ diff --git a/app/src/main/res/drawable/list_view_unselected.xml b/app/src/main/res/drawable/list_view_unselected.xml deleted file mode 100644 index 5150c083..00000000 --- a/app/src/main/res/drawable/list_view_unselected.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/play.png b/app/src/main/res/drawable/play.png new file mode 100644 index 00000000..d03e6e6e Binary files /dev/null and b/app/src/main/res/drawable/play.png differ diff --git a/app/src/main/res/drawable/settings.png b/app/src/main/res/drawable/settings.png new file mode 100644 index 00000000..8d20bbb2 Binary files /dev/null and b/app/src/main/res/drawable/settings.png differ diff --git a/app/src/main/res/layout-land/activity_pc_view.xml b/app/src/main/res/layout-land/activity_pc_view.xml index 8c7b4df4..06ad7c52 100644 --- a/app/src/main/res/layout-land/activity_pc_view.xml +++ b/app/src/main/res/layout-land/activity_pc_view.xml @@ -8,48 +8,64 @@ android:paddingTop="@dimen/activity_vertical_margin" tools:context=".PcView" > - - - - - + android:layout_toLeftOf="@+id/manuallyAddPc" + android:layout_toRightOf="@+id/settingsButton"> + + + + + + -