From 68c1aaf433c940ee0a260df2fc25a2f5b3ba39aa Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 8 Nov 2014 01:07:21 -0800 Subject: [PATCH] Add new app view UI --- app/app.iml | 4 + app/build.gradle | 2 + app/src/main/java/com/limelight/AppView.java | 73 ++++----- app/src/main/java/com/limelight/Game.java | 2 +- .../com/limelight/grid/AppGridAdapter.java | 155 ++++++++++++++++++ .../limelight/grid/GenericGridAdapter.java | 4 + .../com/limelight/grid/PcGridAdapter.java | 2 +- app/src/main/res/drawable/image_loading.png | Bin 0 -> 21648 bytes app/src/main/res/layout/activity_app_view.xml | 16 +- app/src/main/res/layout/app_grid_item.xml | 22 +++ ...generic_grid_item.xml => pc_grid_item.xml} | 0 11 files changed, 232 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/com/limelight/grid/AppGridAdapter.java create mode 100644 app/src/main/res/drawable/image_loading.png create mode 100644 app/src/main/res/layout/app_grid_item.xml rename app/src/main/res/layout/{generic_grid_item.xml => pc_grid_item.xml} (100%) 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 0000000000000000000000000000000000000000..e03a5d7707e415f3cd7d02ea06ec26e098065366 GIT binary patch literal 21648 zcmeI4c|4Tg-|(-ojCBx+NQ5HUcVo|*Y+15awy`slb*xPY$yQmiFG;q@PL@Wtq+|&t zF%lxO%NEb2`t_afcDwKAbwAHv=JnEi=3JlUT<_24oH^%Quj!_)wi=9_i5vg`*g17& zJph1!A_O3Xf^X(-dA8si8A{#69RMik_WmG1aw;PLNS<;qF!nIk(u7+hUHC0+kX8tO zv(g;GM;JE;4*A@qH<4Xk~g ztfg$&<>km_&~VU!3&O*a4ejFW>JCTCvj6Z42j#tH0d}?@DjrUeJ{}O*RT6>LH!Qo>i&xzNKpW7i4qXv7Zms> zBOB}AHYiUw=O10Ou@*o$BU})!9`2x>&_CLt?2#TwcYEZ&g#3N^Uju_V(bD=I`yc!3 z;_{E7-91#iKm@;#{-dS4fe#8Hpoefrdb(L7RJ_1j;rOS!_i(WNBRPNaw0GrS`;I_6 z{EOM%m7iumO5taQWWalcE4d*oJ&E4KP@Bh zSCc>T{f9S%vZV(?Zm%BsgoOA6B@F}x;bIbSkv$nMD2V6er{-ULXd!JJY<>RbLkKj5 zi;3a;z|;Jz4{-e0SbA9gx4hu%;`y<$hT9_DTr55099%5z5CSMyI~jpL8u2uLR~1|d z>5Oy(yM>SwkrDW(W<0AOl?FfO>h59bYK=IjEC)95J2=?DttBkQM8zz{`9#GLR(w_x zqLO@);=;3;U#6P(CTPSr0cd$Nv@ah(<-XC+BKH}=1TmN&x+2O~WgtByVN9>I_S@!=7 zx!;r6?>TDE=Eul|TUzhUW^&eh0|Eg8{!{B8-TH0$%go{bHi19=wh(% ze!BXh_3uvHk+vS*mTm||JFq7HJ7fK)*FT&6)xC_s-t6z}@aIJQZ2fb7{!_7k&!^1Z zqy`6T0qmFMj~oce2>h+>kH|mujrVfu?D<1qLRb)d2?*`|`^yyn@?T9&{%ceG%YQZf z!_v^z!9z~?SG2!0<5}&^yg#P}eQ=S0kP{aZ6A_aU_-pfDEp#2w2xntu2XOql?^UI+ znD{TFzh3!AOOt=K#Jlp#@@KN}zR-W<@@Ff!TmV-p0>76ke@^r7q42*f{BcYF%Z~Bg z9R$Irv9El9YhOh83J19GY3wT>;Mx}vzQO@6d>Z@82e|e{gs*Ub3!lcm@&T@W5#cKw z;KHY|uY7=OUqtu{2e|NQ>?;Mx}vzQO@6d>Z@82e|e{gs*Ub3!lcm@_&ho{Er|1AzZ;< z{CR^v?YqvE^cwtGAe*(io)!T3o&*3)FaT`qg74D+;3)_IGZp{Y~D1N6{rnvaEHf9+a^cgEciP8tR7H(930M-4+GaJGTcNk>V2T7D~UT7!rI$F!MY zc*T%@<(g@w>HDDUL6eS-fwnuJG%=I<5fM>V^IY0<{R3n~e3@jNQhrZIS#jSTS=h$k zH7{>TOZm;&m3%XqH{JT+bh99chE|hOLxB{Ap*c&)7Sauno)87j{(t|t&0(p5(}7D% zHwOebd5LeH*Dy%sF$_tKl|7L|TgDQ(yD+cZO%m@4>o{Uq|1jd)le)!phvWc*p55>Q zJ>nR-I?Yq70w-GKqOH7zY`pUV6;Jq)J|@AWK|(?owmx7Qsh&$?8nf7uWD8}4KJNt2 z3aH&LAD!gLgv4IJmKAlkklP{KvDXVEX{?Ts_7h@uq40BvrV7p2d94RdX3C7rViV8fpoMnJUvz(#?a%|r~P#cRv|@R%qd@M>OZoCkF$++B$6N-n$7q>!OGU7^eK3*=0g zLeH08IgKZSGK3b?*iXjFN6b*)uY|?u!N5z;D<7@}pBN;}%VOgALRf%Sf9B>O>bR5S z)(u2xoVvStCu_+a7!7(lI$9SM1T07C)n@0m4XaDp1NSwAlSHXf8eg06EV-la%M6y( z^RqK3f1C`C3xW!2+z(uagfe*pZ!;WgLQm3qY2Q>%zWng8DCSU8Sy696sqe|d3;=f) zd$IN1ylpl;rx7TIB^fH{&pW3+Hy%Emg*{=={QdH!?q}bOFVu{q&Od(5mRQwQsgjOZ z@Pmr*jxo5iT;zLT0Ts-NyXOC3BoCWp(0unT!M4A3HK$fz#c2|l1#o&-Y1iu3cbb~+ zsj#o#Yha?Bw3AGZO{cM59d@R+@2uoIao;F>Z5TJ(&Fee`D>EVfr`~yVN)@1K6zKaG zZW9A_&$`u?rVUv7affl%oOjQScTaC^le62EORvADI9wx~V6U!6&IDi{`AvOWt1qX) zcFdAPu0ontOVQ2LO}^JHm=#XBUyHXtR|u$;GZPi{Q6zI|y<3=b=+pl45JJ=qENu*& zHvOzac&`mb%FB}Sq*YYQT?EFa;QZkxLkz*@DllA3Xu*Q<)HR{#`6f3pf9k9dG{_Ai zSAyjyZDEM+mGhYywIh+DBYpD@bBjUXxX}^Sp|GZ1DADWine*iJl>Ld$WDUI^qCA67 zhjiEoBMMU11P#_gJ~OD;=t#QJC{sfPyVTAQczzkwt~rW5`V#PuMwm<(#t%VoqX8)C zLWm6c^N84}39;45QPp$Jx`wsOV)Vyc!;ckUB_Ok3zl%YnL}(=#x&eO;yQxdozIA8b zpYV-TLyiRiCgD}mg2Ib6wOxF$_ohHF{g-M$aa7F_JSu@^HFgzKiIXi;%S@d3a*Rl z?iqxiLOb+#M`j-yPMdcPkwvFdwo18P5um~4sb`M~F=1GPCtx2+GLC{NBFloSj*?!1-r%>j_E^=&aSKm5qWm??*9u)p&0rw?Lu zI-#HG$!BMBtMWhY7`rszRP;UGxW)9ie7RVt>g_9js+}nERB4}?&}l`AK;WMET*Z$1 zqgtA=krGmrSXDaN=ja~Y;iRNU&c-uDGjsVh^jHt0T(clsv1be|+2y0E*qh3eUW7F= ziDqvorR!MWq{Cf`jV6T8r&69~5|2NHM0%U_eVyftl=7Z@nKUmlET7`yN#uRsxV?zXHKL8V z%MOYYy3>&}b~sX{m^`q_EYw~!*?yfW_*(ltq4McR_Sq*0p8Ei*yM}oj*Uj!O-hdnv z4#?;umK;{mnwhDdz#Uy|lWF$F+KXE5Xs}fRs^y~Ix8j_qb0FwQFbn6E1ILm_3ek^W z^Y7YTO4~SP?X#dvJ9#HijVi=ygZok$;{7vD81{^D0==Hue2_HNv;lWhHv_=@nOpb2F~3m+c5$(c?&RDTLotJn{8g9U!LF5s!rv=Mm0;J z$QG`h()u9Ckyl6HBTmR2w|2#J7Dv+kC?bh+r{_e1eT|8scJo5R3A7e@*P`MG?aRAI z6&g8K#pr#Gv_7(#y{Kl}GV_v)G5LNfS%q0xMAlQng^d&2FZDulgqXxeDasPkUjoN& zJ4GDZ7RUHd?yw^WJSXX_IxPV>ba8%O>1uzjYd*|(!B3db_|7?%WARpj@WKK&$D)C4 zfl#Z(4ja8NMji%8_*nTUw1KEqDIhw)I^cWiszFx@l&!X+vfy6bCeMQYae)a{j^}YV zN~gI=+D5YB=ZGf?mZAYCnrAQU-)xH*F4aIjiSU&h^q7j4Vz$_ChStuCw+NtT+${{v zFUl0Q&^?%teOT5>BR`g;g7tqgk-|OVz4L`7gt?$lXXqKfl0DFxFquJc#fDgVc`+*lKu1YlaRFL zt*ta_=#q}C>`Akrz!_e$dtiqKdNApaB7%Y0P=!?ft)zNLlL&OBTI6HYd2CEbI4jIz zTF=QpOJ|-wGu*2aUM#p zo-;5m3veuB$rs@!T#SN{t~hH*0u9Pt+2kz@PC!!Rs+5u^U*8af{J59bx2622AxE)c z{?SeHq?=0m+ysmDkPw&q8B4I#pqeROnX^zAKx8I*O9Tisk`)wxWWKSz9Cv-)k~|YL zqC-X^xEp5LVbhyM+d>V+ppessH1+3?dWRox7B9}BY>|MXUY52uQpD3W2@kTIyVq5m zFR`o25{{$=#a^b2?Cu)P)?r(eVMJca7$ z7d2y8I_@_m@Ld+Iq@j`}v2X+`{8>*(#oBcrvIX8t+D@icvik}X=<*ctOo^D3+5+QY>-zd?b`NF zspJ9)#;ql!LvWSs64n7tw~^&<2C0{Ks>toWoE|{j_vlz;>NPMLd`8QJWy?z!a|&h> zW-ya);(dWDflijb8DxzM@pC15JkNc_YjabK?v5y?Nt@Ve(hX9~-#anYZUu7)jsDi* zm6NPIQITzKjyhL-|3N5JQ2k<&c|bftOym+G!`){11|~uPPhQd?DQ1fT1F{08MU3rNeb`q0hcyTOT|NO<}c>k=ue* zPQ2;yx#+7X<63OHj?Nf)LwQ%Ij&DTAITe~;x&BA5K_g||qD&*I_7UdJ z2p{3fD2=|@f0Cbh*cbNvgb3{4EN6zpjww8Bwl~|Aa3u~?3XjZvSe(A<3Q01oTl#Bp71I#G~ zQXJj-aw3vt{_dQ58YFYz!;u^Mlv-X9V{${ss}G}XT3_@V33=4(6w6j-gb(9x zimKRLPw}D`uU~uIx%onyWTiyO(ycwZ?Zwe?S4wi6;3d&qla_7Wz1)M+$!#-BKP zlPKkq>#fuz^?SRoz*?@2PPi6kkv~XMa?Oh3m@KF1;QBF`1u3t?wa=Z}c_{8Q|3g6%l5oq>Ro$A!$2Ln0fSjt;yKD$5&Ap@7aG(iC+2$K^`0GZ7AnN{ELKrv(QM`{-h zacZdqu?lxzSG?3y(e12a@l3V0^3+Nb1!yIEr$^@wogFg)=m>e6nMpeWd-67pKLj|{ zmv?kY*BT(yfxugsxa&CR^RU^}Q(w+_07y>cZNu^){7&#^|;SviL}|aa+?g$4ZC+ zbwR$1|MjE5A|JS~zsuOxFq_DF|Je}-u#>4!h3(FPjjyhUKhkXs)s*`3Jp@`3KS`TT z=HvKz7T-}2u$wa9l(4l2S|lo}&FT`u8%djt3ra7IP`q?xR+d#^O0PmvVbZ?yx6R6+hq>EhM|Bc z_~bipFo8jr87(ars8y?DMXo~?WMcTr(s?M^DNqYXU>J0|@k{@s*g^vG!ygKYnylS4Yn|S4;ZQ=%Jg| zIJE}iD77u;Ixk6`>TC#&5lu|W7jXBad|2}Gb$Q@(77wfK_EOwg?Le!EndHq)spcp4 zFV3=kG=;;DGcJU(v7fBFk=Y3`i1P9yzBgKOgOz#;dGX@cr9e+lPs^4eO+vQ7tU@Qt zj?GHC1VYIg(!^0Kr8P~?mtvQDBlUrg%mXD!D4*LwtUSIkgFzWk1%D-RCl*N41C=NB zbKI^A*{-6%M;~wrnW(&Kd3dhqyw*wDD1knJ^yn(?>C+dwCX}r?>|#wFU z0uFbUouzv*n}35=6DHMLGIqzCOXu>_z@du1&HU&uG)m1BkXf^qQx&b_la8;+pH~A4 z{C0B>s*(xv30If_IbZ&ikAy;xl@0r7znUGs`Et&7mOjHDzDjXpJ2PDCb9vV8WdDl% z0yMVsyQ^I(h5Q9v9K=S+iMXb!BHOMBb9 zGoiOP7!I$G01waV)2}0tI;~6ojT;n{t?V*&nw>XVsRDs7E_~(_o_vH3z}`6orizeJ zJ|rV4mw0^IUv1fvvf~2F#_O<;a0bznCT4!yqYl93FA7Ns^P~%fF|U?M+70N(dz{q@ zZe~d>_4S)SnJ^^|BvXhOj+1wpNig$2;TSsrl)Y2A!oXEQpfDl0TfxU;r|S6XMr7aZ z>4#uI7(wcMP*q;|8}JZ^BalX1_b5hkjF^LF8=%qaF)N==;I#HcgM&jRQTxNx9UKW} zBam!I0Rhxf6&}f+yN}A3mhkK}$#rnGU8lPGBmub-bR_a3&}ZJ8^HjaYTVB2aN~H~D z^Bv>9H2tYSLni5!=@m;-YQz&;mMr3N2L|G&gr3@KW-xU&VN6#TYkIfYP&L(kvyB?! z*z?(B%SMz_kO$ijz^MXG+!&KBu=d?L2RCG2KSuj>nkAev6FfIyx_@O&2HKtCHl6T( zXlRIsaY#C0DW>F2J;78B&-(R5@c=gY)%%l8_Xs547KR(`x;oe|@4m9ryBfOwFs7uo zLd=Mw&7J(%L|N5po~6XPrxq6tE1?uLSL3Q3+^;|bo>(>H64Is3X}cRQv^!=lFLvpe z2`;;KM?RZf@&r@LGS|}?R#HO2!}MQ`r@NofkgM5B>m@dfS`RxW=d!%P&d*k~t*w4W z5Y*BrmkxSAyS$wiE`MW@Sc-@fuNTmExqThiV0XN9PP zN{>5KJ&L|p3NIJq)-S^{VwlBa8P|Jnd{RbLM>T571U@@8=#+{~%l;nFuJnl$*z$Q4 z!@aVyw-zG`$1-9EUU8h?A+RVTNf~_@tDU#(cVGEE`|aGpOLt8siW*u5e2Hfu$H-#% zc`0HeIhg_@Fo*J9Uw1$H#W`H(~z8-vkDe?rGp6YVH;N>XQSKmVK2;C5? zsE+U_=GiU4j+jYk#>@KWevnOhG9j(_sDpGkH*;gVA>Nd6Su&G^2*M10}X49!z4L~#mV$%i)ILuNGjD>R3;92`_g5t_JOyKkWb!)}A?Iz?*xMVG#O#6h2#1u)I+k7%h~`74 zkuQ9YO&HPxj?~$71Sa_v1Y32!HmYaQT_KKP8*3#fGeFdmo8D92d91XY2|A~ ziC?cUpav8tKG{EdrwF>^P~AIqhKzgVUDSseSx_vP-8U%S+kb+G;MnlKckhmV{o*sV z^`sJIFGGKtsZcqsoEZloTGbbBJ;dqH#rEdY33W>UD? zQTc%CO{_T=w#UF)`Lx1S>StrDdyf=y!Q*qI+pmq)&aA+>sdAW_nb&!I!w&TpeyE^l zf?@nA!7miUc**14=OMF7Dxo0+y%D-&5qoE9dx!Q%!Gp1@ZzUSxX7q`~Hd9$+Ezku) z=Y~_JqwACtnGm#}W4EQ2{?#tx95Mwm`i;0~()#>}D`75M#n!f6Cg)Y`fkyVk^0lD3 zRLJw2K(6lXT}0(sw%-TNaIH&8cyUYB^HJ zC0GfsO!WXk;oza|c)Md%L9(vYfRdU$EYzFG-ixv{-G6C=UUB4RB2+<6Rjr+nPKh>} ztb&UD0{5v6T-8o?cBcjNJ0a^jk^X)zm$AFKkk~mpXcn3EDh-LGsbMrtf3wUTmLkHH zL*cx4gLqe$#&W?gW2U_w$9Gw$H6t=DBQ@%v=ZcuGw?2NoQga;3b6u`b@S;?Zp^oh8 z_ASjh@R`Zs=X=M_tg|dZUmsyd2tJru&a!u3Vtebum`l?@9-wbgPbOKSG~p6n&!4>; z99te?=mW-Z3l@xgB`p;#D@2)=S3t+2vcctXT`Tm&chuuUC|_8JUn=c5p= - - + 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