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