diff --git a/app/app.iml b/app/app.iml index 436535aa..875c07e9 100644 --- a/app/app.iml +++ b/app/app.iml @@ -102,11 +102,15 @@ + + + + diff --git a/app/build.gradle b/app/build.gradle index 55348db7..4db98ee3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,6 +65,8 @@ 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 files('libs/jmdns-fixed.jar') compile files('libs/limelight-common.jar') compile files('libs/tinyrtsp.jar') diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index cd1ea178..e3194f51 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,10 +61,11 @@ public class AppView extends Activity { uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA); remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false); if (address == null || uniqueId == null) { + finish(); return; } - String labelText = "App List for "+getIntent().getStringExtra(NAME_EXTRA); + String labelText = "Apps on "+getIntent().getStringExtra(NAME_EXTRA); TextView label = (TextView) findViewById(R.id.appListText); setTitle(labelText); label.setText(labelText); @@ -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; } @@ -137,7 +145,7 @@ public class AppView extends Activity { super.onCreateContextMenu(menu, v, menuInfo); AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; - AppObject selectedApp = appListAdapter.getItem(info.position); + AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position); if (selectedApp == null || selectedApp.app == null) { return; } @@ -162,7 +170,7 @@ public class AppView extends Activity { @Override public boolean onContextItemSelected(MenuItem item) { AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); - AppObject app = appListAdapter.getItem(info.position); + AppObject app = (AppObject) appGridAdapter.getItem(info.position); switch (item.getItemId()) { case RESUME_ID: @@ -191,12 +199,8 @@ public class AppView extends Activity { return str.toString(); } - private void addListPlaceholder() { - appListAdapter.add(new AppObject("No apps found. Try rescanning for games in GeForce Experience.", null)); - } - private void updateAppList() { - final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, "App List", "Refreshing app list...", true); + final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, "App List", "Refreshing apps...", true); new Thread() { @Override public void run() { @@ -208,17 +212,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(generateString(app), app)); + } + + appGridAdapter.notifyDataSetChanged(); } }); @@ -282,17 +281,15 @@ public class AppView extends Activity { } public class AppObject { - public String text; public NvApp app; public AppObject(String text, NvApp app) { - this.text = text; this.app = app; } @Override public String toString() { - return text; + 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 5cbad9f4..a6b96089 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -676,7 +676,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, e.printStackTrace(); stopConnection(); - Dialog.displayDialog(this, "Connection Terminated", "The connection failed unexpectedly", true); + Dialog.displayDialog(this, "Connection Terminated", "The connection was terminated", true); } } 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..b12a9315 --- /dev/null +++ b/app/src/main/java/com/limelight/grid/AppGridAdapter.java @@ -0,0 +1,155 @@ +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; + } +} diff --git a/app/src/main/java/com/limelight/grid/GenericGridAdapter.java b/app/src/main/java/com/limelight/grid/GenericGridAdapter.java index 145f4c2b..ba2bf1db 100644 --- a/app/src/main/java/com/limelight/grid/GenericGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/GenericGridAdapter.java @@ -28,6 +28,10 @@ public abstract class GenericGridAdapter extends BaseAdapter { this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } + public void clear() { + itemList.clear(); + } + @Override public int getCount() { return itemList.size(); diff --git a/app/src/main/java/com/limelight/grid/PcGridAdapter.java b/app/src/main/java/com/limelight/grid/PcGridAdapter.java index a87a5b12..0811560d 100644 --- a/app/src/main/java/com/limelight/grid/PcGridAdapter.java +++ b/app/src/main/java/com/limelight/grid/PcGridAdapter.java @@ -10,7 +10,7 @@ import com.limelight.R; public class PcGridAdapter extends GenericGridAdapter { public PcGridAdapter(Context context) { - super(context, R.layout.generic_grid_item, R.drawable.computer); + super(context, R.layout.pc_grid_item, R.drawable.computer); } public void addComputer(PcView.ComputerObject computer) { 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/layout/activity_app_view.xml b/app/src/main/res/layout/activity_app_view.xml index 30788ad8..c7b3c825 100644 --- a/app/src/main/res/layout/activity_app_view.xml +++ b/app/src/main/res/layout/activity_app_view.xml @@ -8,18 +8,18 @@ android:paddingTop="@dimen/activity_vertical_margin" tools:context=".AppView" > - - + android:layout_below="@+id/appListText"> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/generic_grid_item.xml b/app/src/main/res/layout/pc_grid_item.xml similarity index 100% rename from app/src/main/res/layout/generic_grid_item.xml rename to app/src/main/res/layout/pc_grid_item.xml